@bagelink/blox 1.18.3 → 1.18.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/{PreviewApp-BKrLRDBC.js → PreviewApp-9H9-jLVN.js} +1 -1
  2. package/dist/{PreviewApp-Kb3uTwtH.cjs → PreviewApp-D8ZBTG0j.cjs} +1 -1
  3. package/dist/{PreviewApp.vue_vue_type_style_index_0_lang-AC3W0NSJ.cjs → PreviewApp.vue_vue_type_style_index_0_lang-BYCayJs0.cjs} +1 -1
  4. package/dist/{PreviewApp.vue_vue_type_style_index_0_lang-BMGv1eSR.js → PreviewApp.vue_vue_type_style_index_0_lang-BwiLyRBw.js} +1 -1
  5. package/dist/{appRuntime-CWlPH_5r.js → appRuntime-DZSCSsnS.js} +39 -156
  6. package/dist/{appRuntime-BU2Ls-Y7.cjs → appRuntime-Z0AxTPxf.cjs} +17 -134
  7. package/dist/composables/useLocale.d.ts +3 -0
  8. package/dist/composables/useLocale.d.ts.map +1 -1
  9. package/dist/composables/useLocaleNav.d.ts.map +1 -1
  10. package/dist/createBloxApp.d.ts.map +1 -1
  11. package/dist/hydration.d.ts +2 -0
  12. package/dist/hydration.d.ts.map +1 -1
  13. package/dist/index.cjs +16 -9
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.mjs +44 -36
  17. package/dist/localeStrategy-OwRIn35C.cjs +207 -0
  18. package/dist/localeStrategy-hXlNof1E.js +208 -0
  19. package/dist/localeStrategy.d.ts +21 -0
  20. package/dist/localeStrategy.d.ts.map +1 -1
  21. package/dist/{shard-runner-zmTY7v3n.cjs → shard-runner-Bz46UqN8.cjs} +16 -7
  22. package/dist/{shard-runner-yZNy6pQy.js → shard-runner-CoFcUVGe.js} +17 -8
  23. package/dist/ssg/cli.cjs +10 -2
  24. package/dist/ssg/cli.mjs +10 -2
  25. package/dist/ssg/cms-routes.d.ts +7 -0
  26. package/dist/ssg/cms-routes.d.ts.map +1 -1
  27. package/dist/ssg/index.cjs +4 -2
  28. package/dist/ssg/index.mjs +19 -17
  29. package/dist/ssg/prerender.d.ts +5 -1
  30. package/dist/ssg/prerender.d.ts.map +1 -1
  31. package/dist/ssg/render-resolved-page.d.ts +2 -0
  32. package/dist/ssg/render-resolved-page.d.ts.map +1 -1
  33. package/dist/ssg/shard-runner.d.ts +3 -0
  34. package/dist/ssg/shard-runner.d.ts.map +1 -1
  35. package/dist/ssg/validate-output.d.ts +9 -0
  36. package/dist/ssg/validate-output.d.ts.map +1 -1
  37. package/dist/{validate-output-3fNPagvj.js → validate-output-B8pmE7hf.js} +115 -28
  38. package/dist/{validate-output-CrhXrC1l.cjs → validate-output-CbIQrCwa.cjs} +99 -12
  39. package/package.json +125 -124
  40. package/LICENSE +0 -21
  41. package/dist/pageTitle-B1mtDJKn.js +0 -59
  42. package/dist/pageTitle-wobGhtiZ.cjs +0 -58
@@ -3,7 +3,7 @@ import fs, { readFile } from "node:fs/promises";
3
3
  import path, { resolve } from "node:path";
4
4
  import process$1 from "node:process";
5
5
  import { pathToFileURL } from "node:url";
