@bagelink/blox 1.15.531 → 1.15.533

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.
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
- import { f as fetchCmsSiteData, s as splitSitemapPathsByLocale, l as localeSitemapPath, g as generateSitemapXml, b as generateSitemapIndexXml } from "./prerender-VtzdjvzI.js";
5
+ import { f as fetchCmsSiteData, s as splitSitemapPathsByLocale, l as localeSitemapPath, g as generateSitemapXml, b as generateSitemapIndexXml } from "./validate-output-C-RpYkvf.js";
6
6
  const AUTO_SHARD_CAP = Number(process.env.BLOX_SSG_SHARD_CAP) || 6;
7
7
  const PREFETCH_BUDGET = Number(process.env.BLOX_SSG_PREFETCH_BUDGET) || 24;
8
8
  function resolveShardCount(shards) {
@@ -109,7 +109,7 @@ async function runSharded(opts) {
109
109
  }
110
110
  if (process.env.BLOX_SSG_LLM !== "0") {
111
111
  try {
112
- const { mergeLlmParts } = await import("./prerender-VtzdjvzI.js").then((n) => n.y);
112
+ const { mergeLlmParts } = await import("./validate-output-C-RpYkvf.js").then((n) => n.z);
113
113
  const mergeWebsite = website;
114
114
  const partFiles = Array.from({ length: total }, (_, i) => path.join(llmPartsDir, `part.${i}.json`));
115
115
  const { pages } = await mergeLlmParts({
@@ -26,7 +26,7 @@ const node_child_process = require("node:child_process");
26
26
  const os = require("node:os");
27
27
  const path = require("node:path");
28
28
  const process = require("node:process");
29
- const llmArtifacts = require("./prerender-Dc2Zl2PC.cjs");
29
+ const llmArtifacts = require("./validate-output-C3NhyFsU.cjs");
30
30
  const AUTO_SHARD_CAP = Number(process.env.BLOX_SSG_SHARD_CAP) || 6;
31
31
  const PREFETCH_BUDGET = Number(process.env.BLOX_SSG_PREFETCH_BUDGET) || 24;
32
32
  function resolveShardCount(shards) {
@@ -133,7 +133,7 @@ async function runSharded(opts) {
133
133
  }
134
134
  if (process.env.BLOX_SSG_LLM !== "0") {
135
135
  try {
136
- const { mergeLlmParts } = await Promise.resolve().then(() => require("./prerender-Dc2Zl2PC.cjs")).then((n) => n.llmArtifacts);
136
+ const { mergeLlmParts } = await Promise.resolve().then(() => require("./validate-output-C3NhyFsU.cjs")).then((n) => n.llmArtifacts);
137
137
  const mergeWebsite = website;
138
138
  const partFiles = Array.from({ length: total }, (_, i) => path.join(llmPartsDir, `part.${i}.json`));
139
139
  const { pages } = await mergeLlmParts({
package/dist/ssg/cli.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
  const process = require("node:process");
4
4
  const fs = require("node:fs/promises");
5
- const llmArtifacts = require("../prerender-Dc2Zl2PC.cjs");
5
+ const llmArtifacts = require("../validate-output-C3NhyFsU.cjs");
6
6
  function installFetchTimeout() {
7
7
  const ms = Number(process.env.BLOX_SSG_FETCH_TIMEOUT_MS) || 2e4;
8
8
  const original = globalThis.fetch;
@@ -56,6 +56,7 @@ async function main() {
56
56
  let shardsOpt = null;
57
57
  let routesCmd = "";
58
58
  let siteDataFile = "";
59
+ let validateOutDir = "";
59
60
  const passthroughArgs = [];
60
61
  const excludePaths = ["/_blox_preview"];
61
62
  for (const a of argv) {
@@ -71,6 +72,10 @@ async function main() {
71
72
  routesCmd = a.slice("--routes-cmd=".length);
72
73
  } else if (a.startsWith("--site-data=")) {
73
74
  siteDataFile = a.slice("--site-data=".length);
75
+ } else if (a === "--validate") {
76
+ validateOutDir = "dist";
77
+ } else if (a.startsWith("--validate=")) {
78
+ validateOutDir = a.slice("--validate=".length);
74
79
  } else if (a.startsWith("--shard=")) {
75
80
  const [i, n] = a.slice("--shard=".length).split("/").map(Number);
76
81
  if (Number.isInteger(i) && Number.isInteger(n) && n > 0 && i >= 0 && i < n) {
@@ -95,6 +100,7 @@ Options:
95
100
  --shard=<i/n> (internal) Render only this shard's slice — set by --shards.
96
101
  --routes-cmd=<cmd> Shell command whose stdout (whitespace-separated) is the
97
102
  route list. For router-driven sites with no CMS pages.
103
+ --validate[=dir] Validate generated SSG assets (default directory: dist).
98
104
  --help, -h Show this help
99
105
 
100
106
  Environment:
@@ -120,10 +126,15 @@ Environment:
120
126
  extraPaths.push(a);
121
127
  }
122
128
  }
129
+ if (validateOutDir) {
130
+ await llmArtifacts.validateSsgOutput({ outDir: validateOutDir });
131
+ console.log(`[blox-ssg] validated output in ${validateOutDir}`);
132
+ return;
133
+ }
123
134
  if (shardsOpt !== null || routesCmd) {
124
135
  const { apiBase: apiBase2, websiteName: configWebsiteName2 } = await resolveApiBase(mode);
125
136
  const websiteName2 = process.env.WEBSITE_NAME ?? configWebsiteName2 ?? "default";
126
- const { runSharded } = await Promise.resolve().then(() => require("../shard-runner-Bn4JyeQC.cjs"));
137
+ const { runSharded } = await Promise.resolve().then(() => require("../shard-runner-DTkniXr1.cjs"));
127
138
  const code = await runSharded({
128
139
  shards: shardsOpt ?? "auto",
129
140
  mode,
package/dist/ssg/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import process from "node:process";
3
3
  import fs from "node:fs/promises";
4
- import { u as uploadBloxManifest, p as polyfillBloxSsgGlobals, f as fetchCmsSiteData, a as prerender } from "../prerender-VtzdjvzI.js";
4
+ import { v as validateSsgOutput, u as uploadBloxManifest, p as polyfillBloxSsgGlobals, f as fetchCmsSiteData, a as prerender } from "../validate-output-C-RpYkvf.js";
5
5
  function installFetchTimeout() {
6
6
  const ms = Number(process.env.BLOX_SSG_FETCH_TIMEOUT_MS) || 2e4;
7
7
  const original = globalThis.fetch;
@@ -55,6 +55,7 @@ async function main() {
55
55
  let shardsOpt = null;
56
56
  let routesCmd = "";
57
57
  let siteDataFile = "";
58
+ let validateOutDir = "";
58
59
  const passthroughArgs = [];
59
60
  const excludePaths = ["/_blox_preview"];
60
61
  for (const a of argv) {
@@ -70,6 +71,10 @@ async function main() {
70
71
  routesCmd = a.slice("--routes-cmd=".length);
71
72
  } else if (a.startsWith("--site-data=")) {
72
73
  siteDataFile = a.slice("--site-data=".length);
74
+ } else if (a === "--validate") {
75
+ validateOutDir = "dist";
76
+ } else if (a.startsWith("--validate=")) {
77
+ validateOutDir = a.slice("--validate=".length);
73
78
  } else if (a.startsWith("--shard=")) {
74
79
  const [i, n] = a.slice("--shard=".length).split("/").map(Number);
75
80
  if (Number.isInteger(i) && Number.isInteger(n) && n > 0 && i >= 0 && i < n) {
@@ -94,6 +99,7 @@ Options:
94
99
  --shard=<i/n> (internal) Render only this shard's slice — set by --shards.
95
100
  --routes-cmd=<cmd> Shell command whose stdout (whitespace-separated) is the
96
101
  route list. For router-driven sites with no CMS pages.
102
+ --validate[=dir] Validate generated SSG assets (default directory: dist).
97
103
  --help, -h Show this help
98
104
 
99
105
  Environment:
@@ -119,10 +125,15 @@ Environment:
119
125
  extraPaths.push(a);
120
126
  }
121
127
  }
128
+ if (validateOutDir) {
129
+ await validateSsgOutput({ outDir: validateOutDir });
130
+ console.log(`[blox-ssg] validated output in ${validateOutDir}`);
131
+ return;
132
+ }
122
133
  if (shardsOpt !== null || routesCmd) {
123
134
  const { apiBase: apiBase2, websiteName: configWebsiteName2 } = await resolveApiBase(mode);
124
135
  const websiteName2 = process.env.WEBSITE_NAME ?? configWebsiteName2 ?? "default";
125
- const { runSharded } = await import("../shard-runner-JAI_Tb4S.js");
136
+ const { runSharded } = await import("../shard-runner-DFZAHrwJ.js");
126
137
  const code = await runSharded({
127
138
  shards: shardsOpt ?? "auto",
128
139
  mode,
@@ -22,7 +22,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
25
- const llmArtifacts = require("../prerender-Dc2Zl2PC.cjs");
25
+ const llmArtifacts = require("../validate-output-C3NhyFsU.cjs");
26
26
  const ssg_client = require("./client.cjs");
27
27
  const pinia = require("pinia");
28
28
  const vue = require("vue");
@@ -279,6 +279,7 @@ exports.prerender = llmArtifacts.prerender;
279
279
  exports.resolveManifestProvenance = llmArtifacts.resolveManifestProvenance;
280
280
  exports.splitSitemapPathsByLocale = llmArtifacts.splitSitemapPathsByLocale;
281
281
  exports.uploadBloxManifest = llmArtifacts.uploadBloxManifest;
282
+ exports.validateSsgOutput = llmArtifacts.validateSsgOutput;
282
283
  exports.BLOX_COLLECTIONS_WINDOW_KEY = ssg_client.BLOX_COLLECTIONS_WINDOW_KEY;
283
284
  exports.BLOX_DATA_WINDOW_KEY = ssg_client.BLOX_DATA_WINDOW_KEY;
284
285
  exports.BLOX_PAGE_SEO_WINDOW_KEY = ssg_client.BLOX_PAGE_SEO_WINDOW_KEY;
@@ -29,4 +29,6 @@ export type { LlmArtifactsConfig, LlmPageEntry } from './llm-artifacts';
29
29
  export { installBloxStateCache } from './state-cache';
30
30
  export { resolveManifestProvenance, uploadBloxManifest } from './manifest-upload';
31
31
  export type { ManifestProvenance, ManifestUpload } from './manifest-upload';
32
+ export { validateSsgOutput } from './validate-output';
33
+ export type { ValidateSsgOutputOptions } from './validate-output';
32
34
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ssg/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACvE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC9D,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAA;AACrH,YAAY,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AACrG;;;;;;;;;;GAUG;AACH,OAAO,EAAE,2BAA2B,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAC5J,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,YAAY,EAAE,YAAY,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AAC/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AACjG,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,OAAO,CAAA;AAC9M,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,mBAAmB,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACnH,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AACjF,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ssg/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACvE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC9D,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAA;AACrH,YAAY,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AACrG;;;;;;;;;;GAUG;AACH,OAAO,EAAE,2BAA2B,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAC5J,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,YAAY,EAAE,YAAY,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AAC/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AACjG,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,OAAO,CAAA;AAC9M,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,mBAAmB,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACnH,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AACjF,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,YAAY,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAA"}
@@ -1,5 +1,5 @@
1
- import { c as buildPageHead, d as primaryContextEntry, e as buildJsonLd, h as extractPageText, B as BloxBuildResponseError, i as bloxBuildJson } from "../prerender-VtzdjvzI.js";
2
- import { j, k, m, f, n, o, q, r, b, g, t, v, l, w, p, a, x, s, u } from "../prerender-VtzdjvzI.js";
1
+ import { c as buildPageHead, d as primaryContextEntry, e as buildJsonLd, h as extractPageText, B as BloxBuildResponseError, i as bloxBuildJson } from "../validate-output-C-RpYkvf.js";
2
+ import { j, k, m, f, n, o, q, r, b, g, t, w, l, x, p, a, y, s, u, v } from "../validate-output-C-RpYkvf.js";
3
3
  import { BLOX_STATE_WINDOW_KEY, BLOX_WEBSITE_ID_WINDOW_KEY } from "./client.mjs";
4
4
  import { BLOX_COLLECTIONS_WINDOW_KEY, BLOX_DATA_WINDOW_KEY, BLOX_PAGE_SEO_WINDOW_KEY, getCollectionCache, getEmbeddedWebsiteId, installBloxStateCache, installCollectionCache } from "./client.mjs";
5
5
  import { createPinia } from "pinia";
@@ -258,15 +258,16 @@ export {
258
258
  t as getBloxBuildRequestOptions,
259
259
  getCollectionCache,
260
260
  getEmbeddedWebsiteId,
261
- v as getSiteIconFlags,
261
+ w as getSiteIconFlags,
262
262
  installBloxStateCache,
263
263
  installCollectionCache,
264
264
  l as localeSitemapPath,
265
- w as mergeLlmParts,
265
+ x as mergeLlmParts,
266
266
  p as polyfillBloxSsgGlobals,
267
267
  a as prerender,
268
268
  renderBloxSsgPage,
269
- x as resolveManifestProvenance,
269
+ y as resolveManifestProvenance,
270
270
  s as splitSitemapPathsByLocale,
271
- u as uploadBloxManifest
271
+ u as uploadBloxManifest,
272
+ v as validateSsgOutput
272
273
  };
@@ -0,0 +1,6 @@
1
+ export interface ValidateSsgOutputOptions {
2
+ outDir: string;
3
+ requireFiles?: string[];
4
+ }
5
+ export declare function validateSsgOutput(options: ValidateSsgOutputOptions): Promise<void>;
6
+ //# sourceMappingURL=validate-output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-output.d.ts","sourceRoot":"","sources":["../../src/ssg/validate-output.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,wBAAwB;IACxC,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiDxF"}
@@ -1171,6 +1171,95 @@ function discoverInternalLinks(html) {
1171
1171
  }
1172
1172
  return [...out];
1173
1173
  }
1174
+ const sitemapUrl = /<url>\s*<loc>[^<]+<\/loc>/m;
1175
+ const sitemapIndexEntry = /<sitemap>\s*<loc>[^<]+<\/loc>/gm;
1176
+ const sitemapLoc = /<loc>([^<]+)<\/loc>/;
1177
+ const loadingShell = /class=["'][^"']*\bblox-initial-loading\b/;
1178
+ const embeddedCmsState = /\b__BLOX_(?:STATE|DATA)__\b/;
1179
+ async function validateSsgOutput(options) {
1180
+ const outDir = path.resolve(options.outDir);
1181
+ const failures = [];
1182
+ const requiredFiles = options.requireFiles ?? [
1183
+ "robots.txt",
1184
+ "_redirects",
1185
+ "sitemap.xml",
1186
+ "llms.txt",
1187
+ "llms-full.txt"
1188
+ ];
1189
+ const htmlFiles = await collectHtmlFiles(outDir);
1190
+ let hydratedFiles = 0;
1191
+ for (const file of htmlFiles) {
1192
+ const name = path.relative(outDir, file);
1193
+ const html = await fs.readFile(file, "utf8");
1194
+ if (embeddedCmsState.test(html)) hydratedFiles += 1;
1195
+ if (!/<title>[^<]+<\/title>/i.test(html)) failures.push(`${name}: missing page title`);
1196
+ if (!/<link\s[^>]*rel=["']canonical["'][^>]*>/i.test(html)) {
1197
+ failures.push(`${name}: missing canonical URL`);
1198
+ }
1199
+ if (!/<html[^>]*\slang=["'][^"']+["']/i.test(html)) failures.push(`${name}: missing html lang`);
1200
+ if (loadingShell.test(html) && !embeddedCmsState.test(html)) {
1201
+ failures.push(`${name}: contains only the Blox loading shell`);
1202
+ }
1203
+ }
1204
+ if (!htmlFiles.length) failures.push("output contains no HTML files");
1205
+ if (!hydratedFiles) failures.push("output contains no prerendered Blox hydration state");
1206
+ for (const required of requiredFiles) {
1207
+ try {
1208
+ const content = await fs.readFile(path.join(outDir, required), "utf8");
1209
+ if (required === "robots.txt" && /^Disallow:\s*\/\s*$/im.test(content)) {
1210
+ failures.push("robots.txt blocks the entire site");
1211
+ }
1212
+ if (required === "llms.txt" && !/^- \[[^\]]+\]\([^)]+\)/m.test(content)) {
1213
+ failures.push("llms.txt contains no page links");
1214
+ }
1215
+ if (required === "sitemap.xml") {
1216
+ failures.push(...await validateSitemap(outDir, content));
1217
+ }
1218
+ } catch {
1219
+ failures.push(`missing ${required}`);
1220
+ }
1221
+ }
1222
+ if (failures.length) {
1223
+ throw new Error(`Invalid Blox SSG output:
1224
+ ${failures.map((item) => ` - ${item}`).join("\n")}`);
1225
+ }
1226
+ }
1227
+ async function collectHtmlFiles(directory) {
1228
+ const entries = await fs.readdir(directory, { withFileTypes: true });
1229
+ const files = await Promise.all(entries.map(async (entry) => {
1230
+ const child = path.join(directory, entry.name);
1231
+ if (entry.isDirectory() && entry.name !== "server") return collectHtmlFiles(child);
1232
+ return entry.isFile() && entry.name.endsWith(".html") ? [child] : [];
1233
+ }));
1234
+ return files.flat();
1235
+ }
1236
+ async function validateSitemap(outDir, content) {
1237
+ var _a;
1238
+ if (sitemapUrl.test(content)) return [];
1239
+ const failures = [];
1240
+ const entries = content.match(sitemapIndexEntry) ?? [];
1241
+ if (!entries.length) return ["sitemap.xml contains neither page URLs nor child sitemaps"];
1242
+ let childUrlCount = 0;
1243
+ for (const entry of entries) {
1244
+ const loc = (_a = entry.match(sitemapLoc)) == null ? void 0 : _a[1];
1245
+ if (!loc) continue;
1246
+ const pathname = new URL(loc).pathname.replace(/^\/+/, "");
1247
+ const childPath = path.resolve(outDir, pathname);
1248
+ if (!childPath.startsWith(`${outDir}${path.sep}`)) {
1249
+ failures.push(`child sitemap escapes output directory: ${pathname}`);
1250
+ continue;
1251
+ }
1252
+ try {
1253
+ const child = await fs.readFile(childPath, "utf8");
1254
+ if (!sitemapUrl.test(child)) failures.push(`${pathname} contains no page URLs`);
1255
+ else childUrlCount += (child.match(/<url>/g) ?? []).length;
1256
+ } catch {
1257
+ failures.push(`missing child sitemap ${pathname}`);
1258
+ }
1259
+ }
1260
+ if (!childUrlCount) failures.push("sitemap index child files contain no page URLs");
1261
+ return failures;
1262
+ }
1174
1263
  export {
1175
1264
  BloxBuildResponseError as B,
1176
1265
  prerender as a,
@@ -1194,8 +1283,9 @@ export {
1194
1283
  splitSitemapPathsByLocale as s,
1195
1284
  getBloxBuildRequestOptions as t,
1196
1285
  uploadBloxManifest as u,
1197
- getSiteIconFlags as v,
1198
- mergeLlmParts as w,
1199
- resolveManifestProvenance as x,
1200
- llmArtifacts as y
1286
+ validateSsgOutput as v,
1287
+ getSiteIconFlags as w,
1288
+ mergeLlmParts as x,
1289
+ resolveManifestProvenance as y,
1290
+ llmArtifacts as z
1201
1291
  };
@@ -1194,6 +1194,95 @@ function discoverInternalLinks(html) {
1194
1194
  }
1195
1195
  return [...out];
1196
1196
  }
1197
+ const sitemapUrl = /<url>\s*<loc>[^<]+<\/loc>/m;
1198
+ const sitemapIndexEntry = /<sitemap>\s*<loc>[^<]+<\/loc>/gm;
1199
+ const sitemapLoc = /<loc>([^<]+)<\/loc>/;
1200
+ const loadingShell = /class=["'][^"']*\bblox-initial-loading\b/;
1201
+ const embeddedCmsState = /\b__BLOX_(?:STATE|DATA)__\b/;
1202
+ async function validateSsgOutput(options) {
1203
+ const outDir = path.resolve(options.outDir);
1204
+ const failures = [];
1205
+ const requiredFiles = options.requireFiles ?? [
1206
+ "robots.txt",
1207
+ "_redirects",
1208
+ "sitemap.xml",
1209
+ "llms.txt",
1210
+ "llms-full.txt"
1211
+ ];
1212
+ const htmlFiles = await collectHtmlFiles(outDir);
1213
+ let hydratedFiles = 0;
1214
+ for (const file of htmlFiles) {
1215
+ const name = path.relative(outDir, file);
1216
+ const html = await fs.readFile(file, "utf8");
1217
+ if (embeddedCmsState.test(html)) hydratedFiles += 1;
1218
+ if (!/<title>[^<]+<\/title>/i.test(html)) failures.push(`${name}: missing page title`);
1219
+ if (!/<link\s[^>]*rel=["']canonical["'][^>]*>/i.test(html)) {
1220
+ failures.push(`${name}: missing canonical URL`);
1221
+ }
1222
+ if (!/<html[^>]*\slang=["'][^"']+["']/i.test(html)) failures.push(`${name}: missing html lang`);
1223
+ if (loadingShell.test(html) && !embeddedCmsState.test(html)) {
1224
+ failures.push(`${name}: contains only the Blox loading shell`);
1225
+ }
1226
+ }
1227
+ if (!htmlFiles.length) failures.push("output contains no HTML files");
1228
+ if (!hydratedFiles) failures.push("output contains no prerendered Blox hydration state");
1229
+ for (const required of requiredFiles) {
1230
+ try {
1231
+ const content = await fs.readFile(path.join(outDir, required), "utf8");
1232
+ if (required === "robots.txt" && /^Disallow:\s*\/\s*$/im.test(content)) {
1233
+ failures.push("robots.txt blocks the entire site");
1234
+ }
1235
+ if (required === "llms.txt" && !/^- \[[^\]]+\]\([^)]+\)/m.test(content)) {
1236
+ failures.push("llms.txt contains no page links");
1237
+ }
1238
+ if (required === "sitemap.xml") {
1239
+ failures.push(...await validateSitemap(outDir, content));
1240
+ }
1241
+ } catch {
1242
+ failures.push(`missing ${required}`);
1243
+ }
1244
+ }
1245
+ if (failures.length) {
1246
+ throw new Error(`Invalid Blox SSG output:
1247
+ ${failures.map((item) => ` - ${item}`).join("\n")}`);
1248
+ }
1249
+ }
1250
+ async function collectHtmlFiles(directory) {
1251
+ const entries = await fs.readdir(directory, { withFileTypes: true });
1252
+ const files = await Promise.all(entries.map(async (entry) => {
1253
+ const child = path.join(directory, entry.name);
1254
+ if (entry.isDirectory() && entry.name !== "server") return collectHtmlFiles(child);
1255
+ return entry.isFile() && entry.name.endsWith(".html") ? [child] : [];
1256
+ }));
1257
+ return files.flat();
1258
+ }
1259
+ async function validateSitemap(outDir, content) {
1260
+ var _a;
1261
+ if (sitemapUrl.test(content)) return [];
1262
+ const failures = [];
1263
+ const entries = content.match(sitemapIndexEntry) ?? [];
1264
+ if (!entries.length) return ["sitemap.xml contains neither page URLs nor child sitemaps"];
1265
+ let childUrlCount = 0;
1266
+ for (const entry of entries) {
1267
+ const loc = (_a = entry.match(sitemapLoc)) == null ? void 0 : _a[1];
1268
+ if (!loc) continue;
1269
+ const pathname = new URL(loc).pathname.replace(/^\/+/, "");
1270
+ const childPath = path.resolve(outDir, pathname);
1271
+ if (!childPath.startsWith(`${outDir}${path.sep}`)) {
1272
+ failures.push(`child sitemap escapes output directory: ${pathname}`);
1273
+ continue;
1274
+ }
1275
+ try {
1276
+ const child = await fs.readFile(childPath, "utf8");
1277
+ if (!sitemapUrl.test(child)) failures.push(`${pathname} contains no page URLs`);
1278
+ else childUrlCount += (child.match(/<url>/g) ?? []).length;
1279
+ } catch {
1280
+ failures.push(`missing child sitemap ${pathname}`);
1281
+ }
1282
+ }
1283
+ if (!childUrlCount) failures.push("sitemap index child files contain no page URLs");
1284
+ return failures;
1285
+ }
1197
1286
  exports.BloxBuildResponseError = BloxBuildResponseError;
1198
1287
  exports.bloxBuildJson = bloxBuildJson;
1199
1288
  exports.bloxBuildRequest = bloxBuildRequest;
@@ -1220,3 +1309,4 @@ exports.primaryContextEntry = primaryContextEntry;
1220
1309
  exports.resolveManifestProvenance = resolveManifestProvenance;
1221
1310
  exports.splitSitemapPathsByLocale = splitSitemapPathsByLocale;
1222
1311
  exports.uploadBloxManifest = uploadBloxManifest;
1312
+ exports.validateSsgOutput = validateSsgOutput;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/blox",
3
3
  "type": "module",
4
- "version": "1.15.531",
4
+ "version": "1.15.533",
5
5
  "description": "Blox page builder library for drag-and-drop page building and static data management",
6
6
  "author": {
7
7
  "name": "Bagel Studio",