6
- import { r as resolvePageTitle, p as primaryPageContext, c as contextPageTitle } from "./pageTitle-B1mtDJKn.js";
6
+ import { r as resolvePageTitle, p as primaryPageContext, c as contextPageTitle } from "./localeStrategy-hXlNof1E.js";
7
7
  class BloxBuildResponseError extends Error {
8
8
  constructor(status, url, statusText) {
9
9
  super(`Build API ${status} ${statusText}: ${url}`);
@@ -72,6 +72,24 @@ async function bloxBuildJson(input, init = {}, options = {}) {
72
72
  if (response.status === 204) return null;
73
73
  return response.json();
74
74
  }
75
+ function buildPageSlugMap(localizedBundles, defaultLocale) {
76
+ var _a;
77
+ const map = {};
78
+ const defaultPages = ((_a = localizedBundles.find((result) => result.locale === defaultLocale)) == null ? void 0 : _a.bundle.pages) ?? [];
79
+ const canonicalById = new Map(
80
+ defaultPages.flatMap((page) => page.id && page.slug && !page.slug.includes(":") ? [[page.id, page.slug]] : [])
81
+ );
82
+ for (const { locale, bundle } of localizedBundles) {
83
+ for (const page of bundle.pages) {
84
+ if (!page.slug || page.slug.includes(":") || page.status && page.status !== "published") continue;
85
+ const canonical = page.id ? canonicalById.get(page.id) : void 0;
86
+ if (!canonical) continue;
87
+ const entry = map[canonical] ?? (map[canonical] = { [defaultLocale]: canonical });
88
+ entry[locale] = page.slug;
89
+ }
90
+ }
91
+ return map;
92
+ }
75
93
  function localizePath(slug, locale, defaultLocale) {
76
94
  if (locale === defaultLocale) return slug === "" ? "/" : slug;
77
95
  const s = slug === "" || slug === "/" ? "" : slug;
@@ -118,7 +136,7 @@ async function fetchCmsSiteData(apiBase, websiteName, requestOptions = {}) {
118
136
  return [{
119
137
  from_path: fromPath,
120
138
  to_path: localizePath(page.slug, locale, defaultLocale2),
121
- status_code: 302
139
+ status_code: 301
122
140
  }];
123
141
  });
124
142
  });
@@ -132,6 +150,7 @@ async function fetchCmsSiteData(apiBase, websiteName, requestOptions = {}) {
132
150
  website: website2,
133
151
  paths: paths2,
134
152
  pageSlugs: [...new Set(localizedBundles.flatMap((result) => result.bundle.pages.flatMap((page) => page.slug ? [localizePath(page.slug, result.locale, defaultLocale2)] : [])))],
153
+ pageSlugMap: buildPageSlugMap(localizedBundles, defaultLocale2),
135
154
  redirects: redirects2,
136
155
  collections: extractCollectionRefs(localizedBundles.flatMap((result) => result.bundle.pages)),
137
156
  collectionItems
@@ -175,7 +194,7 @@ async function fetchCmsSiteData(apiBase, websiteName, requestOptions = {}) {
175
194
  return [{
176
195
  from_path: fromPath,
177
196
  to_path: localizePath(page.slug, locale, defaultLocale),
178
- status_code: 302
197
+ status_code: 301
179
198
  }];
180
199
  });
181
200
  });
@@ -191,7 +210,18 @@ async function fetchCmsSiteData(apiBase, websiteName, requestOptions = {}) {
191
210
  const pages = localizedPages.flatMap((result) => result.pages);
192
211
  const collections = extractCollectionRefs(pages);
193
212
  const pageSlugs = [...new Set(localizedPages.flatMap((result) => result.pages.flatMap((page) => page.slug ? [localizePath(page.slug, result.locale, defaultLocale)] : [])))];
194
- return { websiteId: site.id, website, paths, pageSlugs, redirects, collections };
213
+ return {
214
+ websiteId: site.id,
215
+ website,
216
+ paths,
217
+ pageSlugs,
218
+ pageSlugMap: buildPageSlugMap(
219
+ localizedPages.map((result) => ({ locale: result.locale, bundle: { pages: result.pages } })),
220
+ defaultLocale
221
+ ),
222
+ redirects,
223
+ collections
224
+ };
195
225
  }
196
226
  async function expandRoutes(apiBase, pages, requestOptions, locale, collectItems) {
197
227
  const routes = [];
@@ -992,6 +1022,7 @@ async function prerender({
992
1022
  website = null,
993
1023
  websiteId = "",
994
1024
  redirects = [],
1025
+ pageSlugMap = {},
995
1026
  llm = {}
996
1027
  } = {}) {
997
1028
  var _a, _b, _c, _d, _e, _f;
@@ -1099,6 +1130,7 @@ async function prerender({
1099
1130
  template,
1100
1131
  website,
1101
1132
  websiteId,
1133
+ pageSlugMap,
1102
1134
  resolveCache,
1103
1135
  llm
1104
1136
  });
@@ -1242,7 +1274,7 @@ ${existing}`,
1242
1274
  totalDiscovered: seen.size
1243
1275
  };
1244
1276
  }
1245
- function normalizePath(p) {
1277
+ function normalizePath$1(p) {
1246
1278
  if (!p) return "/";
1247
1279
  try {
1248
1280
  if (p.startsWith("http://") || p.startsWith("https://")) {
@@ -1259,7 +1291,7 @@ function normalizePath(p) {
1259
1291
  return p || "/";
1260
1292
  }
1261
1293
  function enqueue(p, queue, seen, isExcluded) {
1262
- const np = normalizePath(p);
1294
+ const np = normalizePath$1(p);
1263
1295
  if (seen.has(np)) return;
1264
1296
  if (isExcluded(np)) return;
1265
1297
  seen.add(np);
@@ -1268,7 +1300,7 @@ function enqueue(p, queue, seen, isExcluded) {
1268
1300
  function makeExcludeChecker(excludePaths) {
1269
1301
  const rules = Array.isArray(excludePaths) ? excludePaths : [excludePaths];
1270
1302
  return (p) => {
1271
- const np = normalizePath(p);
1303
+ const np = normalizePath$1(p);
1272
1304
  for (const r of rules) {
1273
1305
  if (typeof r === "string" && r === "") continue;
1274
1306
  if (typeof r === "function") {
@@ -1276,7 +1308,7 @@ function makeExcludeChecker(excludePaths) {
1276
1308
  } else if (r instanceof RegExp) {
1277
1309
  if (r.test(np)) return true;
1278
1310
  } else if (typeof r === "string") {
1279
- const prefix = normalizePath(r);
1311
+ const prefix = normalizePath$1(r);
1280
1312
  if (np === prefix || np.startsWith(`${prefix}/`)) return true;
1281
1313
  }
1282
1314
  }
@@ -1514,6 +1546,60 @@ async function validateInternalLinks(outDir) {
1514
1546
  ${failures.map((item) => ` - ${item}`).join("\n")}`);
1515
1547
  }
1516
1548
  }
1549
+ async function validateNoRedirectingLinks(outDir) {
1550
+ const root = path.resolve(outDir);
1551
+ const redirectSources = await readRedirectSources(root, "301", "302", "307", "308");
1552
+ if (!redirectSources.size) return;
1553
+ const failures = [];
1554
+ for (const file of await collectHtmlFiles(root)) {
1555
+ const name = path.relative(root, file);
1556
+ const html = await fs.readFile(file, "utf8");
1557
+ for (const href of internalHrefs(html)) {
1558
+ const [withoutSuffix] = href.split(/[?#]/);
1559
+ const normalized = normalizePath(withoutSuffix || "/");
1560
+ if (redirectSources.has(normalized)) {
1561
+ failures.push(`${name}: links to redirecting URL "${href}"`);
1562
+ }
1563
+ }
1564
+ }
1565
+ if (failures.length) {
1566
+ throw new Error(
1567
+ `Invalid Blox SSG output:
1568
+ ${failures.slice(0, 25).map((item) => ` - ${item}`).join("\n")}`
1569
+ );
1570
+ }
1571
+ }
1572
+ async function readRedirectSources(root, ...statuses) {
1573
+ const sources = /* @__PURE__ */ new Set();
1574
+ let content = "";
1575
+ try {
1576
+ content = await fs.readFile(path.join(root, "_redirects"), "utf8");
1577
+ } catch {
1578
+ return sources;
1579
+ }
1580
+ for (const line of content.split("\n")) {
1581
+ const trimmed = line.trim();
1582
+ if (!trimmed || trimmed.startsWith("#")) continue;
1583
+ const parts = trimmed.split(/\s+/);
1584
+ if (parts.length < 3) continue;
1585
+ const status = parts[2];
1586
+ if (!statuses.includes(status)) continue;
1587
+ sources.add(normalizePath(parts[0]));
1588
+ }
1589
+ return sources;
1590
+ }
1591
+ function normalizePath(value) {
1592
+ const clean = value.split(/[?#]/)[0] || "/";
1593
+ return clean !== "/" ? clean.replace(/\/+$/, "") : "/";
1594
+ }
1595
+ function* internalHrefs(html) {
1596
+ const attr = /(?:<a\b[^>]*\shref=["']([^"']+)["'])|(?:data-to=["']([^"']+)["'])/gi;
1597
+ let match;
1598
+ while (match = attr.exec(html)) {
1599
+ const value = match[1] ?? match[2] ?? "";
1600
+ if (value.startsWith("/") && !value.startsWith("//")) yield value;
1601
+ }
1602
+ }
1517
1603
  const anchorHref = /<a\b[^>]*\shref=["']([^"']*)["']/gi;
1518
1604
  const ABSOLUTE_OR_SPECIAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/|\/|#|\?|$)/i;
1519
1605
  async function readSiteNoindex(outDir) {
@@ -1654,33 +1740,34 @@ async function validateSitemap(outDir, content) {
1654
1740
  return failures;
1655
1741
  }
1656
1742
  export {
1657
- splitSitemapPathsByLocale as A,
1743
+ resolveManifestProvenance as A,
1658
1744
  BloxBuildResponseError as B,
1659
- llmArtifacts as C,
1745
+ splitSitemapPathsByLocale as C,
1746
+ llmArtifacts as D,
1660
1747
  prerender as a,
1661
1748
  buildCollectionResolver as b,
1662
1749
  validateInternalLinks as c,
1663
- buildPageHead as d,
1664
- primaryContextEntry as e,
1750
+ validateNoRedirectingLinks as d,
1751
+ buildPageHead as e,
1665
1752
  fetchCmsSiteData as f,
1666
1753
  generateSitemapArtifacts as g,
1667
- buildJsonLd as h,
1668
- extractPageText as i,
1669
- bloxBuildJson as j,
1670
- bloxBuildRequest as k,
1671
- buildSiteHead as l,
1672
- generateLlmsFullTxt as m,
1673
- generateLlmsTxt as n,
1674
- generateNetlifyRedirects as o,
1754
+ primaryContextEntry as h,
1755
+ buildJsonLd as i,
1756
+ extractPageText as j,
1757
+ bloxBuildJson as k,
1758
+ bloxBuildRequest as l,
1759
+ buildSiteHead as m,
1760
+ generateLlmsFullTxt as n,
1761
+ generateLlmsTxt as o,
1675
1762
  polyfillBloxSsgGlobals as p,
1676
- generateRobotsTxt as q,
1677
- generateSitemapIndexXml as r,
1678
- generateSitemapXml as s,
1679
- getBloxBuildRequestOptions as t,
1763
+ generateNetlifyRedirects as q,
1764
+ generateRobotsTxt as r,
1765
+ generateSitemapIndexXml as s,
1766
+ generateSitemapXml as t,
1680
1767
  uploadBloxManifest as u,
1681
1768
  validateSsgOutput as v,
1682
- getSiteIconFlags as w,
1683
- localeSitemapPath as x,
1684
- mergeLlmParts as y,
1685
- resolveManifestProvenance as z
1769
+ getBloxBuildRequestOptions as w,
1770
+ getSiteIconFlags as x,
1771
+ localeSitemapPath as y,
1772
+ mergeLlmParts as z
1686
1773
  };
@@ -26,7 +26,7 @@ const fs = require("node:fs/promises");
26
26
  const path = require("node:path");
27
27
  const process$1 = require("node:process");
28
28
  const node_url = require("node:url");
29
- const pageTitle = require("./pageTitle-wobGhtiZ.cjs");
29
+ const localeStrategy = require("./localeStrategy-OwRIn35C.cjs");
30
30
  class BloxBuildResponseError extends Error {
31
31
  constructor(status, url, statusText) {
32
32
  super(`Build API ${status} ${statusText}: ${url}`);
@@ -95,6 +95,24 @@ async function bloxBuildJson(input, init = {}, options = {}) {
95
95
  if (response.status === 204) return null;
96
96
  return response.json();
97
97
  }
98
+ function buildPageSlugMap(localizedBundles, defaultLocale) {
99
+ var _a;
100
+ const map = {};
101
+ const defaultPages = ((_a = localizedBundles.find((result) => result.locale === defaultLocale)) == null ? void 0 : _a.bundle.pages) ?? [];
102
+ const canonicalById = new Map(
103
+ defaultPages.flatMap((page) => page.id && page.slug && !page.slug.includes(":") ? [[page.id, page.slug]] : [])
104
+ );
105
+ for (const { locale, bundle } of localizedBundles) {
106
+ for (const page of bundle.pages) {
107
+ if (!page.slug || page.slug.includes(":") || page.status && page.status !== "published") continue;
108
+ const canonical = page.id ? canonicalById.get(page.id) : void 0;
109
+ if (!canonical) continue;
110
+ const entry = map[canonical] ?? (map[canonical] = { [defaultLocale]: canonical });
111
+ entry[locale] = page.slug;
112
+ }
113
+ }
114
+ return map;
115
+ }
98
116
  function localizePath(slug, locale, defaultLocale) {
99
117
  if (locale === defaultLocale) return slug === "" ? "/" : slug;
100
118
  const s = slug === "" || slug === "/" ? "" : slug;
@@ -141,7 +159,7 @@ async function fetchCmsSiteData(apiBase, websiteName, requestOptions = {}) {
141
159
  return [{
142
160
  from_path: fromPath,
143
161
  to_path: localizePath(page.slug, locale, defaultLocale2),
144
- status_code: 302
162
+ status_code: 301
145
163
  }];
146
164
  });
147
165
  });
@@ -155,6 +173,7 @@ async function fetchCmsSiteData(apiBase, websiteName, requestOptions = {}) {
155
173
  website: website2,
156
174
  paths: paths2,
157
175
  pageSlugs: [...new Set(localizedBundles.flatMap((result) => result.bundle.pages.flatMap((page) => page.slug ? [localizePath(page.slug, result.locale, defaultLocale2)] : [])))],
176
+ pageSlugMap: buildPageSlugMap(localizedBundles, defaultLocale2),
158
177
  redirects: redirects2,
159
178
  collections: extractCollectionRefs(localizedBundles.flatMap((result) => result.bundle.pages)),
160
179
  collectionItems
@@ -198,7 +217,7 @@ async function fetchCmsSiteData(apiBase, websiteName, requestOptions = {}) {
198
217
  return [{
199
218
  from_path: fromPath,
200
219
  to_path: localizePath(page.slug, locale, defaultLocale),
201
- status_code: 302
220
+ status_code: 301
202
221
  }];
203
222
  });
204
223
  });
@@ -214,7 +233,18 @@ async function fetchCmsSiteData(apiBase, websiteName, requestOptions = {}) {
214
233
  const pages = localizedPages.flatMap((result) => result.pages);
215
234
  const collections = extractCollectionRefs(pages);
216
235
  const pageSlugs = [...new Set(localizedPages.flatMap((result) => result.pages.flatMap((page) => page.slug ? [localizePath(page.slug, result.locale, defaultLocale)] : [])))];
217
- return { websiteId: site.id, website, paths, pageSlugs, redirects, collections };
236
+ return {
237
+ websiteId: site.id,
238
+ website,
239
+ paths,
240
+ pageSlugs,
241
+ pageSlugMap: buildPageSlugMap(
242
+ localizedPages.map((result) => ({ locale: result.locale, bundle: { pages: result.pages } })),
243
+ defaultLocale
244
+ ),
245
+ redirects,
246
+ collections
247
+ };
218
248
  }
219
249
  async function expandRoutes(apiBase, pages, requestOptions, locale, collectItems) {
220
250
  const routes = [];
@@ -444,7 +474,7 @@ function buildPageHead(options) {
444
474
  const page = resolvedData == null ? void 0 : resolvedData.page;
445
475
  const ctx = extractPrimaryContext(resolvedData == null ? void 0 : resolvedData.contexts);
446
476
  const siteName = seo.site_name || void 0;
447
- const title = pageTitle.resolvePageTitle(resolvedData ?? {}, {
477
+ const title = localeStrategy.resolvePageTitle(resolvedData ?? {}, {
448
478
  override: override == null ? void 0 : override.title,
449
479
  siteName,
450
480
  defaultTitle: seo.default_meta_title
@@ -738,20 +768,20 @@ function getSiteIconFlags(website) {
738
768
  function extractPrimaryContext(contexts) {
739
769
  var _a;
740
770
  const empty = { title: null, description: null, image: null };
741
- const ctx = (_a = pageTitle.primaryPageContext(contexts)) == null ? void 0 : _a.ctx;
771
+ const ctx = (_a = localeStrategy.primaryPageContext(contexts)) == null ? void 0 : _a.ctx;
742
772
  if (!ctx) return empty;
743
773
  const str2 = (key) => {
744
774
  const v = ctx[key];
745
775
  return typeof v === "string" && v.trim() ? v.trim() : null;
746
776
  };
747
777
  return {
748
- title: pageTitle.contextPageTitle(ctx),
778
+ title: localeStrategy.contextPageTitle(ctx),
749
779
  description: str2("meta_description") || str2("excerpt") || str2("description") || str2("blurb") || str2("summary"),
750
780
  image: str2("og_image") || str2("cover_image_url") || str2("image_url") || str2("image")
751
781
  };
752
782
  }
753
783
  function primaryContextEntry(contexts) {
754
- return pageTitle.primaryPageContext(contexts);
784
+ return localeStrategy.primaryPageContext(contexts);
755
785
  }
756
786
  function esc(s) {
757
787
  return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1015,6 +1045,7 @@ async function prerender({
1015
1045
  website = null,
1016
1046
  websiteId = "",
1017
1047
  redirects = [],
1048
+ pageSlugMap = {},
1018
1049
  llm = {}
1019
1050
  } = {}) {
1020
1051
  var _a, _b, _c, _d, _e, _f;
@@ -1122,6 +1153,7 @@ async function prerender({
1122
1153
  template,
1123
1154
  website,
1124
1155
  websiteId,
1156
+ pageSlugMap,
1125
1157
  resolveCache,
1126
1158
  llm
1127
1159
  });
@@ -1265,7 +1297,7 @@ ${existing}`,
1265
1297
  totalDiscovered: seen.size
1266
1298
  };
1267
1299
  }
1268
- function normalizePath(p) {
1300
+ function normalizePath$1(p) {
1269
1301
  if (!p) return "/";
1270
1302
  try {
1271
1303
  if (p.startsWith("http://") || p.startsWith("https://")) {
@@ -1282,7 +1314,7 @@ function normalizePath(p) {
1282
1314
  return p || "/";
1283
1315
  }
1284
1316
  function enqueue(p, queue, seen, isExcluded) {
1285
- const np = normalizePath(p);
1317
+ const np = normalizePath$1(p);
1286
1318
  if (seen.has(np)) return;
1287
1319
  if (isExcluded(np)) return;
1288
1320
  seen.add(np);
@@ -1291,7 +1323,7 @@ function enqueue(p, queue, seen, isExcluded) {
1291
1323
  function makeExcludeChecker(excludePaths) {
1292
1324
  const rules = Array.isArray(excludePaths) ? excludePaths : [excludePaths];
1293
1325
  return (p) => {
1294
- const np = normalizePath(p);
1326
+ const np = normalizePath$1(p);
1295
1327
  for (const r of rules) {
1296
1328
  if (typeof r === "string" && r === "") continue;
1297
1329
  if (typeof r === "function") {
@@ -1299,7 +1331,7 @@ function makeExcludeChecker(excludePaths) {
1299
1331
  } else if (r instanceof RegExp) {
1300
1332
  if (r.test(np)) return true;
1301
1333
  } else if (typeof r === "string") {
1302
- const prefix = normalizePath(r);
1334
+ const prefix = normalizePath$1(r);
1303
1335
  if (np === prefix || np.startsWith(`${prefix}/`)) return true;
1304
1336
  }
1305
1337
  }
@@ -1537,6 +1569,60 @@ async function validateInternalLinks(outDir) {
1537
1569
  ${failures.map((item) => ` - ${item}`).join("\n")}`);
1538
1570
  }
1539
1571
  }
1572
+ async function validateNoRedirectingLinks(outDir) {
1573
+ const root = path.resolve(outDir);
1574
+ const redirectSources = await readRedirectSources(root, "301", "302", "307", "308");
1575
+ if (!redirectSources.size) return;
1576
+ const failures = [];
1577
+ for (const file of await collectHtmlFiles(root)) {
1578
+ const name = path.relative(root, file);
1579
+ const html = await fs.readFile(file, "utf8");
1580
+ for (const href of internalHrefs(html)) {
1581
+ const [withoutSuffix] = href.split(/[?#]/);
1582
+ const normalized = normalizePath(withoutSuffix || "/");
1583
+ if (redirectSources.has(normalized)) {
1584
+ failures.push(`${name}: links to redirecting URL "${href}"`);
1585
+ }
1586
+ }
1587
+ }
1588
+ if (failures.length) {
1589
+ throw new Error(
1590
+ `Invalid Blox SSG output:
1591
+ ${failures.slice(0, 25).map((item) => ` - ${item}`).join("\n")}`
1592
+ );
1593
+ }
1594
+ }
1595
+ async function readRedirectSources(root, ...statuses) {
1596
+ const sources = /* @__PURE__ */ new Set();
1597
+ let content = "";
1598
+ try {
1599
+ content = await fs.readFile(path.join(root, "_redirects"), "utf8");
1600
+ } catch {
1601
+ return sources;
1602
+ }
1603
+ for (const line of content.split("\n")) {
1604
+ const trimmed = line.trim();
1605
+ if (!trimmed || trimmed.startsWith("#")) continue;
1606
+ const parts = trimmed.split(/\s+/);
1607
+ if (parts.length < 3) continue;
1608
+ const status = parts[2];
1609
+ if (!statuses.includes(status)) continue;
1610
+ sources.add(normalizePath(parts[0]));
1611
+ }
1612
+ return sources;
1613
+ }
1614
+ function normalizePath(value) {
1615
+ const clean = value.split(/[?#]/)[0] || "/";
1616
+ return clean !== "/" ? clean.replace(/\/+$/, "") : "/";
1617
+ }
1618
+ function* internalHrefs(html) {
1619
+ const attr = /(?:<a\b[^>]*\shref=["']([^"']+)["'])|(?:data-to=["']([^"']+)["'])/gi;
1620
+ let match;
1621
+ while (match = attr.exec(html)) {
1622
+ const value = match[1] ?? match[2] ?? "";
1623
+ if (value.startsWith("/") && !value.startsWith("//")) yield value;
1624
+ }
1625
+ }
1540
1626
  const anchorHref = /<a\b[^>]*\shref=["']([^"']*)["']/gi;
1541
1627
  const ABSOLUTE_OR_SPECIAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/|\/|#|\?|$)/i;
1542
1628
  async function readSiteNoindex(outDir) {
@@ -1704,4 +1790,5 @@ exports.resolveManifestProvenance = resolveManifestProvenance;
1704
1790
  exports.splitSitemapPathsByLocale = splitSitemapPathsByLocale;
1705
1791
  exports.uploadBloxManifest = uploadBloxManifest;
1706
1792
  exports.validateInternalLinks = validateInternalLinks;
1793
+ exports.validateNoRedirectingLinks = validateNoRedirectingLinks;
1707
1794
  exports.validateSsgOutput = validateSsgOutput;