@uniweb/runtime 0.28.3 → 0.28.5

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.
package/dist/ssr.js CHANGED
@@ -23,6 +23,10 @@ function guaranteeItemStructure(item) {
23
23
  lists: item.lists || [],
24
24
  icons: item.icons || [],
25
25
  videos: item.videos || [],
26
+ // ⛔ An entry's own insets were left out until 2026-09-29, as `tables` was: the
27
+ // parser delivered them and this object, built key by key, did not carry them.
28
+ insets: item.insets || [],
29
+ media: item.media || [],
26
30
  snippets: item.snippets || [],
27
31
  buttons: item.buttons || [],
28
32
  data: item.data || {},
@@ -31,7 +35,8 @@ function guaranteeItemStructure(item) {
31
35
  forms: item.forms || [],
32
36
  quotes: item.quotes || [],
33
37
  headings: item.headings || [],
34
- ...item.math && item.math.length ? { math: item.math } : {}
38
+ ...item.math && item.math.length ? { math: item.math } : {},
39
+ ...item.tables && item.tables.length ? { tables: item.tables } : {}
35
40
  };
36
41
  }
37
42
  function guaranteeContentStructure(parsedContent) {
@@ -50,6 +55,9 @@ function guaranteeContentStructure(parsedContent) {
50
55
  icons: content.icons || [],
51
56
  videos: content.videos || [],
52
57
  insets: content.insets || [],
58
+ // The section's visual media — images, videos and insets on their own line — in
59
+ // the order written, each with its `kind`: what a `media` slot holds (2026-09-29).
60
+ media: content.media || [],
53
61
  snippets: content.snippets || [],
54
62
  buttons: content.buttons || [],
55
63
  data: content.data || {},
@@ -62,7 +70,12 @@ function guaranteeContentStructure(parsedContent) {
62
70
  // use them don't pay the allocation cost. Foundations that need them
63
71
  // should check for presence (content.math?.length) or use
64
72
  // content.sequence for in-order rendering.
73
+ //
74
+ // ⛔ `tables` was not passed through until 2026-09-29: the parser delivered it and
75
+ // this object, built key by key, left it out — so a component declaring `tables`
76
+ // received nothing, while `math`, its twin, arrived.
65
77
  ...content.math && content.math.length ? { math: content.math } : {},
78
+ ...content.tables && content.tables.length ? { tables: content.tables } : {},
66
79
  // Items with guaranteed structure
67
80
  items: (content.items || []).map(guaranteeItemStructure),
68
81
  // Sequence for ordered rendering
@@ -186,12 +199,13 @@ function warnUndeclaredBlocks(block, declared) {
186
199
  if (!block.website?.entityStore?.dev) return;
187
200
  const keys = new Set(declared.map(([key]) => key));
188
201
  for (const item of block.parsedContent?.sequence || []) {
189
- if (item?.type !== "dataBlock" || typeof item.tag !== "string" || keys.has(item.tag)) continue;
202
+ const kind = item?.type === "dataBlock" ? "data block" : item?.type === "concept_block" ? "concept block" : null;
203
+ if (!kind || typeof item.tag !== "string" || keys.has(item.tag)) continue;
190
204
  const memo = `${block.type}::${item.tag}`;
191
205
  if (warnedBlocks.has(memo)) continue;
192
206
  warnedBlocks.add(memo);
193
207
  console.warn(
194
- `[uniweb] ${block.type}: the \`${item.tag}\` data block is not in content.data — ${block.type} does not declare \`${item.tag}\` in its meta.js \`data:\`. It stays in content.sequence.`
208
+ `[uniweb] ${block.type}: the \`${item.tag}\` ${kind} is not in content.data — ${block.type} does not declare \`${item.tag}\` in its meta.js \`data:\`. It stays in content.sequence.`
195
209
  );
196
210
  }
197
211
  }
@@ -1221,18 +1235,19 @@ function createPageRenderer({ website, shell }) {
1221
1235
  const resolved = typeof target === "string" ? resolvePage(website, target) : target;
1222
1236
  if (!resolved) return { outcome: "notFound", html: null, page: null, error: null };
1223
1237
  const page = typeof resolved.getRenderableSelf === "function" ? resolved.getRenderableSelf() : resolved;
1238
+ const missing = page?.notFound === true;
1224
1239
  let result;
1225
1240
  try {
1226
1241
  result = renderPage(page, website);
1227
1242
  } catch (err) {
1228
- return { outcome: "failed", html: null, page, error: classifyRenderError(err) };
1243
+ return { outcome: missing ? "notFound" : "failed", html: null, page, error: classifyRenderError(err) };
1229
1244
  }
1230
- if (result.error) return { outcome: "failed", html: null, page, error: result.error };
1245
+ if (result.error) return { outcome: missing ? "notFound" : "failed", html: null, page, error: result.error };
1231
1246
  const html = injectPageContent(shell, result.renderedContent, page, {
1232
1247
  ...inject,
1233
1248
  sectionOverrideCSS: result.sectionOverrideCSS
1234
1249
  });
1235
- return { outcome: "rendered", html, page, error: null };
1250
+ return { outcome: missing ? "notFound" : "rendered", html, page, error: null };
1236
1251
  }
1237
1252
  return { website, render };
1238
1253
  }
package/dist/ssr.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ssr.js","sources":["../src/prepare-props.js","../src/background-shared.js","../src/default-404.js","../src/wire-foundation.js","../src/area-wrappers.js","../src/appearance.js","../src/document-title.js","../src/resolve-page.js","../src/ssr-renderer.js","../src/default-fetcher.js","../src/load-page-data.js","../src/page-renderer.js","../src/collect-records.js"],"sourcesContent":["/**\n * Props Preparation for Runtime Guarantees\n *\n * Prepares props for foundation components with:\n * - Param defaults from runtime schema\n * - Guaranteed content structure (no null checks needed)\n * - Field defaults applied to `content.data` items from the bound schemas\n *\n * This enables simpler component code by ensuring predictable prop shapes.\n */\n\nimport { isRichSchema } from '@uniweb/core'\n\n/**\n * Guarantee item has flat content structure\n *\n * @param {Object} item - Raw item from parser\n * @returns {Object} Item with guaranteed flat structure\n */\nfunction guaranteeItemStructure(item) {\n return {\n title: item.title || '',\n pretitle: item.pretitle || '',\n subtitle: item.subtitle || '',\n paragraphs: item.paragraphs || [],\n links: item.links || [],\n images: item.images || [],\n lists: item.lists || [],\n icons: item.icons || [],\n videos: item.videos || [],\n snippets: item.snippets || [],\n buttons: item.buttons || [],\n data: item.data || {},\n cards: item.cards || [],\n documents: item.documents || [],\n forms: item.forms || [],\n quotes: item.quotes || [],\n headings: item.headings || [],\n ...(item.math && item.math.length ? { math: item.math } : {}),\n }\n}\n\n/**\n * Guarantee content structure exists\n * Returns a flat content object with all standard fields guaranteed to exist\n *\n * @param {Object} parsedContent - Raw parsed content from semantic parser (flat structure)\n * @returns {Object} Content with guaranteed flat structure\n */\nexport function guaranteeContentStructure(parsedContent) {\n const content = parsedContent || {}\n\n return {\n // Flat header fields\n title: content.title || '',\n pretitle: content.pretitle || '',\n subtitle: content.subtitle || '',\n alignment: content.alignment || null,\n\n // Flat body fields\n paragraphs: content.paragraphs || [],\n links: content.links || [],\n images: content.images || [],\n lists: content.lists || [],\n icons: content.icons || [],\n videos: content.videos || [],\n insets: content.insets || [],\n snippets: content.snippets || [],\n buttons: content.buttons || [],\n data: content.data || {},\n cards: content.cards || [],\n documents: content.documents || [],\n forms: content.forms || [],\n quotes: content.quotes || [],\n headings: content.headings || [],\n\n // Rare collections — surfaced only when present so pages that don't\n // use them don't pay the allocation cost. Foundations that need them\n // should check for presence (content.math?.length) or use\n // content.sequence for in-order rendering.\n ...(content.math && content.math.length ? { math: content.math } : {}),\n\n // Items with guaranteed structure\n items: (content.items || []).map(guaranteeItemStructure),\n\n // Sequence for ordered rendering\n sequence: content.sequence || [],\n\n // Preserve raw content if present\n raw: content.raw,\n }\n}\n\n/**\n * Apply a schema to a single object\n * Only processes fields defined in the schema, preserves unknown fields\n *\n * @param {Object} obj - The object to process\n * @param {Object} schema - Schema definition (fieldName -> fieldDef)\n * @returns {Object} Object with schema defaults applied\n */\nfunction applySchemaToObject(obj, schema) {\n if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {\n return obj\n }\n\n const result = { ...obj }\n\n for (const [field, fieldDef] of Object.entries(schema)) {\n // Get the default value - handle both shorthand and full form\n const defaultValue = typeof fieldDef === 'object' ? fieldDef.default : undefined\n\n // Apply default if field is missing and default exists\n if (result[field] === undefined && defaultValue !== undefined) {\n result[field] = defaultValue\n }\n\n // Bare type strings ('string', 'decimal', …) carry nothing more to apply.\n if (typeof fieldDef !== 'object') continue\n\n // Inline picklist (`enum`): if the value is set but not among the allowed\n // values, fall back to the default. By value: an entry may be `{ value, label }`, as a\n // foundation built before its build lowered it to values still carries.\n if (Array.isArray(fieldDef.enum)) {\n const allowed = fieldDef.enum.map((e) => (e && typeof e === 'object' && 'value' in e ? e.value : e))\n if (result[field] !== undefined && !allowed.includes(result[field]) && defaultValue !== undefined) {\n result[field] = defaultValue\n }\n }\n\n // Nested object → recurse into its field map.\n if (fieldDef.type === 'object' && fieldDef.fields && result[field]) {\n result[field] = applySchemaToObject(result[field], fieldDef.fields)\n }\n\n // Array of objects → apply the element field map to each item.\n if (fieldDef.type === 'array' && fieldDef.items && Array.isArray(result[field])) {\n const items = fieldDef.items\n if (items && typeof items === 'object' && items.type === 'object' && items.fields) {\n result[field] = result[field].map((item) => applySchemaToObject(item, items.fields))\n }\n }\n }\n\n return result\n}\n\n/**\n * Apply a schema to a value (object or array of objects)\n *\n * @param {Object|Array} value - The value to process\n * @param {Object} schema - Schema definition\n * @returns {Object|Array} Value with schema defaults applied\n */\nfunction applySchemaToValue(value, schema) {\n if (Array.isArray(value)) {\n return value.map(item => applySchemaToObject(item, schema))\n }\n return applySchemaToObject(value, schema)\n}\n\n/**\n * Apply field defaults from a rich form `fields` array to an object.\n *\n * Recurses into `type: 'form'` (composite arrays with childSchema) and\n * `type: 'nestedObject'` / `type: 'object'` (single nested objects).\n *\n * Conditional visibility (`field.condition`) is not yet applied here —\n * components receive all fields the author filled plus defaults; hiding\n * is a later pass that requires the shared evaluateCondition util.\n *\n * @param {Object} obj - Row data (object keyed by field id)\n * @param {Array} fields - Rich field definitions\n * @returns {Object} - obj with defaults filled in\n */\nfunction applyRichFieldDefaults(obj, fields) {\n if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj\n if (!Array.isArray(fields)) return obj\n\n const result = { ...obj }\n\n for (const field of fields) {\n if (!field || typeof field !== 'object' || !field.id) continue\n const id = field.id\n\n if (result[id] === undefined && field.default !== undefined) {\n result[id] = field.default\n }\n\n if (field.type === 'form' && field.childSchema && Array.isArray(result[id])) {\n result[id] = result[id].map(item =>\n applyRichFieldDefaults(item, field.childSchema.fields)\n )\n } else if (\n (field.type === 'nestedObject' || field.type === 'object') &&\n Array.isArray(field.fields) &&\n result[id] &&\n typeof result[id] === 'object'\n ) {\n result[id] = applyRichFieldDefaults(result[id], field.fields)\n }\n }\n\n return result\n}\n\n/**\n * Apply a rich form schema to its stored value.\n *\n * Shape rules:\n * - composite (isComposite=true) → value is array of childSchema rows\n * - when `childRecords` is set, value may be `{ [childRecords]: [...] }`\n * - non-composite → value is a single object keyed by field id\n */\nfunction applyRichSchemaToValue(value, schema) {\n if (value == null) return value\n\n if (schema.isComposite && schema.childSchema) {\n const childFields = schema.childSchema.fields\n const queryKey = schema.childRecords\n\n if (queryKey && value && typeof value === 'object' && !Array.isArray(value)) {\n const arr = Array.isArray(value[queryKey]) ? value[queryKey] : []\n return {\n ...value,\n [queryKey]: arr.map(row => applyRichFieldDefaults(row, childFields)),\n }\n }\n\n if (Array.isArray(value)) {\n return value.map(row => applyRichFieldDefaults(row, childFields))\n }\n\n return value\n }\n\n if (Array.isArray(schema.fields)) {\n return applyRichFieldDefaults(value, schema.fields)\n }\n\n return value\n}\n\n/**\n * Apply schemas to content.data\n * Only processes tags that have a matching schema, leaves others untouched\n *\n * ## Two orders of schema — what a `data:` declaration may describe\n *\n * A component's `data:` is 1st order: a DEVELOPER says what shape the section\n * consumes. An authored form (```yaml:form```) is 2nd order: an AUTHOR says what\n * shape a VISITOR will submit. It is schema-shaped, but it is content.\n *\n * Declaring a schema for such a tag is legitimate, and it is worth being precise\n * about what it may describe:\n *\n * OK the DEFINITION's envelope — `title?`, `description?`, `fields: <map>`.\n * That asks \"is this a well-formed form?\", which is what build-time\n * validation is for (`build/src/validate-data.js` pairs a section's data\n * input with the schema its meta.js binds to that key).\n * WRONG a schema whose fields are THE FORM'S fields (`name`, `email`, …).\n * Those are author-chosen and unknowable at build time. A form-rendering\n * component receives its fields; it does not declare them.\n *\n * The mechanism is bounded and does not punish the mistake loudly:\n * `applySchemaToObject` recurses only where the schema declares structure\n * (`type: object` + `fields`, `type: array` + `items.fields`), so a\n * form-definition schema — which cannot name the author's fields — can never\n * reach into them. It fills the envelope defaults its own author declared.\n *\n * (Established with the editor team, 2026-07-31, channel frontend↔framework.\n * The editor shadows a foundation's `form` declaration with its own builder via\n * `builtinSchemas()`; that is about the EDITING UI and is orthogonal to whether a\n * foundation declares a schema for validation.)\n *\n * @param {Object} data - The data object from content\n * @param {Object} schemas - Schema definitions from runtime meta\n * @returns {Object} Data with schemas applied\n */\nexport function applySchemas(data, schemas) {\n if (!schemas || !data || typeof data !== 'object') {\n return data || {}\n }\n\n const result = { ...data }\n\n for (const [tag, rawValue] of Object.entries(data)) {\n const schema = schemas[tag]\n if (!schema) continue // No schema for this tag - leave as-is\n\n result[tag] = isRichSchema(schema)\n ? applyRichSchemaToValue(rawValue, schema)\n : applySchemaToValue(rawValue, schema)\n }\n\n return result\n}\n\n/**\n * Apply param defaults from runtime schema\n *\n * @param {Object} params - Params from frontmatter\n * @param {Object} defaults - Default values from runtime schema\n * @returns {Object} Merged params with defaults applied\n */\nexport function applyDefaults(params, defaults) {\n if (!defaults || Object.keys(defaults).length === 0) {\n return params || {}\n }\n\n return {\n ...defaults,\n ...(params || {}),\n }\n}\n\n/**\n * ⭐ `content.data` HOLDS THE KEYS THE COMPONENT DECLARES, AND NOTHING ELSE — ruled 2026-09-14\n * [Diego]. Every key in its `meta.js` `data:` and its foundation's `main.js` `data:`\n * (`website.declaredKeys`), each filled, in order, from:\n *\n * 1. what the section holds under that key (`block.heldData`) — a tagged data block, or\n * its own fetch's answer a static build prerendered into it;\n * 2. what the entity store delivered for it — the fetch that fills it (`fillDeclaredKeys`);\n * 3. `null` — nothing fills it, or its fetch is still out (`block.dataLoading`) or failed\n * (`block.dataError[key]`). ⭐ Never `[]`, which is an answer with no records.\n *\n * A tagged data block under a key the component does not declare is left out, and said so\n * in dev (`warnUndeclaredBlocks`); it stays in `content.sequence`. The data object is\n * rebuilt on each render from those sources, never from the last render's.\n *\n * ⛔ Until 2026-09-14 this merged every delivered key into what the section held, so a\n * section received every fetch that reached it and every tagged block it had.\n */\nfunction assembleData(block, meta, entityData) {\n const declared = block.website?.declaredKeys ? block.website.declaredKeys(meta) : []\n const held = block.heldData || {}\n const data = {}\n for (const [key] of declared) {\n if (held[key] !== undefined) data[key] = held[key]\n else if (entityData && entityData[key] !== undefined) data[key] = entityData[key]\n else data[key] = null\n }\n warnUndeclaredBlocks(block, declared)\n block.parsedContent.data = data\n}\n\n/**\n * Say once, in dev, that a tagged data block did not reach its section's component — its\n * key is not declared. The block is still in `content.sequence`, where a component that\n * renders the sequence finds it.\n */\nconst warnedBlocks = new Set()\nfunction warnUndeclaredBlocks(block, declared) {\n if (!block.website?.entityStore?.dev) return\n const keys = new Set(declared.map(([key]) => key))\n for (const item of block.parsedContent?.sequence || []) {\n if (item?.type !== 'dataBlock' || typeof item.tag !== 'string' || keys.has(item.tag)) continue\n const memo = `${block.type}::${item.tag}`\n if (warnedBlocks.has(memo)) continue\n warnedBlocks.add(memo)\n console.warn(\n `[uniweb] ${block.type}: the \\`${item.tag}\\` data block is not in content.data — ${block.type} does not ` +\n `declare \\`${item.tag}\\` in its meta.js \\`data:\\`. It stays in content.sequence.`\n )\n }\n}\n\n/**\n * Run the foundation-level data handler on a block, if one is\n * registered. Runs after entity data merge and before the content\n * handler — the handler sees the fully assembled data and can filter,\n * reshape, or augment it before Loom (or any content transform) runs.\n *\n * The handler receives `(data, block)` where data is\n * `block.parsedContent.data`. It returns a new data object, or\n * null/undefined for no change. The returned data replaces\n * `block.parsedContent.data` for all downstream processing — both\n * the content handler and the component see the transformed data.\n *\n * Skipped when the block is still waiting on async data\n * (`block.dataLoading`), or when no handler is registered.\n * Errors are logged and the original data is preserved.\n */\nfunction runDataHandler(block) {\n if (block.dataLoading) return\n const handler = globalThis.uniweb?.foundationConfig?.handlers?.data\n if (typeof handler !== 'function') return\n\n try {\n const result = handler(block.parsedContent.data, block)\n if (result != null && result !== block.parsedContent.data) {\n block.parsedContent.data = result\n }\n } catch (err) {\n console.error('Foundation data handler failed:', err)\n }\n}\n\n/**\n * Run the foundation-level content handler on a block, if one is\n * registered. Runs at prop-preparation time — after the data handler\n * has had a chance to filter/reshape the data — so the handler sees\n * the fully assembled (and possibly filtered) data. Replaces\n * `block.parsedContent` in place with the re-parsed, instantiated\n * form. The handler receives `(data, block)` and reads raw\n * ProseMirror from `block.rawContent`.\n *\n * Skipped when the block is still waiting on async data\n * (`block.dataLoading`), when no handler is registered, when the\n * block has no raw content, when the handler returns a no-change\n * signal (undefined, null, or the same reference as rawContent), or\n * when the handler throws. Errors are logged via `console.error`.\n */\nfunction runContentHandler(block) {\n if (block.dataLoading) return\n const handler = globalThis.uniweb?.foundationConfig?.handlers?.content\n if (typeof handler !== 'function') return\n if (!block.rawContent || Object.keys(block.rawContent).length === 0) return\n\n try {\n const transformed = handler(block.parsedContent.data, block)\n if (!transformed || transformed === block.rawContent) return\n const reparsed = block.parseContent(transformed)\n reparsed.data = block.parsedContent.data\n block.parsedContent = reparsed\n block.items = reparsed.items || []\n } catch (err) {\n console.error('Foundation content handler failed:', err)\n }\n}\n\n/**\n * Run the foundation-level props handler on the final { content, params }\n * before they reach the component. Runs after content parsing, param\n * defaults, content guarantees, and schema application — the handler\n * sees the exact shape the component would receive and can modify it.\n *\n * The handler receives `(content, params, block)` and returns a new\n * `{ content, params }` object, or null/undefined for no change.\n *\n * Use cases: post-parse content reshaping, computed fields derived\n * from both content and params, param-driven content reorganization.\n * Errors are logged and the original props are preserved.\n */\nfunction runPropsHandler(content, params, block) {\n const handler = globalThis.uniweb?.foundationConfig?.handlers?.props\n if (typeof handler !== 'function') return null\n\n try {\n const result = handler(content, params, block)\n if (result && typeof result === 'object') return result\n } catch (err) {\n console.error('Foundation props handler failed:', err)\n }\n return null\n}\n\n/**\n * Prepare props for a component with runtime guarantees.\n *\n * Does the full content-assembly pipeline in one place so both\n * renderers (`BlockRenderer.jsx` CSR and `ssr-renderer.js` SSG) share\n * the same code path:\n *\n * 1. Assemble `block.parsedContent.data` from the keys the component\n * declares: what the section holds, else what EntityStore delivered,\n * else `null` (`assembleData`).\n * 2. Run the foundation data handler (if registered) to filter or\n * reshape the assembled data.\n * 3. Run the foundation content handler (if registered) on the\n * block. This may replace `block.parsedContent` with a re-parsed,\n * instantiated version.\n * 4. Apply param defaults from meta.\n * 5. Build the guaranteed content structure.\n * 6. Apply schemas to content.data.\n * 7. Run the foundation props handler (if registered) for\n * post-processing of the final { content, params }.\n *\n * Steps 1–3 mutate the block (vanilla JS layer). Steps 4–7 are\n * pure derivations of the block's now-assembled state.\n *\n * @param {Object} block - The block instance\n * @param {Object} meta - Runtime metadata for the component (from meta[componentName])\n * @param {Object|null} [entityData] - Entity data resolved by EntityStore (null if none)\n * @returns {Object} Prepared props: { content, params }\n */\nexport function prepareProps(block, meta, entityData = null) {\n assembleData(block, meta, entityData)\n // ⭐ A list the section held before the store answered — its own fetch, prerendered\n // into its content by a static build — gets its records' `$route` by the store's rule,\n // which the store did not deliver it by (2026-09-14).\n block.website?.entityStore?.linkOwnRecords?.(block)\n runDataHandler(block)\n runContentHandler(block)\n\n // Apply param defaults\n const defaults = meta?.defaults || {}\n const params = applyDefaults(block.properties, defaults)\n\n // Guarantee content structure\n let content = guaranteeContentStructure(block.parsedContent)\n\n // Apply schemas to content.data\n const schemas = meta?.schemas || null\n if (schemas && content.data) {\n content.data = applySchemas(content.data, schemas)\n }\n\n // Post-process hook\n const adjusted = runPropsHandler(content, params, block)\n if (adjusted) {\n return {\n content: adjusted.content || content,\n params: adjusted.params || params,\n }\n }\n\n return { content, params }\n}\n\n/**\n * Get runtime metadata for a component from the global uniweb instance\n *\n * @param {string} componentName\n * @returns {Object|null}\n */\nexport function getComponentMeta(componentName) {\n return globalThis.uniweb?.getComponentMeta?.(componentName) || null\n}\n\n/**\n * Get default param values for a component\n *\n * @param {string} componentName\n * @returns {Object}\n */\nexport function getComponentDefaults(componentName) {\n return globalThis.uniweb?.getComponentDefaults?.(componentName) || {}\n}\n","/**\n * The two helpers the background renderers share — one home, because they are\n * not rendering.\n *\n * ## Why this module exists\n *\n * Backgrounds are an L3 pair by design: `components/Background.jsx` renders\n * with hooks and DOM, `ssr-renderer.js` renders with `React.createElement`\n * only, and the runtime-layer rule (framework `CLAUDE.md` gotcha #2) says that\n * pair is twinned on purpose. ⛔ **What it also says is that only the RENDERING\n * is twinned — \"pure data → L1, one home\" — and two pure helpers had been\n * dragged across the seam with it**: a base-path joiner and a colour-opacity\n * converter, byte-identical in both files.\n *\n * ⚠️ **And the copies had already drifted from the rule they were copying.**\n * The base-path joiner is `@uniweb/core`'s `applyBasePath`, whose own docblock\n * says it moved down to core *\"rather than grow a second copy (the failure\n * `@uniweb/core/route-match` was created to end, after one matcher was\n * implemented twice and the copies diverged)\"*. Two copies grew anyway, in the\n * runtime, and **both were missing its protocol-relative guard** — so an author\n * writing `//cdn.example.com/hero.jpg` on a site with `base: /docs/` got\n * `/docs//cdn.example.com/hero.jpg`. Measured 2026-09-06; the exact garbage\n * `applyBasePath` documents itself as existing to prevent.\n *\n * ⇒ The lesson is narrower than \"don't duplicate\": **when a twinned pair needs a\n * helper, the helper is the thing that must not be twinned.** A twin is a\n * commitment to keep two renderers in step; every pure function pulled inside it\n * silently joins that commitment.\n */\n\nimport { applyBasePath } from '@uniweb/core/base-path'\n\n/**\n * A site-root-relative URL with the deployment base applied.\n *\n * The base comes off the active website rather than being passed in, because\n * both callers render from the singleton and neither has it to hand. The join\n * itself is core's — protocol-relative and absolute URLs pass through, and an\n * already-based path is not based twice.\n *\n * @param {string} url\n * @returns {string}\n */\nexport function siteUrl(url) {\n return applyBasePath(url, globalThis.uniweb?.activeWebsite?.basePath || '')\n}\n\n/**\n * A colour with an alpha applied, for an overlay drawn over a background.\n *\n * Hex and `rgb()` / `rgba()` are converted; anything else — a named colour, a\n * `var(--token)`, `oklch(…)` — is returned unchanged, because a wrong guess\n * here paints the wrong colour rather than failing.\n *\n * @param {string} color\n * @param {number} opacity\n * @returns {string}\n */\nexport function withOpacity(color, opacity) {\n if (typeof color !== 'string' || !color) return color\n if (color.startsWith('#')) {\n const r = parseInt(color.slice(1, 3), 16)\n const g = parseInt(color.slice(3, 5), 16)\n const b = parseInt(color.slice(5, 7), 16)\n return `rgba(${r}, ${g}, ${b}, ${opacity})`\n }\n if (color.startsWith('rgb')) {\n const match = color.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/)\n if (match) return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${opacity})`\n }\n return color\n}\n","/**\n * Default 404 Page Content\n *\n * Single source of truth for the fallback 404 page shown when a site\n * has no custom 404 page defined. Used by:\n * - PageRenderer.jsx (client-side, as React elements)\n * - ssr-renderer.js generate404Html (build-time, as HTML string)\n *\n * The wrapper uses min-height + flex centering so the 404 content\n * renders at the same position regardless of parent layout context.\n * This prevents a visible flash when React hydrates over the SSR content.\n */\n\nimport React from 'react'\n\nconst styles = {\n wrapper: {\n minHeight: '80vh',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n padding: '2rem',\n textAlign: 'center',\n },\n heading: { fontSize: '3rem', fontWeight: 'bold', color: '#1f2937', marginBottom: '1rem' },\n message: { color: '#64748b', marginBottom: '2rem' },\n link: { color: '#3b82f6', textDecoration: 'underline' },\n}\n\n/**\n * React element for client-side rendering (PageRenderer).\n * Reads basePath from the runtime so the homepage link works\n * in subdirectory deployments (e.g., /sites/testproject).\n */\nexport function Default404() {\n const basePath = globalThis.uniweb?.activeWebsite?.basePath || ''\n const homeHref = basePath ? `${basePath}/` : '/'\n return React.createElement('div', { className: 'page-not-found', style: styles.wrapper },\n React.createElement('h1', { style: styles.heading }, '404'),\n React.createElement('p', { style: styles.message }, 'Page not found'),\n React.createElement('a', { href: homeHref, style: styles.link }, 'Go to homepage')\n )\n}\n\n/**\n * Static HTML string for SSR injection (generate404Html).\n *\n * @param {string} [basePath] - Base path prefix for the homepage link (e.g., '/sites/testproject')\n */\nexport function default404Html(basePath = '') {\n const homeHref = basePath ? `${basePath}/` : '/'\n return (\n `<div class=\"page-not-found\" style=\"min-height:80vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;text-align:center\">` +\n `<h1 style=\"font-size:3rem;font-weight:bold;color:#1f2937;margin-bottom:1rem\">404</h1>` +\n `<p style=\"color:#64748b;margin-bottom:2rem\">Page not found</p>` +\n `<a href=\"${homeHref}\" style=\"color:#3b82f6;text-decoration:underline\">Go to homepage</a>` +\n `</div>`\n )\n}\n","/**\n * Layer-2 wiring helpers: runtime ↔ Uniweb singleton.\n *\n * After `createUniweb()` constructs the singleton, the runtime fills a\n * few declared slots on it before the first render — foundation\n * capabilities (`defaultInsets`, `xref.build()`), per-request data\n * hydration into `website.dataStore`, locale-scoped content slicing.\n * This step is identical in every environment (browser SPA, SSG\n * prerender, cloud SSR) because it's plain data manipulation on a JS\n * object: no React rendering happens here, no hooks are called, no DOM\n * is touched, no `react-dom/server` is needed.\n *\n * That's why these helpers live in one file imported by both\n * `setup.js` (browser boot) and `ssr-renderer.js` (SSG/cloud-SSR boot),\n * instead of being duplicated into each. Things that genuinely differ\n * between environments — routing components, icon-cache hydration from\n * the DOM, the per-page render loop — stay in the per-environment\n * entries; these helpers cover only the environment-agnostic L2 work.\n *\n * Keeping this file React-free matters for the SPA bundle: `setup.js`\n * pulls `wire-foundation.js` directly, but it must NOT transitively\n * pull `ssr-renderer.js` (which imports `react-dom/server`). The L2\n * helpers therefore live here, while the L3-composing\n * `initPrerenderForLocale` lives in `ssr-renderer.js`.\n *\n * Adding a new framework-level capability:\n * 1. Read the foundation declaration via `foundation.default.capabilities.<name>`.\n * 2. Apply it to the uniweb singleton (set a slot, call a build hook,\n * register something on `activeWebsite`).\n * 3. Provide a runtime fallback if the capability is one foundations\n * may legitimately not declare (see `FallbackRef`).\n *\n * Foundation export shape contract: the runtime always loads the\n * **built** foundation artifact (`dist/entry.js`) via\n * `loadFoundation()` in `foundation-loader.js`, which does `import(url)`\n * and returns a module namespace. The build pipeline\n * (`@uniweb/build`'s `src/generate-entry.js`) wraps the foundation's\n * source default export under `default.capabilities.*`, so the runtime\n * sees a single canonical shape with no need for fallback chains. This\n * differs from `@uniweb/press` / `@uniweb/unipress`,\n * which DO need to handle a second shape because they're sometimes\n * called from inside a foundation bundle (where the foundation imports\n * its own source as a bare default object).\n */\n\nimport React from 'react'\nimport { deriveCacheKey, resolveDefaultLocale } from '@uniweb/core'\n// Leaf subpaths, not the package root: this file is pulled into the SSR/Worker\n// bundle, and `@uniweb/core` proper drags semantic-parser and theming with it.\nimport { resolveService, readServiceOptions } from '@uniweb/core/services'\nimport Tracker from '@uniweb/core/tracker'\nimport { buildTheme } from '@uniweb/theming'\n\n/**\n * Renders unhandled `[#id]` cross-reference markers as plain text. Used\n * when the active foundation didn't declare its own `<Ref>` via\n * `defaultInsets`. Pure `React.createElement` — safe in every\n * environment, including the hook-free SSR pipeline.\n *\n * Foundations that support cross-references override this by exporting\n * `defaultInsets: { Ref }` (with kit's xref-aware Ref) from their\n * source — the build pipeline carries it through into\n * `default.capabilities.defaultInsets`.\n */\nexport function FallbackRef({ params }) {\n return React.createElement(\n 'span',\n { className: 'xref xref--unhandled' },\n `[${params?.key || '?'}]`,\n )\n}\n\n/**\n * Wire foundation-declared capabilities onto a freshly constructed\n * Uniweb singleton. Called once, after `createUniweb()`, before any\n * rendering. Identical for SPA, SSG, and cloud SSR.\n *\n * @param {import('@uniweb/core').default} uniweb - From createUniweb(...).\n * @param {object} foundation - Loaded foundation module (built shape).\n */\nexport function wireFoundationCapabilities(uniweb, foundation) {\n const caps = foundation?.default?.capabilities || {}\n\n // defaultInsets: framework provides FallbackRef as the floor;\n // foundation overrides win. `getComponent()` on the Uniweb singleton\n // (core/uniweb.js) falls back to defaultInsets[name] when no\n // foundation/extension component matches — that's how `<Ref>` becomes\n // available to every foundation without each one having to register\n // it explicitly.\n uniweb.defaultInsets = { Ref: FallbackRef, ...(caps.defaultInsets || {}) }\n\n // xref: foundations supporting cross-references export\n // `xref.build(website, { foundationKinds })`. The runtime can't\n // import kit directly (kit is bundled into each foundation, not into\n // runtime, so only the foundations that use it pay for it), so it\n // dispatches through the foundation's reference. Foundations without\n // xref skip this entirely; kit's xref module never enters their\n // bundle thanks to tree-shaking at foundation-build time.\n if (caps.xref?.build && uniweb.activeWebsite) {\n caps.xref.build(uniweb.activeWebsite, {\n foundationKinds: caps.xref.kinds || {},\n })\n }\n}\n\n/**\n * Slice a multi-locale site-content payload to one locale.\n *\n * Sites published through the editor ship a single payload that carries\n * all locales nested under `content.locales[locale]` — `pages`, optional\n * `layouts`, and a `config` overlay. The default locale lives at the\n * top level (no nesting). This helper extracts the requested locale's\n * view as a fresh content object the rest of the runtime can consume\n * unchanged.\n *\n * Returns `content` as-is when `locale` is the default, missing, or not\n * present in `content.locales` — callers that already hand us locale-\n * scoped content (e.g., the framework's per-locale SSG path that loads\n * each `dist/{locale}/site-content.json` separately) get pass-through\n * behavior.\n *\n * The shape comes from the editor's publish payload, which is the canonical\n * form for multi-locale content — a server-side renderer consumes it\n * directly. Build-time SSG pre-flattens to one file per locale and so falls\n * into the pass-through case.\n *\n * @param {Object} content - Site content payload, possibly multi-locale.\n * @param {string} locale - Requested locale code.\n * @returns {Object} Content scoped to the requested locale.\n */\nexport function sliceContentForLocale(content, locale) {\n const defaultLang = resolveDefaultLocale(content?.config)\n const locData = content?.locales?.[locale]\n if (!locale || locale === defaultLang || !locData) return content\n return {\n pages: locData.pages,\n layouts: locData.layouts || content.layouts,\n config: {\n ...locData.config,\n i18n: content.config?.i18n,\n activeLocale: locale,\n },\n }\n}\n\n/**\n * Pre-populate a Website's DataStore from build-time / publish-time\n * fetched data so the dispatcher's first probe hits the cache instead\n * of refetching.\n *\n * The value MUST be wrapped as `{ data }`, or `cached.data` reads `undefined`. Three call sites\n * used to inline this loop independently (browser SPA, Node SSG, and a server-side renderer); one\n * of the three used the wrong shape, silently killing prefetched-data reuse wherever it ran. This\n * helper is the one canonical implementation.\n *\n * ⭐ **The KEY is the dispatcher's, not ours** (`fetcher.hydrate`). ⛔ This filed every entry under\n * `deriveCacheKey(entry.config)` until 2026-09-20, which is right for every request the framework's\n * own fetcher answers and wrong for one the site routes to a transport that declares its own\n * `cacheKey` — the documented case being a request whose identity depends on state its address does\n * not carry. Measured: such an entry landed where no `peek` looks, so every prerendered page of that\n * site rendered empty and the browser refetched it, with no error anywhere. One derivation, one\n * home — the dispatcher's, which is where selection happens.\n *\n * @param {import('@uniweb/core').Website} website\n * @param {Array<{config: Object, data: any}>} fetchedData\n */\nexport function hydrateDataStore(website, fetchedData) {\n if (!website?.dataStore || !fetchedData?.length) return\n for (const entry of fetchedData) {\n // A data step's list carries every declared config with an `outcome`; only what was actually\n // fetched enters the store. A list without outcomes (the SSG lane's) is all fetched.\n if (entry.outcome && entry.outcome !== 'fetched') continue\n // `meta` (the depth the records were fetched at) rides along, so the store\n // files them in its record index exactly as a runtime fetch would.\n const value = entry.meta ? { data: entry.data, meta: entry.meta } : { data: entry.data }\n if (typeof website.fetcher?.hydrate === 'function') {\n website.fetcher.hydrate(entry.config, value, { website })\n } else {\n // A graph with no dispatcher — a stub in a test, or a caller assembling one by hand.\n website.dataStore.set(deriveCacheKey(entry.config), value)\n }\n }\n}\n\n/**\n * Make sure the site's theme CSS exists on the graph, generating it from\n * the authored config when nothing upstream did.\n *\n * **The authored theme config is the source of truth in every lane;\n * generated CSS is a cache of it.** `uniweb build` fills that cache and\n * bakes the result into `<head>`, so this is a no-op on the static lane.\n * A lane that serves a site WITHOUT running the framework's build — a\n * backend-hosted SPA, a cloud shell-mode fallback — carries only the\n * authored `theme.yml` (that is the correct thing for a sync wire to\n * carry: `theme.css` is a build artifact, and with two publishers only\n * one of which computes it, shipping it would make a site's styling\n * depend on who published last). Without this helper those lanes render\n * with every semantic token unset — no colours, no backgrounds.\n *\n * Generating here rather than in a publish step is what keeps the\n * three-ingredient contract true: site + foundation + runtime converge\n * to a *styled* page with no fourth actor. It also stays one\n * implementation — the alternative was re-deriving the OKLCH shade math\n * in another language and keeping the two bit-compatible.\n *\n * L2, not L3: this reads and writes graph state and renders nothing, so\n * it has a single home here and both boot paths call it. **The\n * `@uniweb/theming` import is deliberately static.** An SSR isolate\n * loads a fixed modules map and cannot resolve a chunk graph, so the SSR\n * entry must include the generator statically; a lazy `import()` in the\n * browser entry only would mean two mechanisms for one behaviour,\n * drifting independently. Measured cost of the generator: ~4.9 KB gzip.\n *\n * Foundation-declared vars reach us through\n * `capabilities.vars` — emitted into `dist/entry.js` by\n * `@uniweb/build`'s `generate-entry.js`. Before that existed they lived\n * only in `dist/meta/schema.json` and a theme generated outside the\n * build silently lost every one of them.\n *\n * Callers own the \"should I?\" question, because it is environment-\n * specific: the browser entry skips this when the document already\n * carries a prerendered `<style id=\"uniweb-theme\">` (regenerating from\n * an already-processed config is wasted work at best), while the SSR\n * entry always runs it and lets `injectPageContent()` emit the result\n * idempotently.\n *\n * @param {import('@uniweb/core').default} uniweb - From createUniweb(...).\n * @param {object} foundation - Loaded foundation module (built shape).\n */\nexport function ensureThemeCss(uniweb, foundation) {\n const website = uniweb?.activeWebsite\n const themeData = website?.themeData\n if (!themeData || themeData.css) return\n\n const caps = foundation?.default?.capabilities || {}\n try {\n const { config, css, links } = buildTheme(themeData, {\n foundationVars: caps.vars || {},\n base: website.basePath || '/',\n })\n // Merge rather than replace: `config` is the processed superset (it\n // adds `palettes`, normalized `contexts`, resolved `fonts`), so this\n // also gives a build-less lane the same themeData shape the static\n // lane has — Theme.getPalette() and friends start working too.\n Object.assign(themeData, config, { css, links })\n } catch (err) {\n // This runs on the path taken when something upstream has already\n // gone wrong. A degraded render that is still legibly the site beats\n // one that looks broken, but neither is worth a boot crash.\n console.warn('[uniweb] theme CSS generation failed:', err?.message || err)\n }\n}\n\n/**\n * L2: give the site's tracker its destination.\n *\n * Replaces the disabled `Tracker` that `createUniweb` declares (see\n * `core/src/uniweb.js`) with a configured one, when — and only when — a\n * destination resolves. With none, the disabled default stays and every\n * `track()` call in the site remains a silent no-op, which is the default\n * state for the large majority of sites.\n *\n * ⛔ **WHY THE BASE PATH IS PASSED IN RATHER THAN READ OFF THE WEBSITE.**\n * `resolveService` joins a root-relative endpoint to `website.basePath`, and\n * that field is still `''` until `setBasePath()` runs — which happens later,\n * from `RuntimeProvider`. Resolving against the website as-is would silently\n * drop the prefix on every subdirectory deployment, and the symptom would be a\n * collector quietly receiving nothing. So the caller supplies the basename it\n * has already derived, and the lookup is done against that. `resolveService`\n * reads only `.config` and `.basePath`, so a plain object is a complete input.\n *\n * ⚖️ **Not called from the SSR path, deliberately.** The tracker is\n * browser-guarded, so wiring it there would produce a configured object that\n * can never emit — a slot that looks live and is not. The SSR twin has no\n * page-view effect either; suppression is structural rather than a flag.\n *\n * ## `scripts` — a vendor's own script, when the site declares one\n *\n * A second, independent path — vendor tags:\n * nothing is translated between our stream and theirs, and the framework never\n * learns which vendor it is. ⛔ **The loader is INJECTED rather than imported**,\n * because this file is pulled into the SSR/Worker bundle and a script loader is\n * DOM code. The browser entry passes one; the SSR path passes none, so there is\n * no branch to remember.\n *\n * @param {object} uniweb - the singleton\n * @param {object} [options]\n * @param {string} [options.basePath] - the deployment base (router basename)\n * @param {(urls: string[], opts: object) => void} [options.loadScripts] - DOM\n * loader for declared vendor scripts; omitted outside a browser entry\n */\n/**\n * What `tracking.emit` names, when a site names a preset rather than a list.\n *\n * ⭐ **`all` is deliberately ABSENT from this table.** It resolves to `null` —\n * *no narrowing* — so an event added in a later release is included without the\n * site republishing. A literal list would freeze `all` at the moment the site\n * was built and quietly stop meaning \"all\".\n *\n * ⚖️ **`standard` and `all` select the same events today, and that is not a\n * reason to drop one.** They diverge the moment a new automatic event ships:\n * `standard` is a curated set that a release cannot grow behind an operator's\n * back, `all` is the standing yes. The volume surprise is the thing being\n * avoided — a site that never changed should not start sending more.\n *\n * ⛔ **The curated set is the answer for a site that CONFIGURED ITS OWN\n * DESTINATION. It is NOT the answer for a site whose host supplies one** — see\n * `resolveEmit`, which is where absence stopped meaning one thing.\n */\nconst EMIT_PRESETS = {\n minimal: ['page_view'],\n standard: ['page_view', 'outbound_click', 'section_view']\n}\n\n/** The preset a site gets by declaring a destination and nothing else. */\nconst DEFAULT_EMIT = 'standard'\n\n/**\n * The site's own selection, as a list of event names or `null` for no narrowing.\n *\n * ⛔ **An unknown preset name resolves to the DEFAULT, not to nothing.** A typo\n * (`emit: sandard`) must not silently take a site dark: the failure mode of a\n * misread selection has to be \"you got the usual set\", never \"you got none and\n * nothing said so\".\n *\n * ## ⭐ ABSENCE MEANS TWO DIFFERENT THINGS, and this is where they part\n *\n * **A site that configured its own `endpoint` chose it.** Writing no `emit`\n * there means *\"the curated default\"*, and `standard` is exactly right — a\n * later framework release must not grow it behind that operator's back.\n *\n * **A site whose HOST supplies the collector has no endpoint of its own.** The\n * operator's whole relationship is *\"my host does analytics for me\"*, so\n * writing no `emit` there means **\"whatever my host offers\"** — not a list\n * frozen at the framework version the site was built against.\n *\n * ⇒ **Absent `emit` defers to the host's declared list when there is one, and\n * falls back to `standard` when there is not.** Returning `null` is how the\n * deferral is expressed: it is *no site-tier narrowing*, so `Tracker.arms()` is\n * left with the host's list as the only gate.\n *\n * ⭐ **Why this is a fix and not a relaxation.** §4 of the tracking design says\n * *\"the runtime emits what the SITE OWNER buys\"* — and before this, an owner\n * paying a host for analytics received a **framework-frozen subset** of what\n * that host stores and bills them for. The only way to close the gap was to\n * hand-edit YAML and republish, **a dependency with no symptom when forgotten**,\n * which is the precise failure that rule was written to reject.\n *\n * ⛔ **The fallback is NOT decoration — it is the standalone-first guarantee.**\n * A static host, a foreign backend, and any Uniweb backend predating the\n * `events` key all declare no list. Deferring unconditionally would arm *every*\n * event, forever, on exactly the sites the framework exists to serve without a\n * backend.\n *\n * ⚠️ **A host that declares an EMPTY list still means it** — `[]` is a\n * statement, not an absence, and it arms nothing. That is unchanged: `arms()`\n * has always read an empty host list that way. Only `undefined` means \"nothing\n * declared\".\n *\n * @param {string|string[]|undefined} emit - the site's own `tracking.emit`\n * @param {string[]|null} [hostEvents] - the host's declared list, or `null`\n * when the host declared none. **Only consulted when `emit` is absent**;\n * an author who names anything still wins.\n * @returns {string[]|null}\n */\nfunction resolveEmit(emit, hostEvents = null) {\n // ⛔ Absent is the ONLY branch that consults the host — this is a default,\n // never an override. `emit: minimal` on a host offering everything still\n // sends one event.\n if (emit == null) return hostEvents ? null : EMIT_PRESETS[DEFAULT_EMIT]\n if (Array.isArray(emit)) return emit\n if (emit === 'all') return null\n return EMIT_PRESETS[emit] || EMIT_PRESETS[DEFAULT_EMIT]\n}\n\nexport function wireTracker(uniweb, { basePath = '', loadScripts = null } = {}) {\n const website = uniweb?.activeWebsite\n if (!website) return\n\n // A plain lookup target: `resolveService` reads `.config` and `.basePath`\n // only, so this is the whole of what it needs and carries the *correct* base.\n const target = { config: website.config, basePath }\n\n const { url } = resolveService(target, 'tracking')\n const options = readServiceOptions(target, 'tracking')\n\n // Only whether any were declared — normalizing them is the loader's job, and\n // lives behind the loader's dynamic boundary so a site with none never\n // downloads that code either.\n const declaredScripts = options.scripts\n const hasScripts = Array.isArray(declaredScripts) ? declaredScripts.length > 0 : !!declaredScripts\n\n // Nothing declared on either count — keep the disabled default, nothing\n // armed, nothing queued. This is the state of the large majority of sites.\n if (!url && !hasScripts) return\n\n // The two narrowings, resolved here rather than in core: this is per-request\n // config reshaping, which is L2's job (see this file's header).\n //\n // ⛔ **`hostEvents` is read from the HOST tier only** — `config.services\n // .tracking.events`, never the merged view. A site cannot widen what a host\n // declined to store, and reading the merge would let it, silently, by writing\n // its own `events:` key.\n //\n // ⛔ **Absent stays absent.** No `events` from the host means NO NARROWING,\n // never an empty set: a host that sends no list is an older or simpler one,\n // and the other reading takes every site on it dark with every gate saying\n // yes. `?? null` rather than `?? []` is the whole of that guard.\n // ⛔ Each tier is read from ITS OWN key, not from the merged `options`. The\n // merge exists so a site's own option can override a host's — `consent`, say;\n // the address is not an option, and comes from `resolveService` above. These\n // two are not overrides of each other but answers to different questions, and\n // reading either off the merge would let one tier answer the other's — a site\n // writing `events:` would widen past what the host stores, silently.\n const hostTracking = website.config?.services?.tracking\n const siteTracking = website.config?.tracking\n const hostEvents =\n hostTracking && Array.isArray(hostTracking.events) ? hostTracking.events : null\n\n const tracker = new Tracker({\n endpoint: url,\n hostEvents,\n siteEmit: resolveEmit(siteTracking && siteTracking.emit, hostEvents),\n // ⭐ Read off the MERGED view, unlike the two above — and the difference is\n // the point. `events`/`emit` answer different questions per tier, so each is\n // read from its own key; this is one question with two possible answerers,\n // so the ordinary precedence applies: the host declares a batch window that\n // suits its collector, and a site's own `tracking:` overrides it. Absent on\n // both, `Tracker` keeps its default.\n //\n // ⛔ **A field being READABLE is not the same as it being AVAILABLE**, and\n // that is what made this line worth a test rather than a shrug.\n // `readServiceOptions` has always returned this key, so the plan read as\n // finished while nothing wrote the object being read — it would have shipped\n // as *\"we set the interval and it did nothing\"*, with all three lanes' suites\n // green. The value now has to reach `setInterval`, and a test asserts the\n // delay rather than the field.\n flushIntervalMs: options.flushIntervalMs,\n // Opt-in, not the default. Declaring a destination is itself the operator's\n // decision to track; requiring a second affirmative step would be the\n // framework presuming a jurisdiction on their behalf, which is exactly what\n // it must not do. A site that needs the gate asks for it.\n consentRequired: options.consent === 'required',\n debug: !!options.debug\n })\n uniweb.tracking = tracker\n\n if (!loadScripts || !hasScripts) return\n\n // The same suppression the tracker applies to its own events: a server render\n // or a framed authoring preview is not a visit, and a vendor's script must not\n // fire there either. One predicate in core, so the two cannot drift.\n if (!tracker.isLiveDocument()) return\n\n const load = () => loadScripts(declaredScripts, { basePath, debug: !!options.debug })\n if (tracker.consentStatus() === 'granted') load()\n else tracker.onGranted = load\n}\n","/**\n * What the runtime puts on a layout-area wrapper.\n *\n * When a foundation enables view transitions (the default), the runtime gives\n * each layout region a `view-transition-name` so the browser animates them\n * independently — persistent chrome (header, sidebar, footer) morphs in place\n * while the body crossfades. Without per-region names the browser falls back to\n * a single full-page crossfade, which makes the whole layout (chrome included)\n * flicker on every navigation.\n *\n * Naming is only half of it. A `view-transition-name` **makes its element a\n * stacking context**, so the moment the runtime adds these wrappers it has\n * decided how the areas paint relative to one another — and with no `z-index`\n * on them they all sit at `auto` and paint in DOM order, which puts the body\n * over the header on any layout that renders the header first.\n *\n * That is not theoretical. It is the same mechanism `@uniweb/kit`'s `Overlay`\n * exists for (a modal opened from the header, trapped inside `uw-header`), and\n * it made a real docs page's fixed header unclickable while the identical\n * header on the marketing layout was fine — because that layout's markup\n * happened to wrap its header area in `relative z-40`. A framework that\n * creates stacking contexts owes its users an order; leaving it to DOM order\n * means \"does my header work\" is answered by an accident of someone's JSX.\n *\n * So this module resolves BOTH halves of the wrapper — the transition name and\n * the stacking layer — and hands back the finished style. It is pure (no\n * React/DOM) so the SPA renderer (`components/Layout.jsx`) and the SSR renderer\n * (`ssr-renderer.js`) produce identical wrappers, keeping prerendered HTML and\n * the hydrated SPA aligned.\n */\n\n// Namespace so generated names can't collide with `view-transition-name`s a\n// foundation sets inside its own component CSS. The prefix also guarantees a\n// valid CSS <custom-ident> (starts with a letter).\nconst NS = 'uw-'\n\nconst toIdent = (name) => NS + String(name).replace(/[^a-zA-Z0-9_-]/g, '-')\n\n/**\n * Build the effective view-transition-name map for a layout.\n *\n * Default: every rendered area plus the implicit `body` gets a stable,\n * namespaced name (`uw-<area>`, `uw-body`). Same-named areas across layouts\n * therefore share a name and morph between layouts automatically.\n *\n * The layout's `meta.js` `transitions` value overrides this:\n * - an object overrides per region (`{ left: 'sidebar' }` to group across\n * layouts, or `{ left: null }` to opt one region out);\n * - `false` opts the whole layout out (back to the full-page crossfade).\n *\n * @param {string[]} areaNames - Names of the areas rendered for this page (excludes `body`).\n * @param {Object|false|null|undefined} explicit - `layoutMeta.transitions`.\n * @returns {Object|null} region → view-transition-name; `null` when opted out.\n * A region whose value is null/empty in the returned map gets no name.\n */\nexport function resolveLayoutTransitions(areaNames, explicit) {\n if (explicit === false) return null\n\n const transitions = { body: toIdent('body') }\n for (const name of areaNames) transitions[name] = toIdent(name)\n\n return explicit ? { ...transitions, ...explicit } : transitions\n}\n\n/**\n * The stacking layer of each area's wrapper.\n *\n * Default: every area except the body gets `1`, and the body gets nothing —\n * content is the backdrop, chrome is above it. That is the whole of what the\n * framework claims to know, and it is deliberately not more.\n *\n * The body is left unstacked rather than pinned to `0` on purpose. A layer\n * brings `position: relative` with it (see `areaWrapperStyle`), and a\n * positioned body wrapper would become the containing block for every\n * absolutely-positioned descendant on the page — a real behaviour change across\n * every site, to buy an ordering that lifting the chrome already achieves. An\n * unlayered body stays a plain stacking context and paints below anything with\n * a positive z-index, which is exactly the intent.\n *\n * In particular there is no default ordering BETWEEN chrome areas. Area names\n * are free-form (`header`, `footer`, `left` and `right` are conventions the\n * docs promote, but a foundation may define `topbar`, `rail`, `statusbar`,\n * anything), so ranking `header` above `left` would be the framework reading\n * meaning into a string it does not own — and would then behave differently for\n * a layout that spelled the same idea another way. Where two pieces of chrome\n * genuinely overlap, which one wins is a design decision, and the layout says\n * so with `layers`.\n *\n * The shape mirrors `transitions` exactly, so there is one thing to learn:\n * - an object overrides per region (`{ footer: 0 }`, `{ header: 5 }`), and a\n * region may be set to `null` to leave it unstacked;\n * - `false` opts the whole layout out, and the runtime then emits no\n * stacking at all — for a foundation that would rather own it in its own\n * markup, which is exactly what the marketing layout above was doing.\n *\n * Layers do NOT depend on view transitions. \"Chrome paints above content\" is a\n * property of the layout, not of how it animates — and body sections routinely\n * form their own stacking contexts (a section with a background isolates so its\n * background layer stays contained), so a fixed header in an unstacked sibling\n * area is not guaranteed to win against them either way. Tying the two together\n * was what left `DefaultLayout` hand-rolling its own `z-index: 40` on the\n * header: a second mechanism for the same job, which then swallowed `layers`\n * whole — a foundation could set `layers: { header: 0 }` on the default layout\n * and measurably nothing happened.\n *\n * @param {string[]} areaNames - Names of the areas rendered for this page (excludes `body`).\n * @param {Object|false|null|undefined} explicit - `layoutMeta.layers`.\n * @returns {Object} region → z-index. Empty when the layout opts out.\n */\nexport function resolveLayoutLayers(areaNames, explicit) {\n if (explicit === false) return {}\n\n const defaults = {}\n for (const name of areaNames) defaults[name] = 1\n\n return explicit ? { ...defaults, ...explicit } : defaults\n}\n\n/**\n * The finished inline style for one area's wrapper, or `null` when the region\n * needs no wrapper at all.\n *\n * Returning the whole style from one place is the point: the SPA and SSR\n * renderers each build these wrappers, and a rule applied in one and forgotten\n * in the other is invisible until a prerendered page and its hydrated self\n * disagree about what paints on top.\n *\n * `position: relative` rides along with a layer because `z-index` does nothing\n * on a static element. It is set only on regions that carry a layer, which is\n * why the default leaves the body at `0` rather than lifting everything: a\n * positioned body wrapper would become the containing block for every\n * absolutely-positioned descendant on the page, and the ordering does not need\n * it.\n *\n * @param {string} region - Area name, or `body`.\n * @param {Object|null} transitions - region → view-transition-name.\n * @param {Object} layers - region → z-index.\n * @returns {Object|null} Inline style object, or null for no wrapper.\n */\nexport function areaWrapperStyle(region, transitions, layers) {\n const style = {}\n\n const name = transitions?.[region]\n if (name) style.viewTransitionName = name\n\n const layer = layers?.[region]\n if (layer != null) {\n style.position = 'relative'\n style.zIndex = layer\n }\n\n return Object.keys(style).length > 0 ? style : null\n}\n","/**\n * Appearance — site-wide color scheme (light/dark).\n *\n * ONE resolver, reached two ways:\n *\n * 1. SPA boot — `initAppearance()` runs inside initRuntime, after initUniweb()\n * (so website.themeData.appearance is readable) and before\n * createRoot().render(). That position precedes React's first paint, so no\n * section renders with the wrong tokens and then flips, and it covers every\n * delivery mode because all three start() branches funnel into initRuntime.\n *\n * 2. Prerendered HTML — `renderAppearanceBootScript()` serializes the SAME\n * function into a synchronous <head> script. HTML that ships real body\n * content is styled from :root (light) tokens until a bundle loads, so\n * without this a dark visitor sees a flash of light. The script is emitted\n * by injectPageContent() in ssr-renderer.js, which every prerender lane\n * goes through — the framework's SSG and the cloud worker's JIT render\n * alike. Emitting it from a lane-specific injector is how the cloud lane\n * silently missed it once already.\n *\n * Why serialize instead of hand-writing the inline script: the two paths must\n * agree exactly. `applyBootScheme` is therefore written to be SELF-CONTAINED —\n * it references no module-scope binding, only its two arguments and the browser\n * globals it needs — so `Function.prototype.toString()` yields a script that\n * behaves identically to calling it directly. Keep it that way: an import, a\n * module const, or a helper call would survive `toString()` as an undefined\n * identifier at first paint. appearance.test.js pins the equivalence.\n *\n * Environment-neutral by construction. `applyBootScheme` no-ops its DOM writes\n * outside a browser, and `renderAppearanceBootScript` only stringifies — so\n * ssr-renderer.js can import this module in Node and in a server-side isolate.\n *\n * Two writers with independent resolution is the bug this replaced:\n * WebsiteRenderer used to re-apply `appearance.default` from an effect, and\n * because React runs child effects before parent effects it clobbered the\n * visitor's stored preference on every page load — the page came back light\n * while the toggle button still believed it was dark, making the next click a\n * no-op.\n */\n\nimport { hasDarkScheme } from '@uniweb/core'\n\nexport const APPEARANCE_STORAGE_KEY = 'uniweb-appearance'\nexport const DARK_SCHEME_CLASS = 'scheme-dark'\nexport const LIGHT_SCHEME_CLASS = 'scheme-light'\n\n/**\n * Resolve and apply the visitor's color scheme.\n *\n * Precedence: stored visitor preference → OS preference (when the site opts in)\n * → the theme's declared default.\n *\n * SELF-CONTAINED ON PURPOSE — see the module header. This function is both\n * called directly (SPA boot) and serialized with toString() into the pre-paint\n * <script> of prerendered HTML. It must never reference anything outside its own\n * arguments and the browser globals below; the storage key and class names are\n * inlined as literals rather than read from the exported constants for exactly\n * that reason.\n *\n * Written in ES5 so it needs no transpilation in the inline-script form, and\n * every browser access is guarded: Safari private mode throws on localStorage,\n * old webviews lack matchMedia, and Node has no document.\n *\n * @param {boolean} respectSystem - follow prefers-color-scheme when unset\n * @param {'light'|'dark'} fallback - the theme's declared default\n * @returns {'light'|'dark'} the scheme applied\n */\nexport function applyBootScheme(respectSystem, fallback) {\n var stored = null\n try {\n stored = localStorage.getItem('uniweb-appearance')\n } catch (e) {\n // Safari private mode and some embedded webviews throw on access\n }\n\n var hasStored = stored === 'light' || stored === 'dark'\n var scheme = hasStored ? stored : fallback\n\n if (!hasStored && respectSystem) {\n try {\n if (window.matchMedia('(prefers-color-scheme: dark)').matches) scheme = 'dark'\n } catch (e) {\n // No matchMedia — keep the declared default\n }\n }\n\n try {\n var root = document.documentElement\n // Always set an explicit class rather than relying on the absence of one.\n // `default: system` themes emit a `@media (prefers-color-scheme: dark)`\n // block scoped to `:root:not(.scheme-light)`, so forcing light on a dark OS\n // requires `scheme-light` to be present — removing `scheme-dark` alone would\n // leave the media query still applying dark tokens.\n if (scheme === 'dark') {\n root.classList.add('scheme-dark')\n root.classList.remove('scheme-light')\n } else {\n root.classList.add('scheme-light')\n root.classList.remove('scheme-dark')\n }\n } catch (e) {\n // No DOM (Node / prerender) — the resolved scheme is still returned\n }\n\n return scheme\n}\n\n/**\n * Reduce a theme's `appearance:` block to the two arguments applyBootScheme\n * takes, or null when the site can never show dark.\n *\n * THE ONLY PLACE `appearance.*` FIELDS ARE READ. Both the SPA boot and the\n * inline-script emitter go through here, so the two cannot disagree about what\n * `respectSystemPreference` defaults to. They used to: the runtime treated an\n * unset value as false while the script emitter and @uniweb/core's\n * Theme.getAppearance() treated it as true. Those agreed only by the grace of\n * @uniweb/theming's normalizeAppearance() always injecting the key — any path\n * handing raw theme.yml appearance to the runtime would have produced a\n * pre-paint script and a boot resolver that disagree, i.e. the exact\n * flash-then-flip this whole module exists to prevent. Unset means true, which\n * is what the docs promise and what core already did.\n *\n * The null gate is @uniweb/core's hasDarkScheme() — the same predicate\n * @uniweb/theming uses to decide whether `.scheme-dark` CSS is generated at all.\n * Sharing it means we can never apply a scheme that has no matching rules, and\n * a light-only site correctly gets no class and no inline script.\n *\n * @param {Object} [appearance] - the resolved theme.yml `appearance:` block\n * @returns {{respectSystem: boolean, fallback: 'light'|'dark'}|null}\n */\nexport function resolveAppearanceBoot(appearance) {\n if (!appearance || !hasDarkScheme(appearance)) return null\n\n return {\n respectSystem: appearance.respectSystemPreference !== false,\n fallback: appearance.default === 'dark' ? 'dark' : 'light',\n }\n}\n\n/**\n * Resolve and apply the boot scheme in the browser. Called by initRuntime.\n *\n * @param {Object} [appearance] - the resolved theme.yml `appearance:` block\n * @returns {'light'|'dark'|null} the applied scheme, or null when the site has\n * no dark scheme to switch to (nothing is written to the document)\n */\nexport function initAppearance(appearance) {\n const opts = resolveAppearanceBoot(appearance)\n if (!opts) return null\n\n return applyBootScheme(opts.respectSystem, opts.fallback)\n}\n\n/**\n * Emit the pre-paint <script> for prerendered HTML.\n *\n * Returns '' when the site has no dark scheme — a light-only page always renders\n * light, so there is nothing to correct before paint and no reason to ship the\n * bytes. Pure SPA builds don't need it either: the body is empty until the\n * bundle renders and initAppearance() runs before that first render.\n *\n * Only a boolean and a JSON-quoted 'light'/'dark' are interpolated, both derived\n * from resolveAppearanceBoot rather than taken from the theme verbatim, so\n * author-supplied theme.yml values cannot inject script.\n *\n * @param {Object} [appearance] - the resolved theme.yml `appearance:` block\n * @returns {string} a `<script>` tag, or '' when no script is needed\n */\nexport function renderAppearanceBootScript(appearance) {\n const opts = resolveAppearanceBoot(appearance)\n if (!opts) return ''\n\n const call = `(${applyBootScheme.toString()})(${opts.respectSystem}, ${JSON.stringify(opts.fallback)})`\n\n return `<script id=\"uniweb-appearance\">${call}</script>`\n}\n","/**\n * The DOCUMENT TITLE — what a browser tab, a bookmark and a search result show for a page.\n *\n * ONE rule, reached two ways (the shape `appearance.js` has for the color scheme):\n *\n * 1. SPA — `useHeadMeta` sets `document.title` on every navigation.\n * 2. Prerendered HTML — `injectPageContent()` in ssr-renderer.js writes `<title>`, for\n * every prerender lane: the framework's SSG and a host's render alike.\n *\n * ⛔ Until 2026-09-14 each side carried its own rule. The SPA wrote `<page> | <site>` and\n * the prerender wrote `<page>` alone, so every prerendered page changed its title the\n * moment the bundle loaded (`Home` → `Home | Acme`), and a crawler that runs no script\n * indexed the shorter one.\n *\n * `<page title> | <site name>`; the page title alone when the site has no name; the empty\n * string when the page has no title — a caller then leaves the title it already has.\n *\n * @param {string|number|null|undefined} pageTitle - the page's display title (`page.getTitle()`)\n * @param {string|null|undefined} siteName - the site's name (`website.name`)\n * @returns {string}\n */\nexport function documentTitle(pageTitle, siteName) {\n if (!pageTitle) return ''\n const title = String(pageTitle)\n return siteName ? `${title} | ${siteName}` : title\n}\n","/**\n * WHICH PAGE A ROUTE NAMES — the one lookup, importable without a renderer.\n *\n * ⭐ **It is one line, and it is a module because of who imports it.** `ssr-renderer.js` imports\n * `react-dom/server` at module scope, so anything reaching for this lookup through that file drags\n * the React server renderer into its own graph. The render's data step (`load-page-data.js`)\n * fetches and renders nothing, and did exactly that until 2026-09-20 — for `website.getPage`.\n *\n * ⚠️ **The docstring below already said \"no React … safe in a Worker isolate\", and was right.** It\n * was the FILE that made it false, which is the shape to watch for: a claim about a function's\n * dependencies is a claim about its module's imports, and nothing checks the two agree.\n * `tests/data-step-graph.test.js` checks it for the one caller that needs it.\n *\n * @module\n */\n\n/**\n * Resolve a route to the Page that should render it.\n *\n * Exists because `ssr-renderer.js` exported `renderPage(page, …)` and no supported way\n * to *get* a page — so every host rendering server-side wrote its own lookup,\n * and the obvious one (`website.pages.find(p => p.route === route)`) cannot\n * match a dynamic route, because the payload holds `/blog/:id` and the request\n * carries `/blog/1`. One host wrote that lookup three times in three files\n * before the gap was noticed. A renderer that takes a Page owes callers a Page.\n *\n * ⛔ Move it, never copy it: a second lookup here would be the fourth.\n *\n * This is `Website#getPage` — the same seven-step resolution the browser runs,\n * literally the same function, so a server-rendered page and the one hydrating\n * over it cannot disagree. Pure `@uniweb/core`: no React, no DOM, no DataStore\n * required, safe in a Worker isolate.\n *\n * @param {Website} website\n * @param {string} route - The requested path, e.g. `/blog/1`\n * @returns {Page|undefined} The page, or undefined when nothing matches — which\n * is a genuine 404 and the caller's to turn into one.\n */\nexport function resolvePage(website, route) {\n return website.getPage(route)\n}\n","/**\n * SSR Renderer\n *\n * Hook-free rendering pipeline for SSG (build) and server-side SSR.\n * Mirrors BlockRenderer.jsx + Background.jsx using React.createElement\n * directly — no hooks, no JSX, no browser APIs.\n *\n * This is the single source of truth for how blocks render during prerender.\n * When modifying BlockRenderer.jsx or Background.jsx, update this file to match.\n *\n * Exports three layers:\n * 1. Rendering functions (renderBlock, renderBlocks, renderLayout, renderBackground)\n * 2. Initialization (initPrerender, prefetchIcons)\n * 3. Per-page rendering (renderPage, classifyRenderError, injectPageContent, escapeHtml)\n */\n\nimport React from 'react'\nimport { renderToString } from 'react-dom/server'\nimport { createUniweb, resolveDefaultLocale } from '@uniweb/core'\nimport { sectionDomId } from '@uniweb/core/section-id'\nimport { siteUrl, withOpacity } from './background-shared.js'\nimport { routePatternToRegex, isDynamicRoute } from '@uniweb/core/route-match'\nimport { DEFAULT_ICON_BASE, iconUrl } from '@uniweb/core/icon-corpus'\nimport { buildSectionOverrides, FONT_LINKS_MARKER } from '@uniweb/theming'\nimport { prepareProps, getComponentMeta } from './prepare-props.js'\nimport { default404Html } from './default-404.js'\nimport {\n wireFoundationCapabilities,\n sliceContentForLocale,\n hydrateDataStore,\n ensureThemeCss,\n} from './wire-foundation.js'\nimport { resolveLayoutTransitions, resolveLayoutLayers, areaWrapperStyle } from './area-wrappers.js'\nimport { renderAppearanceBootScript } from './appearance.js'\nimport { documentTitle } from './document-title.js'\n\n// Re-export L2 helpers so the public `@uniweb/runtime/ssr` surface\n// carries everything an SSR consumer needs from one entry point.\nexport { sliceContentForLocale, hydrateDataStore }\n\n// ============================================================================\n// Layer 1: Rendering functions\n// ============================================================================\n\n/**\n * Valid color contexts for section theming\n */\nconst VALID_CONTEXTS = ['light', 'medium', 'dark']\n\n/**\n * Build wrapper props from block configuration.\n * Mirrors getWrapperProps in BlockRenderer.jsx.\n */\nexport function getWrapperProps(block) {\n const theme = block.themeName\n const blockClassName = block.state?.className || ''\n\n // Empty themeName = Auto → no context class → inherits tokens from :root\n // Non-empty = Pinned → context class sets tokens directly on the element\n let contextClass = ''\n if (theme && VALID_CONTEXTS.includes(theme)) {\n contextClass = `context-${theme}`\n }\n\n let className = contextClass\n if (blockClassName) {\n className = className ? `${className} ${blockClassName}` : blockClassName\n }\n\n // Normalized once by core (`block.background`) — the same value the SPA renderer reads.\n const background = block.background || {}\n const style = {}\n\n // If background has content, ensure relative positioning and a stacking context\n // so the background's z-index stays contained within this section.\n if (background.mode) {\n style.position = 'relative'\n style.isolation = 'isolate'\n }\n\n // The section's tokens in effect whatever the scheme, inline — as in the SPA renderer\n // (`BlockRenderer.jsx`); the rest of its theme is the page stylesheet's.\n if (block.contextOverrides) {\n for (const [key, value] of Object.entries(block.contextOverrides)) {\n style[`--${key}`] = value\n }\n }\n\n // Same rule as the SPA renderer and the search extractor — @uniweb/core/section-id.\n return { id: sectionDomId(block), style, className, background }\n}\n\n\n/**\n * Render a background element for SSR.\n * Mirrors Background.jsx (color, gradient, image — not video).\n * Video backgrounds require JS for autoplay and are skipped during SSR.\n */\nexport function renderBackground(background) {\n if (!background?.mode) return null\n\n const containerStyle = {\n position: 'absolute',\n inset: '0',\n overflow: 'hidden',\n zIndex: 0,\n }\n\n const children = []\n\n // Color background\n if (background.mode === 'color' && background.color) {\n children.push(\n React.createElement('div', {\n key: 'bg-color',\n className: 'background-color',\n style: { position: 'absolute', inset: '0', backgroundColor: background.color },\n 'aria-hidden': 'true',\n })\n )\n }\n\n // Gradient background (supports string or object with opacity)\n if (background.mode === 'gradient' && background.gradient) {\n const g = background.gradient\n\n let bgValue\n if (typeof g === 'string') {\n bgValue = g\n } else {\n const {\n start = 'transparent',\n end = 'transparent',\n angle = 0,\n startPosition = 0,\n endPosition = 100,\n startOpacity = 1,\n endOpacity = 1,\n } = g\n const startColor = startOpacity < 1 ? withOpacity(start, startOpacity) : start\n const endColor = endOpacity < 1 ? withOpacity(end, endOpacity) : end\n bgValue = `linear-gradient(${angle}deg, ${startColor} ${startPosition}%, ${endColor} ${endPosition}%)`\n }\n\n children.push(\n React.createElement('div', {\n key: 'bg-gradient',\n className: 'background-gradient',\n style: { position: 'absolute', inset: '0', background: bgValue },\n 'aria-hidden': 'true',\n })\n )\n }\n\n // Image background\n if (background.mode === 'image' && background.image?.src) {\n const img = background.image\n children.push(\n React.createElement('div', {\n key: 'bg-image',\n className: 'background-image',\n style: {\n position: 'absolute',\n inset: '0',\n backgroundImage: `url(${siteUrl(img.src)})`,\n backgroundPosition: img.position || 'center',\n backgroundSize: img.size || 'cover',\n backgroundRepeat: 'no-repeat',\n },\n 'aria-hidden': 'true',\n })\n )\n }\n\n // Overlay (gradient or solid)\n if (background.overlay?.enabled) {\n const ov = background.overlay\n let overlayStyle\n\n if (ov.gradient) {\n const g = ov.gradient\n overlayStyle = {\n position: 'absolute', inset: '0', pointerEvents: 'none',\n background: `linear-gradient(${g.angle || 180}deg, ${g.start || 'rgba(0,0,0,0.7)'} ${g.startPosition || 0}%, ${g.end || 'rgba(0,0,0,0)'} ${g.endPosition || 100}%)`,\n opacity: ov.opacity ?? 0.5,\n }\n } else {\n const baseColor = ov.type === 'light' ? '255, 255, 255' : '0, 0, 0'\n overlayStyle = {\n position: 'absolute', inset: '0', pointerEvents: 'none',\n backgroundColor: `rgba(${baseColor}, ${ov.opacity ?? 0.5})`,\n }\n }\n\n children.push(\n React.createElement('div', {\n key: 'bg-overlay',\n className: ov.gradient ? 'background-overlay background-overlay--gradient' : 'background-overlay background-overlay--solid',\n style: overlayStyle,\n 'aria-hidden': 'true',\n })\n )\n }\n\n if (children.length === 0) return null\n\n return React.createElement('div', {\n className: `background background--${background.mode}`,\n style: containerStyle,\n 'aria-hidden': 'true',\n }, ...children)\n}\n\n/**\n * Render a single block for SSR.\n * Mirrors BlockRenderer.jsx but without hooks (no runtime data fetching).\n *\n * Two modes (mirrors client BlockRenderer):\n * - Bare (as=null/false): component only, no wrapper\n * - Section (as='section'/'div'/etc.): full treatment with wrapper, context, background\n *\n * @param {Block} block - Block instance to render\n * @param {Object} [options]\n * @param {string|null} [options.as='section'] - Wrapper element tag, or null/false for bare mode\n * @returns {React.ReactElement}\n */\nexport function renderBlock(block, { as = 'section' } = {}) {\n const Component = block.initComponent()\n\n if (!Component) {\n return React.createElement('div', {\n className: 'block-error',\n style: { padding: '1rem', background: '#fef2f2', color: '#dc2626' },\n }, `Component not found: ${block.type}`)\n }\n\n // Resolve entity data synchronously (SSG has no async): EntityStore fills the keys\n // the component declares (`meta.data`) from the fetches that reach the block.\n const meta = getComponentMeta(block.type)\n const entityStore = block.website?.entityStore\n let entityData = null\n if (entityStore) {\n const resolved = entityStore.resolve(block, meta)\n if (resolved.status === 'ready') entityData = resolved.data\n }\n\n // Build content and params with runtime guarantees.\n // prepareProps handles the full pipeline: entity data merge,\n // foundation content handler invocation, guaranteed content\n // structure, schema application, and param defaults.\n // See prepare-props.js for the pipeline details.\n const prepared = prepareProps(block, meta, entityData)\n const params = prepared.params\n const content = { ...prepared.content, ...block.properties }\n\n const componentProps = { content, params, block }\n\n // Bare mode: component only, no wrapper or section chrome.\n // Used by ChildBlocks for grid cells, tab panels, inline children, insets.\n if (!as) {\n return React.createElement(Component, componentProps)\n }\n\n // Section mode: full treatment with wrapper, context classes, background.\n const { background, ...wrapperProps } = getWrapperProps(block)\n\n // Merge Component.className (static classes declared on the component function)\n const componentClassName = Component.className\n if (componentClassName) {\n wrapperProps.className = wrapperProps.className\n ? `${wrapperProps.className} ${componentClassName}`\n : componentClassName\n }\n\n // Check if component handles its own background\n const hasBackground = background?.mode && meta?.background !== 'self'\n block.hasBackground = hasBackground\n\n // Determine wrapper element:\n // - Explicit as (not 'section') → use as prop directly\n // - Component.as → use component's declared tag (e.g., Header.as = 'header')\n // - fallback → 'section'\n const wrapperTag = as !== 'section' ? as : (Component.as || 'section')\n\n if (hasBackground) {\n return React.createElement(wrapperTag, wrapperProps,\n renderBackground(background),\n React.createElement('div', { style: { position: 'relative', zIndex: 10 } },\n React.createElement(Component, componentProps)\n )\n )\n }\n\n return React.createElement(wrapperTag, wrapperProps,\n React.createElement(Component, componentProps)\n )\n}\n\n/**\n * Render an array of blocks for SSR.\n */\nexport function renderBlocks(blocks) {\n if (!blocks || blocks.length === 0) return null\n return blocks.map((block, index) =>\n React.createElement(React.Fragment, { key: block.id || index },\n renderBlock(block)\n )\n )\n}\n\n/**\n * Render page layout for SSR.\n * Mirrors Layout.jsx but without hooks.\n */\nexport function renderLayout(page, website) {\n const layoutName = page.getLayoutName()\n const RemoteLayout = website.getRemoteLayout(layoutName)\n const layoutMeta = website.getLayoutMeta(layoutName)\n\n const bodyBlocks = page.getBodyBlocks()\n const areas = page.getLayoutAreas()\n\n // Mirror Layout.jsx: wrap body + each area in a thin div carrying its\n // view-transition-name, so the prerendered HTML matches what the SPA hydrates\n // and the browser can animate regions independently on client navigation.\n const areaNames = Object.keys(areas)\n const transitions = website.viewTransitions\n ? resolveLayoutTransitions(areaNames, layoutMeta?.transitions)\n : null\n const layers = resolveLayoutLayers(areaNames, layoutMeta?.layers)\n const wrapArea = (name, element) => {\n const style = areaWrapperStyle(name, transitions, layers)\n return style ? React.createElement('div', { style }, element) : element\n }\n\n const bodyElement = bodyBlocks ? wrapArea('body', renderBlocks(bodyBlocks)) : null\n const areaElements = {}\n for (const [name, blocks] of Object.entries(areas)) {\n areaElements[name] = wrapArea(name, renderBlocks(blocks))\n }\n\n if (RemoteLayout) {\n const params = { ...(layoutMeta?.defaults || {}), ...(page.getLayoutParams() || {}) }\n return React.createElement(RemoteLayout, {\n page, website, params,\n body: bodyElement,\n ...areaElements,\n })\n }\n\n // Default layout — mirror DefaultLayout in Layout.jsx, including its lack of\n // stacking: the area wrappers already carry their layers, and a positioned\n // element here would seal those layers inside it.\n return React.createElement(React.Fragment, null,\n areaElements.header && React.createElement('header', null, areaElements.header),\n bodyElement && React.createElement('main', null, bodyElement),\n areaElements.footer && React.createElement('footer', null, areaElements.footer)\n )\n}\n\n// ============================================================================\n// Layer 2: Initialization\n// ============================================================================\n\n/**\n * Construct a Uniweb singleton scoped to a single locale.\n *\n * Combines the three steps that every SSR consumer (browser SPA, Node\n * SSG, a server-side isolate) needs in the same order: slice the\n * multi-locale content payload, run `initPrerender` (which builds the\n * Website + wires foundation capabilities), then `setActiveLocale` so\n * `website.activeLang` stays in sync with what the page is rendering\n * for. Caller still owns DataStore hydration (per-request data differs\n * between requests; locale construction can be cached).\n *\n * @param {Object} content - Site content payload (possibly multi-locale).\n * @param {Object} foundation - Loaded foundation module.\n * @param {string} locale - Locale code to render in.\n * @param {Array<Object>|Object} [extensionsOrOptions] - Same shape as initPrerender's\n * third arg: an extensions array, or an options object when no extensions.\n * @param {Object} [maybeOptions] - Options object when extensions are passed.\n * @returns {import('@uniweb/core').default} The configured Uniweb singleton.\n */\nexport function initPrerenderForLocale(content, foundation, locale, extensionsOrOptions, maybeOptions) {\n const localeContent = sliceContentForLocale(content, locale)\n const uniweb = initPrerender(localeContent, foundation, extensionsOrOptions, maybeOptions)\n const defaultLang = resolveDefaultLocale(content?.config)\n if (locale && locale !== defaultLang && uniweb.activeWebsite?.setActiveLocale) {\n uniweb.activeWebsite.setActiveLocale(locale)\n }\n return uniweb\n}\n\n/**\n * Create and configure the Uniweb runtime for prerendering.\n *\n * Handles the full initialization sequence in the correct order:\n * createUniweb → setFoundation → capabilities → layoutMeta → basePath → childBlockRenderer.\n *\n * Returns the configured uniweb instance. Consumers can add extras after:\n * - Build: pre-populate DataStore, load extensions\n * - A server-side renderer: (none needed — the payload is complete)\n *\n * NOTE: Does NOT clone content. Cloning is the consumer's responsibility\n * (build modifies content before init; a server-side renderer clones upfront).\n *\n * @param {Object} content - Site content JSON (pages, config, hierarchy)\n * @param {Object} foundation - Loaded foundation module\n * @param {Object} [options]\n * @param {function} [options.onProgress] - Progress callback\n * @returns {Object} Configured uniweb instance\n */\nexport function initPrerender(content, foundation, extensionsOrOptions, maybeOptions) {\n // Backwards-compatible arg shape: (content, foundation, options) or\n // (content, foundation, extensions, options). Extensions must be passed at\n // construction so the Website's FetcherDispatcher sees their routes.\n let extensions = []\n let options = {}\n if (Array.isArray(extensionsOrOptions)) {\n extensions = extensionsOrOptions\n options = maybeOptions || {}\n } else {\n options = extensionsOrOptions || {}\n }\n const { onProgress = () => {} } = options\n\n onProgress('Initializing runtime...')\n // Uniweb constructor wires foundation, capabilities, layoutMeta, handlers,\n // and extensions at construction time — see `@uniweb/core`'s src/uniweb.js.\n const uniweb = createUniweb(content, foundation, extensions)\n\n // Set base path from site config for subdirectory deployments\n if (content.config?.base && uniweb.activeWebsite?.setBasePath) {\n uniweb.activeWebsite.setBasePath(content.config.base)\n }\n\n // Set childBlockRenderer so ChildBlocks/Visual/Render work during prerender.\n // Mirrors the client's ChildBlocks component in PageRenderer.jsx:\n // - default bare rendering (no wrapAs) — component only, no wrapper\n // - pass wrapAs to opt into full section treatment\n uniweb.childBlockRenderer = function InlineChildBlocks({ blocks, from, wrapAs }) {\n const blockList = blocks || from?.childBlocks || []\n return blockList.map((childBlock, index) =>\n React.createElement(React.Fragment, { key: childBlock.id || index },\n renderBlock(childBlock, { as: wrapAs || null })\n )\n )\n }\n\n // The same background a section gets, for kit's `SectionBackground` — a component that\n // paints its own (`background: 'self'`). Mirrors the SPA's `Background` (setup.js).\n uniweb.backgroundRenderer = function InlineBackground(background) {\n return renderBackground(background)\n }\n\n // L2 (singleton wiring): defaultInsets, xref.build(), and any future\n // framework-level capability bridge — shared with setup.js so both\n // boot paths apply the same foundation contract. See\n // wire-foundation.js — its header states the rule for what belongs in\n // that helper vs. here vs. setup.js.\n wireFoundationCapabilities(uniweb, foundation)\n\n // Site-wide theme CSS. Unconditional here: at this point there is no\n // <head> to inspect, and injectPageContent() emits the result\n // idempotently, so a lane that already baked the style tag is unaffected.\n ensureThemeCss(uniweb, foundation)\n\n // Register SSR-safe routing so useRouting()/useActiveRoute() work during prerender.\n // renderPage() calls website.setActivePage() before rendering each page,\n // so activePage.route always reflects the page being rendered.\n const website = uniweb.activeWebsite\n uniweb.routingComponents = {\n useLocation: () => {\n // ⛔ `activePage.route` IS ALREADY ROOT-RELATIVE — `/`, `/about`. This used to\n // return `'/' + route`, so every prerendered page reported a pathname with a\n // doubled leading slash: `//` on the homepage, `//about` elsewhere.\n //\n // It failed silently for as long as it existed, because nothing in the\n // runtime reads pathname — a FOUNDATION does. Measured 2026-09-18: a language\n // switcher passing `useLocation().pathname` to `getLocaleUrl` emitted\n // `href=\"//\"`, which a browser resolves as a PROTOCOL-RELATIVE url (host\n // empty), not as the site root. The SPA was unaffected, so it reproduced only\n // in the built output.\n const route = website?.activePage?.route || '/'\n const pathname = route.startsWith('/') ? route : `/${route}`\n return { pathname, search: '', hash: '', state: null, key: 'default' }\n },\n useParams: () => ({}),\n useNavigate: () => () => {},\n }\n\n return uniweb\n}\n\n/**\n * Pre-fetch icons from CDN and populate the Uniweb icon cache.\n * Stores the cache on siteContent._iconCache for embedding in HTML.\n *\n * @param {Object} siteContent - Site content JSON (mutated: _iconCache added)\n * @param {Object} uniweb - Configured uniweb instance\n * @param {function} [onProgress] - Progress callback\n */\nexport async function prefetchIcons(siteContent, uniweb, onProgress = () => {}) {\n const icons = siteContent.icons?.used || []\n if (icons.length === 0) return\n\n const cdnBase = siteContent.config?.icons?.cdnUrl || DEFAULT_ICON_BASE\n\n onProgress(`Fetching ${icons.length} icons for SSR...`)\n\n const results = await Promise.allSettled(\n icons.map(async (iconRef) => {\n const [family, name] = iconRef.split(':')\n const url = iconUrl(family, name, cdnBase)\n const response = await fetch(url)\n if (!response.ok) throw new Error(`HTTP ${response.status}`)\n const svg = await response.text()\n uniweb.iconCache.set(`${family}:${name}`, svg)\n })\n )\n\n const succeeded = results.filter(r => r.status === 'fulfilled').length\n const failed = results.filter(r => r.status === 'rejected').length\n if (failed > 0) {\n const msg = `Fetched ${succeeded}/${icons.length} icons (${failed} failed)`\n console.warn(`[prerender] ${msg}`)\n onProgress(` ${msg}`)\n }\n\n // Store icon cache on siteContent for embedding in HTML\n if (uniweb.iconCache.size > 0) {\n siteContent._iconCache = Object.fromEntries(uniweb.iconCache)\n }\n}\n\n// ============================================================================\n// Layer 3: Per-page rendering\n// ============================================================================\n\n// ⭐ `resolvePage` LIVES IN ITS OWN LEAF and is re-exported here, so the public\n// `@uniweb/runtime/ssr` surface is unchanged. It is `website.getPage` and needs\n// no renderer; this file imports `react-dom/server` at module scope, so a caller\n// that only wants the lookup used to get React's server renderer with it\n// (`./resolve-page.js`, 2026-09-20).\nexport { resolvePage } from './resolve-page.js'\n\n/**\n * Classify an SSR rendering error.\n *\n * @param {Error} err\n * @returns {{ type: 'hooks'|'null-component'|'unknown', message: string }}\n */\nexport function classifyRenderError(err) {\n const msg = err.message || ''\n\n if (msg.includes('Invalid hook call') || msg.includes('useState') || msg.includes('useEffect')) {\n return {\n type: 'hooks',\n message: 'contains components with React hooks (renders client-side)',\n }\n }\n\n if (msg.includes('Element type is invalid') && msg.includes('null')) {\n return {\n type: 'null-component',\n message: 'a component resolved to null (often hook-related, renders client-side)',\n }\n }\n\n return {\n type: 'unknown',\n message: msg,\n }\n}\n\n/**\n * Render a single page to HTML.\n *\n * Handles the full per-page pipeline:\n * setActivePage → renderLayout → renderToString → error handling → section override CSS.\n *\n * @param {Page} page - Page instance to render\n * @param {Website} website - Website instance\n * @returns {{ renderedContent: string, sectionOverrideCSS: string } | { error: { type: string, message: string } }}\n */\nexport function renderPage(page, website) {\n website.setActivePage(page.route)\n\n // A page that claims content but yields no blocks has not been loaded — it is\n // not an empty page. `Page#bodyBlocks` returns [] when its sections are absent\n // from the payload (split content), on the understanding that a caller loads\n // them first: the SPA does, in PageRenderer and at boot. THIS path never has.\n //\n // Left alone, that renders a structurally valid, completely empty document and\n // reports success — which is the worst shape a failure can take, and it cost a\n // host most of a day chasing a renderer that was doing what it was told.\n // Distinguishing it here is cheap: a content-less container reports\n // hasContent() === false and is correctly empty, so the two never collide.\n if (page.hasContent?.() && page.getBodyBlocks().length === 0) {\n return {\n error: {\n type: 'content-not-loaded',\n message:\n `page \"${page.route}\" declares content but has no loaded sections — ` +\n 'its sections are not in the payload and this renderer does not fetch them',\n },\n }\n }\n\n const element = renderLayout(page, website)\n\n let renderedContent\n try {\n renderedContent = renderToString(element)\n } catch (err) {\n return { error: classifyRenderError(err) }\n }\n\n // Build per-page section override CSS (theme pinning, component vars)\n const appearance = website.themeData?.appearance\n // Every block the page renders — each layout area it shows, a sidebar too; the builder walks\n // their child sections. The SPA renderer passes the same (`PageRenderer.jsx`).\n const sectionOverrideCSS = buildSectionOverrides(page.getAllBlocks(), appearance)\n\n return { renderedContent, sectionOverrideCSS }\n}\n\n// ============================================================================\n// HTML injection\n// ============================================================================\n\n/**\n * Escape HTML special characters.\n */\nexport function escapeHtml(str) {\n if (!str) return ''\n return String(str)\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n\n/**\n * Inject prerendered content into an HTML shell.\n *\n * THE SHARED PRERENDER SEAM. Every lane that turns a Page into HTML calls this:\n * the framework's SSG (@uniweb/build's prerender.js) and the cloud worker's\n * just-in-time render. Anything a page needs *because it was prerendered at all*\n * belongs here, so a new feature reaches both lanes at once.\n *\n * Common operations shared by both build and cloud:\n * - Replace #root div with rendered HTML\n * - Update page title\n * - Add/update meta description\n * - Inject section override CSS\n * - Inject the pre-paint appearance script\n *\n * Build layers its additional injections on top of this return value:\n * __SITE_CONTENT__ JSON, icon cache, theme CSS (build-specific).\n *\n * WHICH SIDE OF THE SEAM? Ask what the injection is derived from. If it needs\n * only the page/website graph — which every lane has — it goes here. If it needs\n * a build-only artifact (the emitted bundle, the collection JSON files, the\n * prefetched icon cache, the compiled theme CSS), it goes in the caller. Getting\n * this wrong is silent: the appearance boot script started life in\n * @uniweb/build's injectBuildData and therefore never reached cloud-rendered\n * pages, which flashed light for every dark-mode visitor.\n *\n * @param {string} html - HTML shell\n * @param {string} renderedContent - React renderToString output\n * @param {Object} page - Page data { title, description, route }\n * @param {Object} [options]\n * @param {string} [options.sectionOverrideCSS] - Per-page section override CSS\n * @returns {string} HTML with injected content\n */\nexport function injectPageContent(html, renderedContent, page, options = {}) {\n let result = html\n\n // Pre-paint appearance script. Prerendered HTML carries real content styled\n // from :root (light) tokens, so a dark visitor would see a flash of light\n // before the bundle hydrates and applies the class. Read off the page's\n // website back-ref rather than a parameter: every lane builds the same graph,\n // so this needs no plumbing and no per-lane opt-in. Idempotent, because\n // @uniweb/build's injectBuildData may run over this HTML afterwards.\n if (!result.includes('id=\"uniweb-appearance\"')) {\n const bootScript = renderAppearanceBootScript(page?.website?.themeData?.appearance)\n if (bootScript) {\n result = result.replace('</head>', ` ${bootScript}\\n</head>`)\n }\n }\n\n // Site-wide theme CSS. Derived from the website graph\n // (`website.themeData`), so it belongs on THIS side of the seam — every\n // lane builds that graph. It lived in @uniweb/build's injectBuildData\n // until 2026-07-28, which meant sites served by any lane that doesn't run\n // the framework's build rendered with every semantic token unset: no\n // colours, no backgrounds, no failure anywhere. That is the same mistake\n // the appearance script above was moved out of, four lines below the\n // comment warning about it — see the note in `@uniweb/build`'s src/prerender.js.\n // Idempotent, so a shell that already carries the tag is left alone.\n const themeData = page?.website?.themeData\n const themeCss = themeData?.css\n if (themeCss && !result.includes('id=\"uniweb-theme\"')) {\n result = result.replace(\n '</head>',\n ` <style id=\"uniweb-theme\">\\n${themeCss}\\n </style>\\n</head>`\n )\n }\n\n // The theme's font <link> tags — same seam, same reasoning. Graph-derived,\n // so a lane that never runs @uniweb/build still gets its webfonts instead of\n // falling back to system faces. Deduped on FONT_LINKS_MARKER rather than an\n // id because <link> tags have none; the marker is owned by @uniweb/theming,\n // which generates the block, so this and @uniweb/build read one literal.\n if (themeData?.links && !result.includes(FONT_LINKS_MARKER)) {\n result = result.replace(\n '</head>',\n ` ${FONT_LINKS_MARKER}\\n${themeData.links}\\n</head>`\n )\n }\n\n // Inject per-page section override CSS before </head>\n if (options.sectionOverrideCSS) {\n const overrideStyle = `<style id=\"uniweb-page-overrides\">\\n${options.sectionOverrideCSS}\\n</style>`\n result = result.replace('</head>', `${overrideStyle}\\n</head>`)\n }\n\n // Replace the empty root div with pre-rendered content\n result = result.replace(\n /<div id=\"root\">[\\s\\S]*?<\\/div>/,\n `<div id=\"root\">${renderedContent}</div>`\n )\n\n // Update the document title — by the rule the SPA sets `document.title` with\n // (`documentTitle`: `<page> | <site>`), so the title a crawler reads here is the one a\n // visitor keeps once the bundle loads. ⛔ Until 2026-09-14 this wrote the page title\n // alone. getTitle() so isIndex pages inherit their parent's title.\n const title = documentTitle(page.getTitle?.() || page.title, page.website?.name)\n if (title) {\n result = result.replace(\n /<title>.*?<\\/title>/,\n `<title>${escapeHtml(title)}</title>`\n )\n }\n\n // Add/update meta description\n if (page.description) {\n const metaDesc = `<meta name=\"description\" content=\"${escapeHtml(page.description)}\">`\n if (result.includes('<meta name=\"description\"')) {\n result = result.replace(/<meta name=\"description\"[^>]*>/, metaDesc)\n } else {\n result = result.replace('</head>', `${metaDesc}\\n</head>`)\n }\n }\n\n // Social / SEO meta from the page's effective head metadata (page seo\n // cascading over site-level seo — see Page.getHeadMeta). The SPA emits these\n // client-side via useHeadMeta; this is the SSR twin, so crawlers and social\n // unfurlers (which don't run JS) get them in the static HTML too.\n const headMeta = page.getHeadMeta?.()\n if (headMeta) {\n const og = headMeta.og || {}\n const keywords = Array.isArray(headMeta.keywords)\n ? headMeta.keywords.join(', ')\n : headMeta.keywords\n const tags = []\n if (keywords) tags.push(`<meta name=\"keywords\" content=\"${escapeHtml(keywords)}\">`)\n if (headMeta.robots) tags.push(`<meta name=\"robots\" content=\"${escapeHtml(headMeta.robots)}\">`)\n if (og.title) tags.push(`<meta property=\"og:title\" content=\"${escapeHtml(og.title)}\">`)\n if (og.description) tags.push(`<meta property=\"og:description\" content=\"${escapeHtml(og.description)}\">`)\n if (og.image) tags.push(`<meta property=\"og:image\" content=\"${escapeHtml(og.image)}\">`)\n if (og.url) tags.push(`<meta property=\"og:url\" content=\"${escapeHtml(og.url)}\">`)\n tags.push('<meta property=\"og:type\" content=\"website\">')\n tags.push(`<meta name=\"twitter:card\" content=\"${og.image ? 'summary_large_image' : 'summary'}\">`)\n if (og.title) tags.push(`<meta name=\"twitter:title\" content=\"${escapeHtml(og.title)}\">`)\n if (og.description) tags.push(`<meta name=\"twitter:description\" content=\"${escapeHtml(og.description)}\">`)\n if (og.image) tags.push(`<meta name=\"twitter:image\" content=\"${escapeHtml(og.image)}\">`)\n if (headMeta.canonical) tags.push(`<link rel=\"canonical\" href=\"${escapeHtml(headMeta.canonical)}\">`)\n if (tags.length) result = result.replace('</head>', `${tags.join('\\n')}\\n</head>`)\n }\n\n return result\n}\n\n// ============================================================================\n// 404 fallback generation\n// ============================================================================\n\n/**\n * Generate 404.html content for static hosting fallback.\n *\n * Serves two purposes on static hosts (GitHub Pages, Cloudflare Pages, etc.):\n * 1. Real 404: pre-rendered custom 404 page content (or blank #root if none defined)\n * 2. Valid dynamic route (e.g. /blog/2): inline script clears #root so SPA renders fresh\n *\n * Flow: static host serves 404.html → inline script runs before React mounts →\n * - dynamic route: clears #root, React renders the page normally\n * - real 404: leaves #root with pre-rendered content, React re-renders same 404 page\n *\n * @param {Object} options\n * @param {string} options.baseHtml - Assembled HTML shell (with site content already injected)\n * @param {Object} options.website - Initialized Website instance (from initPrerender)\n * @param {Object} options.siteContent - Site content object (to find dynamic templates)\n * @returns {{ html: string, hasNotFoundPage: boolean }}\n */\nexport function generate404Html({ baseHtml, website, siteContent }) {\n // Extract patterns for routes that remain as dynamic templates (prerender: false)\n // '/blog/:id' → /^\\/blog\\/([^/]+)$/. Compiled by the shared matcher rather\n // than a second regex built here: this file used to build its own with\n // `:[^/]+`, which disagreed with core's `:(\\w+)` on any param name carrying a\n // non-word character. See @uniweb/core/route-match for the whole story.\n // ⛔ **ROUTE, NOT THE FLAG — a filter must not read absence as \"not parametric\".**\n // This filtered on `p.isDynamic` alone. A producer that marks only the bracket\n // page (or none at all) left every parametric route out of the list, so a cold\n // hit on one showed the 404 body and never cleared #root for the SPA. The flag\n // is a producer's claim; the `:` in the route is the thing itself, and every\n // flagged page has one — so this is a superset and cannot lose a route.\n const dynamicTemplates = siteContent.pages?.filter((p) => p.isDynamic || isDynamicRoute(p.route)) || []\n const routePatterns = dynamicTemplates.map((p) => routePatternToRegex(p.route).regex.source)\n\n let html = baseHtml\n\n // Pre-render the custom 404 page content into #root (if the site defines one),\n // otherwise inject a default 404 message so the page isn't blank before JS loads\n const notFoundPage = website.getNotFoundPage()\n if (notFoundPage) {\n const notFoundResult = renderPage(notFoundPage, website)\n if (notFoundResult && !notFoundResult.error) {\n html = injectPageContent(html, notFoundResult.renderedContent, notFoundPage, {\n sectionOverrideCSS: notFoundResult.sectionOverrideCSS,\n })\n }\n } else {\n const basePath = website.basePath || ''\n html = html.replace(\n /<div id=\"root\">[\\s\\S]*?<\\/div>/,\n `<div id=\"root\">${default404Html(basePath)}</div>`\n )\n }\n\n // Inject inline script: if path matches a dynamic route, clear #root before React mounts\n // so the SPA renders the correct page rather than the 404 content\n if (routePatterns.length > 0) {\n const patternList = routePatterns.map((p) => `/${p}/`).join(',')\n // The path is normalized here rather than by making every pattern accept a\n // trailing slash — same rule the matcher applies, applied in one place.\n const dynamicScript =\n `<script>(function(){` +\n `var p=[${patternList}],r=window.location.pathname.replace(/\\\\/+$/,'')||'/';` +\n `if(p.some(function(x){return x.test(r)})){` +\n `var el=document.getElementById('root');if(el)el.innerHTML='';` +\n `}})()</script>`\n html = html.replace('</body>', `${dynamicScript}\\n</body>`)\n }\n\n return { html, hasNotFoundPage: !!notFoundPage }\n}\n","/**\n * Runtime default fetcher.\n *\n * Used as the FetcherDispatcher's terminal fallback when no foundation\n * transport claims the request. Sites that declare no transport at all —\n * starter/docs/marketing templates hitting /data/*.json — ride on this path\n * with zero config, and so does a site a host serves live.\n *\n * ⭐ It speaks exactly TWO lanes, and takes NO site-level vocabulary for a\n * backend of the author's own:\n *\n * - a compiled file — `path:` under the site's base (`/data/<query>.json`,\n * a per-record file), or a plain JSON `url:` the author wrote;\n * - the host's RECORDS SERVICE — `ask:`, one POST per tick carrying every\n * question the page asked, answered per key (the records contract, as this\n * client reads it).\n *\n * A query's set — `scope:` / `where:` / `sort:` / `limit:` — and the `narrow`\n * a fetch takes of it are evaluated HERE, locally, over what the first lane\n * returns — with `@uniweb/core`'s one evaluator (`evaluateQuery`), the same the\n * build uses — and at the source when asked. Nothing decides that per site: the\n * LANE decides.\n *\n * ⛔ RETIRED 2026-09-04 [Diego]: `fetcher.baseUrl`, `headers`, `envelope`,\n * `supports`, `request.style` / `request.rename` and the `json-body`\n * request-style registry. *\"3rd party endpoints must be supported at the\n * foundation level… making the runtime+core lean.\"* A backend with its own\n * base, headers, wire or query language is a TRANSPORT — a named\n * `{ resolve, cacheKey? }` the foundation (or an extension) registers and\n * the site selects per binding key (`as`) in `fetcher.transports`. The build warns once\n * and drops a retired key from the payload, so an author's backend does not\n * silently stop being reached.\n *\n * A request may carry `method: 'POST'` + `body:` for an endpoint that takes a\n * query in a body (GraphQL, a search endpoint) — an external query's, or its\n * `record:` request's — and `transform:`, a dot-path to the records in ONE\n * response. `{paramName}` placeholders in body strings are substituted from\n * `request.dynamicContext`, so a parametric page's record request can reference\n * its route param. ⛔ `envelope` (the object form of `detail:`) is retired with\n * it, 2026-09-13: a record request's own `transform` is `record.transform`.\n *\n * Exported from a subpath — `@uniweb/runtime/default-fetcher` — for\n * runtime-level callers (the editor's preview iframe, custom runtime\n * harnesses). **Foundations should not import this.** A foundation that\n * wants plain URL + JSON behavior simply omits its own transport; the\n * runtime installs this one automatically.\n *\n * Intentional omission: credentials / secrets. Any value the framework puts\n * into the served HTML is public to the browser. Sites needing private\n * credentials use a deployment-layer proxy — the site fetches a same-origin\n * URL, and a layer in front resolves the credential and forwards upstream.\n */\n\nimport {\n substitutePlaceholders,\n evaluateQuery,\n sortRecords,\n sortToWire,\n deriveCacheKey,\n resolveServiceUrl,\n} from '@uniweb/core'\n\n/**\n * @param {Object} [options]\n * @param {string} [options.basePath=''] - Prepended to local absolute paths\n * for subpath deployments. Remote URLs pass through unchanged.\n * @param {boolean} [options.dev=false] - Dev-mode diagnostics: a bad `sort:`\n * throws instead of delivering the records unsorted.\n * @param {Function|null} [options.fetch=null] - The transport. A host executing\n * fetches outside a browser (an SSR isolate) decides how a site-relative\n * address is dispatched — through its own origin or a service binding — and\n * hands that in. Defaults to the global `fetch`, resolved at call time so a\n * test stub installed later is honoured.\n * @returns {{ cacheKey: (req: Object) => string, resolve: (req: Object, ctx: Object) => Promise<{ data, error?, meta? }> }}\n */\nexport function createDefaultFetcher({ basePath = '', dev = false, fetch: fetchImpl = null } = {}) {\n const doFetch = (input, init) => (fetchImpl || globalThis.fetch)(input, init)\n const pathPrefix = basePath && basePath !== '/' ? basePath.replace(/\\/$/, '') : ''\n\n\n // ⭐ THE RECORDS SERVICE — a batch of the misses, one POST, merged per key.\n //\n // The entity store dispatches every config a page needs in one synchronous\n // loop before awaiting any of them, so a question enqueued here and flushed\n // on the next microtask carries every miss of that page in one body. The\n // batch response is never cached as one: each request gets its own answer,\n // keyed by its own question.\n const askQueues = new Map()\n const askRecords = (request, ctx) => {\n // ⛔ A question needs the query's Model ref. A payload that offers the\n // service and carries no `config.queries` entry for the query cannot ask;\n // that is a producer defect and it is said here, per key, with no request.\n if (typeof request.schema !== 'string' || !request.schema) {\n return Promise.resolve({\n data: null,\n error: `the payload offers the records service but carries no Model ref for query ` +\n `\"${request.query ?? request.as}\" (config.queries) — it cannot be asked`,\n })\n }\n return new Promise((resolve) => {\n const url = resolveServiceUrl(request.ask, pathPrefix)\n let queue = askQueues.get(url)\n if (!queue) {\n queue = []\n askQueues.set(url, queue)\n queueMicrotask(() => {\n askQueues.delete(url)\n flushAsked(url, queue, doFetch)\n })\n }\n queue.push({ request, ctx, resolve })\n })\n }\n\n // ⭐ VIEWS OF ONE FILE SHARE ONE READ IN FLIGHT. Each view — its own `scope`,\n // `where`, `sort` or `limit` — is its own cache entry (`deriveCacheKey` hashes\n // the view) and is cut from the read; views asked together (a page's sections,\n // a host's prefetch) share one request. ⛔ Until 2026-09-11 the view was applied\n // before the dispatcher stored the answer under a key that left the view out, so\n // one file had ONE entry and whoever asked first decided what everyone got\n // (measured). A settled read is not kept: a view asked later reads again (a\n // static host's HTTP cache answers it), so dropping an entry and asking again\n // still reaches the source. The read carries no caller's abort signal — several\n // views may be waiting on it, and one navigating away must not cancel it for\n // the others.\n const reads = new Map()\n const readOnce = (target, init) => {\n const key = `${init.method} ${target} ${init.body ?? ''}`\n let pending = reads.get(key)\n if (!pending) {\n pending = readResponse(doFetch, target, init).finally(() => reads.delete(key))\n reads.set(key, pending)\n }\n return pending\n }\n\n return {\n /**\n * The cache identity is the request's ADDRESS and the view it takes of it —\n * or, when asked of the records service, the QUESTION (`deriveCacheKey`).\n * Two pages declaring different `where:` clauses against one path are two\n * entries, each cut from a read of the file (`readOnce`, above).\n */\n cacheKey(request) {\n return deriveCacheKey(request)\n },\n\n async resolve(request, ctx = {}) {\n if (!request) return { data: null }\n if (request.ask) return askRecords(request, ctx)\n const { path, url, transform, body: rawBody } = request\n\n // Normalize method. Only GET and POST are supported by the default\n // fetcher — mutations (PUT/PATCH/DELETE) are a different feature\n // (optimistic updates, action semantics) and don't belong here.\n let method = (request.method || 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'POST') {\n console.warn(`[default-fetcher] method \"${request.method}\" is not supported — falling back to GET.`)\n method = 'GET'\n }\n\n let target\n if (path) {\n // Local file under public/ — basePath applies for subpath deploys.\n target = pathPrefix && path.startsWith('/') && !path.startsWith('//')\n ? pathPrefix + path\n : path\n } else if (url) {\n // A URL the author wrote, sent exactly as written.\n target = url\n } else {\n return { data: [], error: 'No path, url or ask specified' }\n }\n\n const init = { method }\n\n if (method === 'POST') {\n // Substitute {paramName} placeholders in body strings using the\n // dynamic-route context. The helper expects a flat key→value map;\n // build it from dynamicContext's { paramName, paramValue } shape.\n // Strict-brace matcher: GraphQL selection sets pass through unchanged.\n const dc = request.dynamicContext\n const body = (rawBody !== undefined && rawBody !== null && dc && dc.paramName)\n ? substitutePlaceholders(rawBody, { [dc.paramName]: dc.paramValue }, { encode: false })\n : rawBody\n if (body !== undefined && body !== null) {\n init.headers = { 'Content-Type': 'application/json' }\n init.body = typeof body === 'string' ? body : JSON.stringify(body)\n }\n }\n\n try {\n const response = await readOnce(target, init)\n if (ctx.signal?.aborted) return { data: [], error: 'aborted' }\n\n if (!response.ok) {\n return { data: [], error: `HTTP ${response.status}: ${response.statusText}` }\n }\n\n let data = response.body\n\n // Unwrap the response: the request's own `transform:`, and nothing else — a\n // record request carries its `record.transform`, never the list's.\n if (transform && data !== null && data !== undefined) {\n data = getNestedValue(data, transform)\n }\n\n // Evaluate the query locally. Only applies to array data\n // (filtering/sorting/limiting a single record doesn't make sense).\n // For non-arrays, operators are ignored — the source returned what\n // it returned.\n data = applyOperators(data, request, { dev, locale: sortLocale(request, ctx) })\n\n // ⭐ Say whether WHOLE records were delivered, so the record index can\n // file them — what the config asked for, echoed: briefs when the query\n // has a per-record source, whole records otherwise. (The service reports\n // `whole` per\n // key and overrides this with what it actually served.)\n const whole = typeof request.whole === 'boolean' ? request.whole : undefined\n return whole === undefined ? { data: data ?? [] } : { data: data ?? [], meta: { whole } }\n } catch (error) {\n if (error?.name === 'AbortError') {\n return { data: [], error: 'aborted' }\n }\n return { data: [], error: error?.message || String(error) }\n }\n },\n }\n}\n\n/**\n * One question of a batch, in the records service's own vocabulary\n * (the records contract, §2).\n * ⭐ TWO LEVELS, ruled 2026-09-14 [Diego]:\n *\n * - **the top level is the query as saved** — `schema` (required), `scope` a bare\n * path, `where`, `sort` one key spelled `date` / `-date`, `limit` — and `whole`\n * when the whole record is wanted. Its records are the set, and the top level is\n * the same on every question about it;\n * - **`narrow`** — how this page takes less than the set: `where`, `match` (a\n * parametric page's record), `sort`, `cursor` (to resume a previous answer) and\n * `limit`. Absent when the question asks for the set as it is.\n *\n * ⛔ Until 2026-09-14 the two were one flat question — the page's `where` joined the\n * query's, its `sort` and `limit` replaced the query's — and `match` and `cursor` sat\n * at the top level, where the service no longer takes them. There is no fallback:\n * the service answers an unknown or misplaced field with a `400` for the request.\n *\n * ⭐ A where-object crosses exactly as authored: the evaluator and the service speak\n * one language (`@uniweb/core`'s where evaluator). ⛔ `nin` was respelled `not_in`\n * here until 2026-09-13, when the evaluator took `not_in` as its only spelling.\n * Anything the service does not answer (a retired operator) is sent as written and\n * answered there: never approximated.\n */\nfunction toQuestion(request) {\n const q = { schema: request.schema }\n const where = request.where && typeof request.where === 'object' ? request.where : null\n // ⭐ `scope` crosses as authored. ⛔ A top-level `where.path.under` was respelled\n // into it until 2026-09-11, when that form was retired in favour of `scope:`\n // [Diego] — the build refuses it now, so there is nothing left to respell.\n const scope = typeof request.scope === 'string' && request.scope ? request.scope : null\n if (scope) q.scope = scope\n if (where) q.where = where\n const sort = wireSort(request.sort)\n if (sort) q.sort = sort\n if (typeof request.limit === 'number' && request.limit > 0) q.limit = request.limit\n // ⛔ ONLY WHEN TRUE. The brief is the default and absent means the brief, so\n // sending `whole: false` would be noise on every list question the client makes.\n if (request.whole === true) q.whole = true\n const narrow = toNarrow(request.narrow)\n if (narrow) q.narrow = narrow\n // ⛔ `exhaustive` and `maxPages` deliberately do NOT cross — they are client\n // instructions about how many times to ask, never part of what is being asked.\n return q\n}\n\n/**\n * A question's `narrow`, with only the fields the service takes inside it — any other\n * is a `400` for the whole request there — or `null` when nothing is left.\n *\n * - `where` crosses as authored;\n * - `match` — the record of a parametric page: one key and the URL's value, beside\n * the author's `where`, never merged into it (`buildDetailConfig`);\n * - `sort` in the service's spelling;\n * - ⭐ `cursor` — the ONLY field in a question that is not the author's: opaque, from a\n * previous answer's `cursors`, sent back on the same question and nothing else\n * changed. A cursor is a narrowing like the rest, so the top level stays the query as\n * saved on every page;\n * - `limit`, when it cuts.\n */\nfunction toNarrow(narrow) {\n if (!narrow || typeof narrow !== 'object') return null\n const n = {}\n if (narrow.where && typeof narrow.where === 'object') n.where = narrow.where\n if (narrow.match && typeof narrow.match === 'object') n.match = narrow.match\n const sort = wireSort(narrow.sort)\n if (sort) n.sort = sort\n if (typeof narrow.cursor === 'string' && narrow.cursor) n.cursor = narrow.cursor\n if (typeof narrow.limit === 'number' && narrow.limit > 0) n.limit = narrow.limit\n return Object.keys(n).length > 0 ? n : null\n}\n\n/**\n * A `sort` in the service's spelling — `date` / `-date` — or, for one outside the\n * language, the author's text AS WRITTEN, which the service answers by its own rule\n * (ignored: the author's arrangement). ⛔ Never a throw: this runs while a page's whole\n * batch is being composed, before any request, and until 2026-09-14 a sort `parseSort`\n * refuses — two keys, a bad direction — threw here and left every question of the batch\n * pending forever.\n */\nfunction wireSort(sort) {\n try {\n return sortToWire(sort)\n } catch {\n return typeof sort === 'string' && sort.trim() ? sort : null\n }\n}\n\n/**\n * Send one batch to the records service and hand each question its own answer.\n *\n * The response is `{ data, whole?, cursors?, partial? }` (backend's records-query\n * contract rev D, 2026-09-13), each map absent when empty: `data` answers exactly the\n * keys sent — `[]` when nothing matched, and `[]` too for a question an author got\n * wrong, since the service answers a mistake rather than refusing it; `whole[key]` says\n * a key was served as whole records; `cursors[key]` that more records remain past the\n * question's `narrow.limit` — a query's own `limit` defines the set and never produces\n * one; `partial[key]` that a source of the key did not answer. A key\n * missing from `data` is a protocol violation and is reported as an error, never as\n * silence. ⛔ `errors` and `limits` are not read: the service stopped sending them in\n * the same revision, and this client read both until 2026-09-14.\n *\n * ⭐ `cursors` ARE READ — 2026-09-06 [Diego], reversing the ruling that had them\n * \"received and IGNORED, because framework has no paging concept.\" ⛔ **That ruling\n * described our client and was silently wrong about our USERS:** a `cursors` entry is\n * how the service says there is more, and discarding it meant a hosted list could\n * render part of its records with no way for an author to tell that from the end of\n * the data. **The silent class, on a visitor's page.**\n *\n * Two behaviours, deliberately not one:\n *\n * - **a page render REPORTS** — `meta.partial` rides the answer, and nothing pages\n * automatically. Auto-paging here would put unbounded round trips in front of\n * paint for a section that may only show ten rows.\n * - **an exhaustive caller PAGES** — `request.exhaustive` follows `cursors`\n * until the service stops issuing them. `collectSiteRecords` is the caller\n * that wants it (a corpus is not a page), and `maxPages` bounds the loop so\n * a service that always answers with a cursor cannot spin.\n */\n\n/**\n * Pages an exhaustive request follows before stopping and reporting the answer\n * as partial. A caller sets its own with `request.maxPages`.\n *\n * ⚖️ **20 is a bound, not a target** — it is the value a real caller chose for a\n * real per-request budget (a search index, 20 × 100), taken as the default\n * because any bound prevents a runaway and a low one fails visibly rather than\n * expensively. A caller that knows its budget passes its own.\n */\nconst DEFAULT_MAX_PAGES = 20\nasync function flushAsked(url, queue, doFetch) {\n // One shared page loop: the batch is sent, and any entry that asked to be\n // exhaustive and came back with a cursor is re-sent alone until it is done.\n const pending = new Map()\n const body = {}\n const keys = []\n for (const entry of queue) {\n const base = entry.request.as || 'q'\n let key = base\n for (let n = 2; key in body; n += 1) key = `${base}#${n}`\n keys.push(key)\n body[key] = toQuestion(entry.request)\n }\n let parsed\n try {\n const response = await doFetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!response.ok) {\n // A protocol violation is refused for the WHOLE request with a problem body\n // whose `detail` names the key and the fault (an unknown operator, an empty\n // binding key, a non-BCP-47 locale segment…). Surface that sentence on every\n // key of the batch rather than the bare status: the author reads\n // `block.dataError` and the status alone says nothing they can act on.\n let detail = null\n try {\n const problem = safeParseJSON(await response.text())\n if (problem && typeof problem.detail === 'string' && problem.detail) detail = problem.detail\n } catch { /* an unreadable body falls back to the status line */ }\n const error = detail\n ? `HTTP ${response.status}: ${detail}`\n : `HTTP ${response.status}: ${response.statusText}`\n for (const entry of queue) entry.resolve({ data: null, error })\n return\n }\n parsed = await response.json()\n } catch (error) {\n const message = error?.name === 'AbortError' ? 'aborted' : (error?.message || String(error))\n // ⛔ AN ABORT KEEPS WHAT ARRIVED. On an exhaustive walk every in-flight key\n // fails at once here, so discarding held pages loses the whole corpus rather\n // than one key — the \"rejection that loses every key\" a caller cannot have.\n for (const entry of queue) {\n const held = Array.isArray(entry.collected) && entry.collected.length ? entry.collected : null\n entry.resolve(held\n ? { data: held, error: message, meta: withMeta({ partial: true, pages: entry.page }) }\n : { data: null, error: message })\n }\n return\n }\n const data = parsed && typeof parsed.data === 'object' && parsed.data ? parsed.data : {}\n // ⭐ `whole[key]` is present only for keys delivered as WHOLE entities — so a\n // key's ABSENCE is the brief, and a key present that never asked is the\n // brief-less Model saying so. Absent entirely when every key is briefs.\n const wholes = parsed && typeof parsed.whole === 'object' && parsed.whole ? parsed.whole : {}\n const cursors = parsed && typeof parsed.cursors === 'object' && parsed.cursors ? parsed.cursors : {}\n // ⭐ `partial[key]` — `{ source, code, detail }`: a source of that key did not answer,\n // and the key carries what the others returned. Unread, a degraded key looked complete.\n const degraded = parsed && typeof parsed.partial === 'object' && parsed.partial ? parsed.partial : {}\n queue.forEach((entry, i) => {\n const key = keys[i]\n // ⛔ A FAILURE MUST NOT DISCARD PAGES ALREADY COLLECTED. An exhaustive walk\n // that fails on page 7 has six pages in hand, and a caller that asked for a\n // corpus would rather have them marked partial than lose the key — losing it\n // is indistinguishable from \"this query has no records\".\n const held = Array.isArray(entry.collected) && entry.collected.length ? entry.collected : null\n if (!(key in data)) {\n const error = `the records service answered without the key \"${key}\"`\n entry.resolve(held\n ? { data: held, error, meta: withMeta({ partial: true, pages: entry.page }) }\n : { data: null, error })\n return\n }\n const whole = typeof wholes[key] === 'boolean'\n ? wholes[key]\n : (typeof entry.request.whole === 'boolean' ? entry.request.whole : undefined)\n\n const cursor = typeof cursors[key] === 'string' && cursors[key] ? cursors[key] : null\n const rows = Array.isArray(data[key]) ? data[key] : data[key]\n\n // An exhaustive caller collects the page and asks for the next one.\n if (cursor && entry.request.exhaustive && Array.isArray(rows)) {\n const acc = entry.collected ? entry.collected.concat(rows) : rows.slice()\n const page = (entry.page || 1) + 1\n const cap = typeof entry.request.maxPages === 'number' && entry.request.maxPages > 0\n ? entry.request.maxPages\n : DEFAULT_MAX_PAGES\n if (page <= cap) {\n pending.set(entry, { cursor, collected: acc, page, whole })\n return\n }\n // The caller's own bound, not the service's: report rather than spin.\n entry.resolve({ data: acc, meta: withMeta({ whole, partial: true, pages: cap }) })\n return\n }\n\n const collected = entry.collected ? entry.collected.concat(Array.isArray(rows) ? rows : []) : rows\n const meta = withMeta({\n whole,\n // ⭐ ONE FLAG, MEANING **NOT THE WHOLE POPULATION** — asked for by name, so\n // a caller has one boolean to branch on rather than several signals to\n // combine. It is set in every case that means it:\n // · a cursor came back and this caller does not page (a page render);\n // · a cursor came back and the caller's `maxPages` stopped the walk;\n // · the walk failed or aborted with pages already in hand;\n // · a source of the key did not answer (`partial[key]`), and `unavailable`\n // carries the service's own account of which one.\n // ⛔ Named `truncated` when it shipped in 0.17.0; renamed the same day, before\n // any consumer adopted it, because \"truncated\" says something was cut and this\n // also means \"there is more you did not ask for\".\n partial: (cursor || key in degraded) ? true : undefined,\n unavailable: key in degraded ? degraded[key] : undefined,\n pages: entry.page && entry.page > 1 ? entry.page : undefined,\n })\n entry.resolve(meta ? { data: collected, meta } : { data: collected })\n })\n\n if (pending.size === 0) return\n // Re-ask each unfinished key on its own — the cursor is per key, so a batch\n // would have to correlate several independent positions through one body.\n // ⭐ The cursor rides inside `narrow` and NOTHING ELSE in the question changes: the\n // service resumes only the question a cursor came from, and answers a cursor sent\n // with anything else changed with a `400`.\n await Promise.all([...pending].map(([entry, state]) => {\n const next = {\n ...entry,\n request: { ...entry.request, narrow: { ...(entry.request.narrow || {}), cursor: state.cursor } },\n collected: state.collected,\n page: state.page,\n }\n return flushAsked(url, [next], doFetch)\n }))\n}\n\n/**\n * Read one response and parse its body — the part of a fetch every view of an\n * address shares. A JSON body is parsed as JSON; anything else is tried as JSON\n * and kept as text when it is not. A failed response keeps its text, so a caller\n * can read a message out of it (the records service's problem details).\n *\n * ⛔ A WHOLE HTML DOCUMENT IS NOT DATA — it is a failed read. A host that serves a site\n * answers an address it holds no file for with the site's page, a `200` of `text/html`,\n * and kept as text that page reached a component as a query's records: the component\n * called `.map` on it and the page fell to its error boundary. Measured 2026-09-26 on a\n * site a local backend serves, `/es/data/team.json` answering with the page. A fragment of\n * HTML is still kept as text; only a document — `<!doctype html>` or `<html>` — is refused.\n */\nasync function readResponse(doFetch, target, init) {\n const response = await doFetch(target, init)\n if (!response.ok) {\n let text = null\n try {\n text = await response.text()\n } catch {\n // Body not readable — the status line is all there is.\n }\n return { ok: false, status: response.status, statusText: response.statusText, text }\n }\n const contentType = response.headers.get('content-type') || ''\n if (contentType.includes('application/json')) return { ok: true, body: await response.json() }\n const text = await response.text()\n try {\n return { ok: true, body: JSON.parse(text) }\n } catch {\n if (HTML_DOCUMENT.test(text)) {\n return { ok: false, status: response.status, statusText: 'an HTML page, not data', text: null }\n }\n return { ok: true, body: text }\n }\n}\n\nconst HTML_DOCUMENT = /^\\s*(?:<!doctype\\s+html|<html[\\s>])/i\n\n/** Build a `meta` from the fields that are actually present, or `undefined`. */\nfunction withMeta(fields) {\n const out = {}\n for (const [k, v] of Object.entries(fields)) if (v !== undefined) out[k] = v\n return Object.keys(out).length ? out : undefined\n}\n\n/**\n * The locale a list's texts are collated in: the page's (ruled 2026-09-14 [Diego]).\n * A request asked in a locale carries it; otherwise the caller's — the website's\n * active locale in the browser, the locale a host's prerender renders.\n */\nfunction sortLocale(request, ctx) {\n if (typeof request?.locale === 'string' && request.locale) return request.locale\n if (typeof ctx?.locale === 'string' && ctx.locale) return ctx.locale\n const active = ctx?.website?.getActiveLocale?.()\n return typeof active === 'string' && active ? active : null\n}\n\n/**\n * Evaluate the query over what the source returned — the ONE evaluator,\n * `@uniweb/core`'s `evaluateQuery`, so the browser orders and filters exactly as\n * the build does: the query's set first (`scope` over the branch a compiled record is\n * held with, `where`, `sort`, `limit`), then the fetch's `narrow` of it. What comes back\n * carries no branch — `evaluateQuery` drops it, so no component receives one. Only the\n * sort is this lane's own, for what it does with a bad `sort:` in production.\n */\nfunction applyOperators(data, request, { dev = false, locale = null } = {}) {\n return evaluateQuery(data, request, {\n locale,\n sort: (items, sortExpr, options) => applySort(items, sortExpr, dev, options.locale),\n })\n}\n\n/**\n * ⛔ This was a second sort implementation until 2026-09-04, and it honoured\n * a comma-separated MULTI-KEY sort the language does not have (single-key by\n * ruling). A bad `sort:` is an authoring error: dev throws so it is seen; in\n * production the records are delivered in source order and the reason is\n * logged once — a wrong order is not worth a broken page for a visitor.\n */\nconst warnedBadSorts = new Set()\nfunction applySort(items, sortExpr, dev, locale) {\n try {\n return sortRecords(items, sortExpr, { locale })\n } catch (err) {\n if (dev) throw err\n const key = String(sortExpr)\n if (!warnedBadSorts.has(key)) {\n warnedBadSorts.add(key)\n console.error(`[default-fetcher] ${err.message} Records delivered unsorted.`)\n }\n return items\n }\n}\n\n/**\n * Walk a dotted path into an object. Missing segments short-circuit to\n * `undefined` so callers can distinguish \"present and empty\" from \"not there.\"\n */\nfunction getNestedValue(obj, path) {\n if (!obj || !path) return obj\n let current = obj\n for (const part of path.split('.')) {\n if (current === null || current === undefined) return undefined\n current = current[part]\n }\n return current\n}\n\n/**\n * JSON.parse that returns `undefined` on failure instead of throwing.\n * Used when we want to probe a response body for an error path but don't\n * want a non-JSON body to surface as a parser exception.\n */\nfunction safeParseJSON(text) {\n try {\n return JSON.parse(text)\n } catch {\n return undefined\n }\n}\n","/**\n * THE RENDER'S DATA STEP — the page a visit starts on, fetched by the rule that renders it.\n *\n * ⭐ **Why a step at all.** The server render is synchronous: while it renders, a section reads its\n * data from the cache and never waits for a request (`EntityStore.resolve`). So whatever a page\n * shows has to be there before the render starts. That is this, and nothing else: it is not an\n * optimization, it is how a server-rendered page has content.\n *\n * ⭐ **Why it runs the render's own rule.** It asks, block by block, exactly what the render will\n * look for — `EntityStore.fetch`, the same plan `resolve` reads back (`@uniweb/core/page-data`) —\n * through a dispatcher built from the site's own foundation, so a transport the site chose answers\n * here too. ⛔ Until 2026-09-19 a host's prefetch worked this out from the payload alone: it could\n * not know which keys a component declares, which layout the page draws, or which transport a key\n * is routed to, so it asked for every fetch on every level and still got four cases wrong\n * (`kb/framework/plans/what-a-page-needs.md` §0). A payload cannot answer those; the graph the\n * isolate is about to render with can.\n *\n * ⚖️ **What it still over-asks.** The blocks are the page's — body, the layout areas it uses, their\n * child blocks and insets. Whether a component actually draws its children, or a layout its area,\n * is known only by rendering; a block that declares keys and is never drawn is asked for anyway.\n * Bounded by what such a block declares, and cheaper than rendering the page twice.\n *\n * @module\n */\nimport DataStore, { deriveCacheKey } from '@uniweb/core/datastore'\nimport { resolvePage } from './resolve-page.js'\nimport { getComponentMeta } from './prepare-props.js'\nimport { createDefaultFetcher } from './default-fetcher.js'\n\n/**\n * Every block the page's render can reach: its body and the layout areas it uses (the runtime's own\n * layout choice — the page's `layout:`, else the foundation's default, else the site's default set,\n * minus any area the page hides), plus what those blocks hold.\n */\nfunction blocksOf(page) {\n const out = []\n const walk = (list) => {\n for (const block of list || []) {\n if (!block) continue\n out.push(block)\n walk(block.childBlocks)\n walk(block.insets)\n }\n }\n walk(page.getBodyBlocks?.() ?? page.bodyBlocks)\n for (const area of Object.values(page.getLayoutAreas?.() ?? {})) walk(area)\n return out\n}\n\n/**\n * Fetch what one route's render will read.\n *\n * @param {Object} options\n * @param {Object} options.website - the initialized Website the render will use (`initPrerender`)\n * @param {string|Object} options.route - the route, or an already-resolved Page\n * @param {Function} options.fetch - the transport, per request\n * @param {boolean} [options.dev]\n * @param {string|null} [options.locale] - ⛔ NOT a locale to fetch in: the Website is already\n * locale-sliced (`initPrerenderForLocale`) and every address is resolved in ITS locale. Passing\n * one that disagrees throws, because the alternative is a page prerendered in one language with\n * the data of another, and nothing to see it.\n * @param {'always'|'author'} [options.prerender] - `'always'` asks for everything, which is what an\n * isolate rendering a visit's first page wants; `'author'` honours `prerender: false`.\n * @param {Object} [options.cache] - a `DataStore` to answer from and fill, shared across calls.\n * ⭐ For a caller that runs the step for MANY pages of one site — a static build — where every\n * page asks for the site's and its parent's data and reading each address once is the difference\n * between N reads and N×M. ⛔ Not for a host serving requests: there a page's data is asked\n * fresh, and the default (a cache per call) is what does that. A shared cache changes nothing\n * about what is ASKED or returned — every request is still recorded, hit or miss.\n * @returns {Promise<Array<{ config: Object, outcome: 'fetched'|'failed'|'skipped', data: *, meta?: Object, error?: string }>>}\n * one entry per request the render will make, keyed downstream by `deriveCacheKey(config)`\n */\nexport async function loadPageData({ website, route, fetch, dev = false, locale = null, prerender = 'always', cache = null }) {\n if (prerender !== 'author' && prerender !== 'always') {\n throw new Error(`loadPageData: prerender must be 'author' or 'always', got ${JSON.stringify(prerender)}`)\n }\n const active = website?.getActiveLocale?.() ?? null\n if (locale && active && locale !== active) {\n throw new Error(\n `loadPageData: this Website renders ${JSON.stringify(active)} and the caller asked for ` +\n `${JSON.stringify(locale)}. A Website is locale-sliced — build one per locale ` +\n '(`initPrerenderForLocale`) rather than asking this step for another language.'\n )\n }\n const resolved = typeof route === 'string' ? resolvePage(website, route) : route\n // Nothing matched: a genuine 404, and the caller's to turn into one. Nothing to fetch.\n if (!resolved) return []\n // ⭐ The same promotion the render applies: a content-less folder renders its designated child,\n // and it is that child's data the page needs.\n const page = typeof resolved.getRenderableSelf === 'function' ? resolved.getRenderableSelf() : resolved\n\n // ⛔ A real Page, not page data. `createPageRenderer` tolerates plain data because rendering it\n // still produces a page; here the same tolerance would walk raw sections, find no blocks and no\n // graph behind them, and fetch NOTHING — a clean run, an empty page, no error anywhere. That is\n // the failure this whole step exists to end, so it is refused.\n if (typeof page.getBodyBlocks !== 'function') {\n throw new Error('loadPageData: `route` must be a route or a Page from this Website — page data is not enough to say what its render reads.')\n }\n\n // A cache of this call's own unless the caller shares one: the graph's belongs to whatever it is\n // rendering, and a page's data is asked fresh per request. The site's transports still apply\n // (`dispatcherFor`).\n const dispatcher = website.dispatcherFor({\n dataStore: cache ?? new DataStore(),\n defaultFetcher: createDefaultFetcher({ basePath: website.config?.base || '', dev, fetch }),\n })\n\n /** One entry per request, in the order they were first asked for. */\n const entries = new Map()\n const record = (request, entry) => {\n const key = deriveCacheKey(request)\n if (!entries.has(key)) entries.set(key, entry)\n }\n\n const recording = {\n peek: (request, ctx) => dispatcher.peek(request, ctx),\n peekRecord: (id) => dispatcher.peekRecord(id),\n dispatch: async (request, ctx) => {\n if (prerender === 'author' && request?.prerender === false) {\n // The author deferred this one to the browser and the caller honours that. Present, so a\n // host can count what was declared against what was tried; not fetched, not hydrated.\n record(request, { config: request, outcome: 'skipped', data: null })\n return { data: null }\n }\n const result = await dispatcher.dispatch(request, ctx)\n record(request, result?.error\n ? { config: request, outcome: 'failed', data: null, error: result.error }\n : { config: request, outcome: 'fetched', data: result?.data ?? null, ...(result?.meta ? { meta: result.meta } : {}) })\n return result\n },\n }\n\n const store = website.entityStore\n // ⭐ Every block's first request goes out in the same tick, which is what lets a question door\n // answer a page in one POST. A step that depends on an answer takes a later one, and only its\n // own key waits.\n await Promise.all([\n // The page's own record: asked whether or not a section declares the route key, because the\n // page names itself from it and a URL naming no record is a 404.\n store.fetchPageRecord(page, { dispatcher: recording }),\n ...blocksOf(page).map((block) => store.fetch(block, getComponentMeta(block.type), { dispatcher: recording })),\n ])\n\n return [...entries.values()]\n}\n","/**\n * The composed render entry — resolve a route, render it, inject it into a shell.\n *\n * ⭐ **This exists because framework had two callers of one unshared sequence, not\n * because a consumer asked.** `@uniweb/runtime/ssr` exported every step of the\n * per-page render and never the sequence, so each host assembled it:\n *\n * - `@uniweb/build`'s `prerender.js` — `renderPage` → classify → `injectPageContent`,\n * once per page in its loop.\n * - an SSR isolate prerendering the page a visit starts on (the SPA renders every\n * page after it) — `resolvePage` → `renderPage` → `injectPageContent`, and it wrote\n * the route lookup three times in three files before `resolvePage` was exported at\n * all (see that function's header).\n *\n * Two unlike callers is what makes the interface honest: a build bakes files and an\n * isolate answers a request, so anything only one of them needs stayed out.\n *\n * ⛔ WHAT IS DELIBERATELY NOT IN HERE, and the boundary is the point:\n *\n * - **Shell assembly.** The shell arrives built. The import map, the CDN base and\n * cache headers are host layout, and a runtime that assembled them would be\n * modelling a deployment it cannot see (the host drew this line itself,\n * 2026-09-03: a serve location is read from the payload, never constructed here).\n * - **Init and hydration.** The two lanes differ REALLY here, not incidentally: a\n * build initializes once and hydrates every collection up front, an isolate\n * initializes per locale and prefetches per route. Folding either in would fit\n * one caller and lie to the other. `initPrerender` / `initPrerenderForLocale` /\n * `prefetchPageData` / `hydrateDataStore` stay separate exports, and\n * `prefetchAndHydrate` below is the isolate's two-step, not a general one.\n * - **Anything build-only.** `injectBuildData` stays in the build lane; it is the\n * other half of the head seam and has its own parity guard.\n */\nimport { resolvePage, renderPage, classifyRenderError, injectPageContent } from './ssr-renderer.js'\nimport { hydrateDataStore } from './wire-foundation.js'\nimport { loadPageData } from './load-page-data.js'\n\n/**\n * A renderer bound to one initialized Website and one shell.\n *\n * Create it once per locale (the Website is already locale-sliced by\n * `initPrerenderForLocale`) and call `render` per route or per page.\n *\n * @param {Object} opts\n * @param {Object} opts.website - `uniweb.activeWebsite`, already initialized\n * @param {string} opts.shell - the HTML shell to inject into, taken as given\n * @returns {{ website: Object, render: Function }}\n */\nexport function createPageRenderer({ website, shell }) {\n if (!website) throw new Error('createPageRenderer: `website` is required')\n if (typeof shell !== 'string') throw new Error('createPageRenderer: `shell` must be an HTML string')\n\n /**\n * Render one page.\n *\n * ⭐ Returns an OUTCOME rather than throwing or returning a bare string, because\n * the two callers branch differently on the same three cases and neither wants an\n * exception: a build logs and keeps going so one broken section cannot fail a whole\n * site, an isolate decides a status code and a cache policy. Same reasoning as\n * `prefetchPageData`'s per-entry outcome.\n *\n * @param {string|Object} target - a route (`/blog/1`, resolved through the same\n * matcher the browser uses, so a dynamic route works) or an already-resolved\n * Page, which the build lane already holds from its own loop.\n * @param {Object} [options]\n * @param {Object} [options.inject] - extra options forwarded to `injectPageContent`\n * @returns {{ outcome: 'rendered'|'notFound'|'failed', html: string|null,\n * page: Object|null, error: {type: string, message: string}|null }}\n */\n function render(target, { inject = {} } = {}) {\n const resolved = typeof target === 'string' ? resolvePage(website, target) : target\n\n // ⛔ Not an error: nothing matched, which is a genuine 404 and the caller's to\n // turn into one — a build skips it, an isolate serves its 404 page with a 404\n // status. Returning `failed` here would make those indistinguishable.\n if (!resolved) return { outcome: 'notFound', html: null, page: null, error: null }\n\n // ⭐ **THE INDEX-CHILD PROMOTION APPLIES TO BOTH TARGET FORMS** — ruled\n // 2026-09-12 [Diego]. A content-less folder with a designated `isIndex` child\n // renders that child. `Website#getPage` already did this for a ROUTE on its\n // exact-match branches, and nothing did it for an already-resolved Page, so the\n // same page rendered differently depending on which form the caller happened to\n // hold — silently, with no error on either path. `getRenderableSelf` is the one\n // rule for it and now has exactly one caller: here.\n // ⚠️ Guarded on the method rather than assumed: the documented target is a Page,\n // but a caller handing in plain page data should degrade to \"render what I gave\n // you\", not throw.\n const page = typeof resolved.getRenderableSelf === 'function' ? resolved.getRenderableSelf() : resolved\n\n let result\n try {\n result = renderPage(page, website)\n } catch (err) {\n // `renderPage` handles its own errors, but a foundation can throw from\n // module scope in ways it does not catch. Classify rather than propagate,\n // so one page cannot take down a build loop or an isolate's request.\n return { outcome: 'failed', html: null, page, error: classifyRenderError(err) }\n }\n\n if (result.error) return { outcome: 'failed', html: null, page, error: result.error }\n\n // ⛔ `sectionOverrideCSS` LAST, so a caller's `inject` cannot displace it. It is\n // computed by `renderPage` for this page — theme pinning and component vars —\n // and a caller passing a same-named key would silently drop it, rendering a page\n // that looks fine and is unstyled in exactly the places the author pinned. That\n // is the empty-success shape this module keeps refusing elsewhere; the spread was\n // the other way round for one commit.\n const html = injectPageContent(shell, result.renderedContent, page, {\n ...inject,\n sectionOverrideCSS: result.sectionOverrideCSS,\n })\n return { outcome: 'rendered', html, page, error: null }\n }\n\n return { website, render }\n}\n\n/**\n * Prefetch a route's data and hydrate it onto the graph — the isolate's two-step.\n *\n * ⭐ Its whole purpose is that a host stops assembling our structure by hand. It\n * returns the prefetch outcomes rather than swallowing them, because a host reads\n * them to tell \"nothing was tried\" from \"everything tried failed\", which is a\n * different cache decision.\n *\n * ⭐ **Since 2026-09-19 it asks what the RENDER will read, block by block** (`loadPageData`),\n * on the Website it is handed: the keys each component declares, the layout the page actually\n * draws, and the transport the site chose for a key. `content` is no longer read — the graph\n * carries everything — and stays in the signature for callers that pass it.\n * ⛔ `prefetchPageData` works from the payload alone and cannot know any of those; it asks for\n * every fetch on every level instead, and still gets four cases wrong\n * (`kb/framework/plans/what-a-page-needs.md` §0). Prefer this entry.\n *\n * ⛔ The build lane does NOT call this: it hydrates every collection once, before\n * its page loop, from its own executor that honours the author's `prerender:` flag.\n * That difference is why this is a named isolate helper and not a step inside\n * `render`.\n *\n * @returns {Promise<Array<{config: Object, outcome: string, data: any, error?: string}>>}\n */\nexport async function prefetchAndHydrate({ website, content, route, locale = null, fetch = null, dev = false, prerender = 'always' }) {\n // ⛔ Guarded for the same reason `createPageRenderer` is, and it was not for one\n // commit. `hydrateDataStore` no-ops on a graph with no `dataStore`, so a caller\n // passing the wrong object gets a successful-looking prefetch, an unhydrated graph\n // and a page that renders empty — no error anywhere. Fail where the mistake is.\n if (!website?.dataStore) {\n throw new Error('prefetchAndHydrate: `website` must be an initialized Website with a dataStore')\n }\n\n // ⛔ THE TRANSPORT IS REQUIRED HERE, unlike on `prefetchPageData`, and this is the\n // one place the difference matters.\n //\n // A function does NOT survive every isolate boundary. Measured by a host against\n // a real isolate loader, 2026-09-03: passed through an\n // entrypoint's `fetch(Request)` with a JSON body the transport arrives\n // **`undefined`**; passed as an argument to an RPC method it arrives as a callable\n // function and the isolate invokes it. Only the RPC shape carries it.\n //\n // ⚠️ And `undefined` is not where it stops, which is the part their measurement\n // could not see from outside our code. `createDefaultFetcher` resolves the\n // transport as `fetchImpl || globalThis.fetch`, so a transport that failed to cross\n // silently becomes THE ISOLATE'S OWN NETWORK — outside the host's timeout, byte\n // budget and site-relative address resolution. With an absolute address it does not\n // even fail: the request goes out from the wrong place and comes back\n // `outcome: 'fetched'`. A wiring mistake wearing a success.\n //\n // ⇒ So the entry whose only caller crosses that boundary demands a real function\n // rather than defaulting. A Node or browser caller that genuinely wants the global\n // passes `fetch: globalThis.fetch` — one word, and it says so.\n if (typeof fetch !== 'function') {\n throw new Error(\n 'prefetchAndHydrate: `fetch` must be a function. A transport does not survive a ' +\n 'JSON-serialized isolate boundary — pass it as an RPC method argument. ' +\n 'To use the ambient fetch deliberately, pass `fetch: globalThis.fetch`.'\n )\n }\n // ⛔ `locale` is forwarded to be CHECKED, not applied: the Website is locale-sliced already, so a\n // caller whose two disagree gets an error rather than a page prerendered in one language with the\n // data of another.\n const fetched = await loadPageData({ website, route, fetch, dev, locale, prerender })\n hydrateDataStore(website, fetched)\n return fetched\n}\n","/**\n * Every record a site's declared queries return — for a host building something\n * over the whole corpus rather than rendering one page.\n *\n * ## Why this is framework's and not the caller's\n *\n * A host that indexes a site has to ask the same questions the site's pages ask.\n * ⛔ **If it composes its own, the index and the page can disagree**, and the way\n * they disagree is the worst one available: the index offers a result whose page\n * then renders empty. That is the two-answers-to-one-question failure the GET\n * lane was retired for, reintroduced one lane over.\n *\n * ⇒ So the composition is not duplicated here either. This walks the site's\n * `config.queries`, hands each one to `resolveFetchConfigs` — **the same rule a\n * page render uses**, applying each saved query's own `scope` and `where` — and\n * asks through the same client.\n *\n * ## ⭐ Each query is asked AS SAVED — its `limit` included\n *\n * A query defines its SET: the records its `scope` and `where` select, in its\n * `sort`, up to its `limit` (ruled 2026-09-14 [Diego]). A parametric page has a URL\n * for each record of its route query's set and no other, so the set is exactly the\n * population a site's pages reach — a field note older than a query's 100 most recent\n * has no page to index. Asked with no `narrow`, the service answers the set whole in\n * one answer, so a walk has nothing to page; it still follows a cursor if one ever\n * comes back, bounded by `maxPages`.\n *\n * ⛔ **Until 2026-09-14 this dropped the query's `limit`**, on the rule it replaced — a\n * `limit` was the list page's presentation and every record matching `scope` + `where`\n * had a page — which was right under that rule. ⚠️ And that rule's own history: the\n * first version, in 0.17.0, passed `limit` through while claiming the corpus was the\n * population detail pages reach, and the consumer found it (2026-09-06).\n *\n * ## What the caller supplies\n *\n * ⭐ **The transport, and nothing else.** A host outside a browser decides how a\n * site-relative address is dispatched — through its own origin, a service\n * binding, a forwarding table that turns `/_…` into an upstream — and hands that\n * in as `fetch`. **Framework decides what to ask and how to read the answer;\n * the host decides how the bytes travel.** That split is the whole of the\n * boundary here.\n *\n * ⛔ **A site with no records service is not an error.** It has no live records,\n * so this returns empty rather than throwing: a static site's corpus comes from\n * its own compiled artifacts, which the caller already has.\n */\n\nimport { resolveFetchConfigs } from '@uniweb/core/fetch-config'\nimport { resolveRecordsService } from '@uniweb/core/records-service'\nimport { createDefaultFetcher } from './default-fetcher.js'\n\n/**\n * Ask a site's records service for every record its declared queries return.\n *\n * @param {Object} content - the render payload (`config.services`, `config.queries`)\n * @param {Object} options\n * @param {string} options.locale - the locale to ask in; it is a route segment\n * of the service's address, so there is no asking without one\n * @param {Function} options.fetch - the transport, `(url, init) => Response`\n * @param {AbortSignal} [options.signal]\n * @param {string[]} [options.only] - restrict to these query names\n * @param {boolean} [options.whole=false] - what to ask for. `false` is the brief a\n * list shows; **`true` — whole records — is what an index wants**: a brief index\n * cannot match body text the record's own detail page displays, and a reader who\n * finds a word on the page and not in search meets the inconsistency two\n * rankings would produce. The cost is the caller's, and bounded by each query's\n * own `limit`. An external query is never collected — it has no records service\n * to ask.\n * @param {number} [options.maxPages] - the caller's own bound on a walk that pages. A\n * question with no `narrow` is answered whole with no cursor, so today's walk is one\n * request per query; the bound holds should a service page anyway.\n * @returns {Promise<{records: Object, errors: Object|null, meta: Object}>}\n * `records` is keyed by query NAME, each a flat array; `errors` is keyed the\n * same and is null when nothing failed; `meta[name]`, when there is anything to\n * say, carries `{ whole?, pages?, partial?, unavailable? }`.\n *\n * ⭐ **`partial` means NOT THE WHOLE POPULATION**, and a key can be in BOTH\n * `records` and `errors`: a walk that failed or was aborted with pages already\n * in hand keeps them, marked. ⛔ Losing them would be indistinguishable from\n * \"this query has no records\", and on an abort every in-flight key fails at\n * once — so discarding would lose the corpus, not a key.\n */\nexport async function collectSiteRecords(\n content,\n { locale, fetch, signal, only = null, whole = false, maxPages } = {},\n) {\n const config = content?.config\n const services = config?.services ?? null\n const queries = config?.queries ?? null\n\n // No service, no queries, or no locale to ask in ⇒ nothing to collect. Each is\n // an ordinary state of a site, not a fault: the caller reads its own artifacts.\n if (!queries || typeof queries !== 'object') return empty()\n if (!resolveRecordsService(services, locale)) return empty()\n\n const names = Object.keys(queries).filter((n) => (only ? only.includes(n) : true))\n if (names.length === 0) return empty()\n\n // One synthetic source per query — `as` is the query's own name, so the answer\n // comes back under the name the caller asked by.\n const configs = resolveFetchConfigs(\n names.map((name) => ({ query: name, as: name })),\n { services, queries, locale, defaultLocale: config?.defaultLanguage ?? locale },\n )\n\n const fetcher = createDefaultFetcher({ fetch, basePath: config?.base ?? '' })\n const records = {}\n const errors = {}\n const meta = {}\n\n await Promise.all([...configs].map(async ([name, cfg]) => {\n // ⛔ Only a config the service answers. A query that resolved to a compiled\n // `path` has no live lane, and reading that file is the caller's business,\n // not ours — it is in the site's own URL space and they already serve it.\n if (!cfg.ask) return\n // The query as saved — the set its pages reach, its `limit` included; see the header.\n const asked = { ...cfg, whole, exhaustive: true }\n if (typeof maxPages === 'number' && maxPages > 0) asked.maxPages = maxPages\n\n const result = await fetcher.resolve(asked, { signal })\n if (result?.error) errors[name] = result.error\n // ⭐ Data and an error are not exclusive: a partial walk reports both, and\n // the caller decides whether partial is usable. Only a walk that collected\n // nothing leaves the key out of `records` entirely.\n if (Array.isArray(result?.data)) records[name] = result.data\n else if (!result?.error) records[name] = []\n if (result?.meta) meta[name] = result.meta\n }))\n\n return { records, errors: Object.keys(errors).length ? errors : null, meta }\n}\n\nfunction empty() {\n return { records: {}, errors: null, meta: {} }\n}\n"],"names":["text","fetch","deriveCacheKey"],"mappings":"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"}
1
+ {"version":3,"file":"ssr.js","sources":["../src/prepare-props.js","../src/background-shared.js","../src/default-404.js","../src/wire-foundation.js","../src/area-wrappers.js","../src/appearance.js","../src/document-title.js","../src/resolve-page.js","../src/ssr-renderer.js","../src/default-fetcher.js","../src/load-page-data.js","../src/page-renderer.js","../src/collect-records.js"],"sourcesContent":["/**\n * Props Preparation for Runtime Guarantees\n *\n * Prepares props for foundation components with:\n * - Param defaults from runtime schema\n * - Guaranteed content structure (no null checks needed)\n * - Field defaults applied to `content.data` items from the bound schemas\n *\n * This enables simpler component code by ensuring predictable prop shapes.\n */\n\nimport { isRichSchema } from '@uniweb/core'\n\n/**\n * Guarantee item has flat content structure\n *\n * @param {Object} item - Raw item from parser\n * @returns {Object} Item with guaranteed flat structure\n */\nfunction guaranteeItemStructure(item) {\n return {\n title: item.title || '',\n pretitle: item.pretitle || '',\n subtitle: item.subtitle || '',\n paragraphs: item.paragraphs || [],\n links: item.links || [],\n images: item.images || [],\n lists: item.lists || [],\n icons: item.icons || [],\n videos: item.videos || [],\n // ⛔ An entry's own insets were left out until 2026-09-29, as `tables` was: the\n // parser delivered them and this object, built key by key, did not carry them.\n insets: item.insets || [],\n media: item.media || [],\n snippets: item.snippets || [],\n buttons: item.buttons || [],\n data: item.data || {},\n cards: item.cards || [],\n documents: item.documents || [],\n forms: item.forms || [],\n quotes: item.quotes || [],\n headings: item.headings || [],\n ...(item.math && item.math.length ? { math: item.math } : {}),\n ...(item.tables && item.tables.length ? { tables: item.tables } : {}),\n }\n}\n\n/**\n * Guarantee content structure exists\n * Returns a flat content object with all standard fields guaranteed to exist\n *\n * @param {Object} parsedContent - Raw parsed content from semantic parser (flat structure)\n * @returns {Object} Content with guaranteed flat structure\n */\nexport function guaranteeContentStructure(parsedContent) {\n const content = parsedContent || {}\n\n return {\n // Flat header fields\n title: content.title || '',\n pretitle: content.pretitle || '',\n subtitle: content.subtitle || '',\n alignment: content.alignment || null,\n\n // Flat body fields\n paragraphs: content.paragraphs || [],\n links: content.links || [],\n images: content.images || [],\n lists: content.lists || [],\n icons: content.icons || [],\n videos: content.videos || [],\n insets: content.insets || [],\n // The section's visual media — images, videos and insets on their own line — in\n // the order written, each with its `kind`: what a `media` slot holds (2026-09-29).\n media: content.media || [],\n snippets: content.snippets || [],\n buttons: content.buttons || [],\n data: content.data || {},\n cards: content.cards || [],\n documents: content.documents || [],\n forms: content.forms || [],\n quotes: content.quotes || [],\n headings: content.headings || [],\n\n // Rare collections — surfaced only when present so pages that don't\n // use them don't pay the allocation cost. Foundations that need them\n // should check for presence (content.math?.length) or use\n // content.sequence for in-order rendering.\n //\n // ⛔ `tables` was not passed through until 2026-09-29: the parser delivered it and\n // this object, built key by key, left it out — so a component declaring `tables`\n // received nothing, while `math`, its twin, arrived.\n ...(content.math && content.math.length ? { math: content.math } : {}),\n ...(content.tables && content.tables.length ? { tables: content.tables } : {}),\n\n // Items with guaranteed structure\n items: (content.items || []).map(guaranteeItemStructure),\n\n // Sequence for ordered rendering\n sequence: content.sequence || [],\n\n // Preserve raw content if present\n raw: content.raw,\n }\n}\n\n/**\n * Apply a schema to a single object\n * Only processes fields defined in the schema, preserves unknown fields\n *\n * @param {Object} obj - The object to process\n * @param {Object} schema - Schema definition (fieldName -> fieldDef)\n * @returns {Object} Object with schema defaults applied\n */\nfunction applySchemaToObject(obj, schema) {\n if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {\n return obj\n }\n\n const result = { ...obj }\n\n for (const [field, fieldDef] of Object.entries(schema)) {\n // Get the default value - handle both shorthand and full form\n const defaultValue = typeof fieldDef === 'object' ? fieldDef.default : undefined\n\n // Apply default if field is missing and default exists\n if (result[field] === undefined && defaultValue !== undefined) {\n result[field] = defaultValue\n }\n\n // Bare type strings ('string', 'decimal', …) carry nothing more to apply.\n if (typeof fieldDef !== 'object') continue\n\n // Inline picklist (`enum`): if the value is set but not among the allowed\n // values, fall back to the default. By value: an entry may be `{ value, label }`, as a\n // foundation built before its build lowered it to values still carries.\n if (Array.isArray(fieldDef.enum)) {\n const allowed = fieldDef.enum.map((e) => (e && typeof e === 'object' && 'value' in e ? e.value : e))\n if (result[field] !== undefined && !allowed.includes(result[field]) && defaultValue !== undefined) {\n result[field] = defaultValue\n }\n }\n\n // Nested object → recurse into its field map.\n if (fieldDef.type === 'object' && fieldDef.fields && result[field]) {\n result[field] = applySchemaToObject(result[field], fieldDef.fields)\n }\n\n // Array of objects → apply the element field map to each item.\n if (fieldDef.type === 'array' && fieldDef.items && Array.isArray(result[field])) {\n const items = fieldDef.items\n if (items && typeof items === 'object' && items.type === 'object' && items.fields) {\n result[field] = result[field].map((item) => applySchemaToObject(item, items.fields))\n }\n }\n }\n\n return result\n}\n\n/**\n * Apply a schema to a value (object or array of objects)\n *\n * @param {Object|Array} value - The value to process\n * @param {Object} schema - Schema definition\n * @returns {Object|Array} Value with schema defaults applied\n */\nfunction applySchemaToValue(value, schema) {\n if (Array.isArray(value)) {\n return value.map(item => applySchemaToObject(item, schema))\n }\n return applySchemaToObject(value, schema)\n}\n\n/**\n * Apply field defaults from a rich form `fields` array to an object.\n *\n * Recurses into `type: 'form'` (composite arrays with childSchema) and\n * `type: 'nestedObject'` / `type: 'object'` (single nested objects).\n *\n * Conditional visibility (`field.condition`) is not yet applied here —\n * components receive all fields the author filled plus defaults; hiding\n * is a later pass that requires the shared evaluateCondition util.\n *\n * @param {Object} obj - Row data (object keyed by field id)\n * @param {Array} fields - Rich field definitions\n * @returns {Object} - obj with defaults filled in\n */\nfunction applyRichFieldDefaults(obj, fields) {\n if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj\n if (!Array.isArray(fields)) return obj\n\n const result = { ...obj }\n\n for (const field of fields) {\n if (!field || typeof field !== 'object' || !field.id) continue\n const id = field.id\n\n if (result[id] === undefined && field.default !== undefined) {\n result[id] = field.default\n }\n\n if (field.type === 'form' && field.childSchema && Array.isArray(result[id])) {\n result[id] = result[id].map(item =>\n applyRichFieldDefaults(item, field.childSchema.fields)\n )\n } else if (\n (field.type === 'nestedObject' || field.type === 'object') &&\n Array.isArray(field.fields) &&\n result[id] &&\n typeof result[id] === 'object'\n ) {\n result[id] = applyRichFieldDefaults(result[id], field.fields)\n }\n }\n\n return result\n}\n\n/**\n * Apply a rich form schema to its stored value.\n *\n * Shape rules:\n * - composite (isComposite=true) → value is array of childSchema rows\n * - when `childRecords` is set, value may be `{ [childRecords]: [...] }`\n * - non-composite → value is a single object keyed by field id\n */\nfunction applyRichSchemaToValue(value, schema) {\n if (value == null) return value\n\n if (schema.isComposite && schema.childSchema) {\n const childFields = schema.childSchema.fields\n const queryKey = schema.childRecords\n\n if (queryKey && value && typeof value === 'object' && !Array.isArray(value)) {\n const arr = Array.isArray(value[queryKey]) ? value[queryKey] : []\n return {\n ...value,\n [queryKey]: arr.map(row => applyRichFieldDefaults(row, childFields)),\n }\n }\n\n if (Array.isArray(value)) {\n return value.map(row => applyRichFieldDefaults(row, childFields))\n }\n\n return value\n }\n\n if (Array.isArray(schema.fields)) {\n return applyRichFieldDefaults(value, schema.fields)\n }\n\n return value\n}\n\n/**\n * Apply schemas to content.data\n * Only processes tags that have a matching schema, leaves others untouched\n *\n * ## Two orders of schema — what a `data:` declaration may describe\n *\n * A component's `data:` is 1st order: a DEVELOPER says what shape the section\n * consumes. An authored form (```yaml:form```) is 2nd order: an AUTHOR says what\n * shape a VISITOR will submit. It is schema-shaped, but it is content.\n *\n * Declaring a schema for such a tag is legitimate, and it is worth being precise\n * about what it may describe:\n *\n * OK the DEFINITION's envelope — `title?`, `description?`, `fields: <map>`.\n * That asks \"is this a well-formed form?\", which is what build-time\n * validation is for (`build/src/validate-data.js` pairs a section's data\n * input with the schema its meta.js binds to that key).\n * WRONG a schema whose fields are THE FORM'S fields (`name`, `email`, …).\n * Those are author-chosen and unknowable at build time. A form-rendering\n * component receives its fields; it does not declare them.\n *\n * The mechanism is bounded and does not punish the mistake loudly:\n * `applySchemaToObject` recurses only where the schema declares structure\n * (`type: object` + `fields`, `type: array` + `items.fields`), so a\n * form-definition schema — which cannot name the author's fields — can never\n * reach into them. It fills the envelope defaults its own author declared.\n *\n * (Established with the editor team, 2026-07-31, channel frontend↔framework.\n * The editor shadows a foundation's `form` declaration with its own builder via\n * `builtinSchemas()`; that is about the EDITING UI and is orthogonal to whether a\n * foundation declares a schema for validation.)\n *\n * @param {Object} data - The data object from content\n * @param {Object} schemas - Schema definitions from runtime meta\n * @returns {Object} Data with schemas applied\n */\nexport function applySchemas(data, schemas) {\n if (!schemas || !data || typeof data !== 'object') {\n return data || {}\n }\n\n const result = { ...data }\n\n for (const [tag, rawValue] of Object.entries(data)) {\n const schema = schemas[tag]\n if (!schema) continue // No schema for this tag - leave as-is\n\n result[tag] = isRichSchema(schema)\n ? applyRichSchemaToValue(rawValue, schema)\n : applySchemaToValue(rawValue, schema)\n }\n\n return result\n}\n\n/**\n * Apply param defaults from runtime schema\n *\n * @param {Object} params - Params from frontmatter\n * @param {Object} defaults - Default values from runtime schema\n * @returns {Object} Merged params with defaults applied\n */\nexport function applyDefaults(params, defaults) {\n if (!defaults || Object.keys(defaults).length === 0) {\n return params || {}\n }\n\n return {\n ...defaults,\n ...(params || {}),\n }\n}\n\n/**\n * ⭐ `content.data` HOLDS THE KEYS THE COMPONENT DECLARES, AND NOTHING ELSE — ruled 2026-09-14\n * [Diego]. Every key in its `meta.js` `data:` and its foundation's `main.js` `data:`\n * (`website.declaredKeys`), each filled, in order, from:\n *\n * 1. what the section holds under that key (`block.heldData`) — a tagged data block, or\n * its own fetch's answer a static build prerendered into it;\n * 2. what the entity store delivered for it — the fetch that fills it (`fillDeclaredKeys`);\n * 3. `null` — nothing fills it, or its fetch is still out (`block.dataLoading`) or failed\n * (`block.dataError[key]`). ⭐ Never `[]`, which is an answer with no records.\n *\n * A tagged data block under a key the component does not declare is left out, and said so\n * in dev (`warnUndeclaredBlocks`); it stays in `content.sequence`. The data object is\n * rebuilt on each render from those sources, never from the last render's.\n *\n * ⛔ Until 2026-09-14 this merged every delivered key into what the section held, so a\n * section received every fetch that reached it and every tagged block it had.\n */\nfunction assembleData(block, meta, entityData) {\n const declared = block.website?.declaredKeys ? block.website.declaredKeys(meta) : []\n const held = block.heldData || {}\n const data = {}\n for (const [key] of declared) {\n if (held[key] !== undefined) data[key] = held[key]\n else if (entityData && entityData[key] !== undefined) data[key] = entityData[key]\n else data[key] = null\n }\n warnUndeclaredBlocks(block, declared)\n block.parsedContent.data = data\n}\n\n/**\n * Say once, in dev, that a tagged block — a data block or a concept block — did not reach its\n * section's component: its key is not declared. The block is still in `content.sequence`, where a component that\n * renders the sequence finds it.\n */\nconst warnedBlocks = new Set()\nfunction warnUndeclaredBlocks(block, declared) {\n if (!block.website?.entityStore?.dev) return\n const keys = new Set(declared.map(([key]) => key))\n for (const item of block.parsedContent?.sequence || []) {\n // A data block (```yaml:tag) and a concept block (```md:tag) both land under their tag, so\n // both need the key declared. ⛔ Until 2026-09-29 only a data block was reported, and an\n // undeclared concept block left content.data without a word.\n const kind = item?.type === 'dataBlock' ? 'data block' : item?.type === 'concept_block' ? 'concept block' : null\n if (!kind || typeof item.tag !== 'string' || keys.has(item.tag)) continue\n const memo = `${block.type}::${item.tag}`\n if (warnedBlocks.has(memo)) continue\n warnedBlocks.add(memo)\n console.warn(\n `[uniweb] ${block.type}: the \\`${item.tag}\\` ${kind} is not in content.data — ${block.type} does not ` +\n `declare \\`${item.tag}\\` in its meta.js \\`data:\\`. It stays in content.sequence.`\n )\n }\n}\n\n/**\n * Run the foundation-level data handler on a block, if one is\n * registered. Runs after entity data merge and before the content\n * handler — the handler sees the fully assembled data and can filter,\n * reshape, or augment it before Loom (or any content transform) runs.\n *\n * The handler receives `(data, block)` where data is\n * `block.parsedContent.data`. It returns a new data object, or\n * null/undefined for no change. The returned data replaces\n * `block.parsedContent.data` for all downstream processing — both\n * the content handler and the component see the transformed data.\n *\n * Skipped when the block is still waiting on async data\n * (`block.dataLoading`), or when no handler is registered.\n * Errors are logged and the original data is preserved.\n */\nfunction runDataHandler(block) {\n if (block.dataLoading) return\n const handler = globalThis.uniweb?.foundationConfig?.handlers?.data\n if (typeof handler !== 'function') return\n\n try {\n const result = handler(block.parsedContent.data, block)\n if (result != null && result !== block.parsedContent.data) {\n block.parsedContent.data = result\n }\n } catch (err) {\n console.error('Foundation data handler failed:', err)\n }\n}\n\n/**\n * Run the foundation-level content handler on a block, if one is\n * registered. Runs at prop-preparation time — after the data handler\n * has had a chance to filter/reshape the data — so the handler sees\n * the fully assembled (and possibly filtered) data. Replaces\n * `block.parsedContent` in place with the re-parsed, instantiated\n * form. The handler receives `(data, block)` and reads raw\n * ProseMirror from `block.rawContent`.\n *\n * Skipped when the block is still waiting on async data\n * (`block.dataLoading`), when no handler is registered, when the\n * block has no raw content, when the handler returns a no-change\n * signal (undefined, null, or the same reference as rawContent), or\n * when the handler throws. Errors are logged via `console.error`.\n */\nfunction runContentHandler(block) {\n if (block.dataLoading) return\n const handler = globalThis.uniweb?.foundationConfig?.handlers?.content\n if (typeof handler !== 'function') return\n if (!block.rawContent || Object.keys(block.rawContent).length === 0) return\n\n try {\n const transformed = handler(block.parsedContent.data, block)\n if (!transformed || transformed === block.rawContent) return\n const reparsed = block.parseContent(transformed)\n reparsed.data = block.parsedContent.data\n block.parsedContent = reparsed\n block.items = reparsed.items || []\n } catch (err) {\n console.error('Foundation content handler failed:', err)\n }\n}\n\n/**\n * Run the foundation-level props handler on the final { content, params }\n * before they reach the component. Runs after content parsing, param\n * defaults, content guarantees, and schema application — the handler\n * sees the exact shape the component would receive and can modify it.\n *\n * The handler receives `(content, params, block)` and returns a new\n * `{ content, params }` object, or null/undefined for no change.\n *\n * Use cases: post-parse content reshaping, computed fields derived\n * from both content and params, param-driven content reorganization.\n * Errors are logged and the original props are preserved.\n */\nfunction runPropsHandler(content, params, block) {\n const handler = globalThis.uniweb?.foundationConfig?.handlers?.props\n if (typeof handler !== 'function') return null\n\n try {\n const result = handler(content, params, block)\n if (result && typeof result === 'object') return result\n } catch (err) {\n console.error('Foundation props handler failed:', err)\n }\n return null\n}\n\n/**\n * Prepare props for a component with runtime guarantees.\n *\n * Does the full content-assembly pipeline in one place so both\n * renderers (`BlockRenderer.jsx` CSR and `ssr-renderer.js` SSG) share\n * the same code path:\n *\n * 1. Assemble `block.parsedContent.data` from the keys the component\n * declares: what the section holds, else what EntityStore delivered,\n * else `null` (`assembleData`).\n * 2. Run the foundation data handler (if registered) to filter or\n * reshape the assembled data.\n * 3. Run the foundation content handler (if registered) on the\n * block. This may replace `block.parsedContent` with a re-parsed,\n * instantiated version.\n * 4. Apply param defaults from meta.\n * 5. Build the guaranteed content structure.\n * 6. Apply schemas to content.data.\n * 7. Run the foundation props handler (if registered) for\n * post-processing of the final { content, params }.\n *\n * Steps 1–3 mutate the block (vanilla JS layer). Steps 4–7 are\n * pure derivations of the block's now-assembled state.\n *\n * @param {Object} block - The block instance\n * @param {Object} meta - Runtime metadata for the component (from meta[componentName])\n * @param {Object|null} [entityData] - Entity data resolved by EntityStore (null if none)\n * @returns {Object} Prepared props: { content, params }\n */\nexport function prepareProps(block, meta, entityData = null) {\n assembleData(block, meta, entityData)\n // ⭐ A list the section held before the store answered — its own fetch, prerendered\n // into its content by a static build — gets its records' `$route` by the store's rule,\n // which the store did not deliver it by (2026-09-14).\n block.website?.entityStore?.linkOwnRecords?.(block)\n runDataHandler(block)\n runContentHandler(block)\n\n // Apply param defaults\n const defaults = meta?.defaults || {}\n const params = applyDefaults(block.properties, defaults)\n\n // Guarantee content structure\n let content = guaranteeContentStructure(block.parsedContent)\n\n // Apply schemas to content.data\n const schemas = meta?.schemas || null\n if (schemas && content.data) {\n content.data = applySchemas(content.data, schemas)\n }\n\n // Post-process hook\n const adjusted = runPropsHandler(content, params, block)\n if (adjusted) {\n return {\n content: adjusted.content || content,\n params: adjusted.params || params,\n }\n }\n\n return { content, params }\n}\n\n/**\n * Get runtime metadata for a component from the global uniweb instance\n *\n * @param {string} componentName\n * @returns {Object|null}\n */\nexport function getComponentMeta(componentName) {\n return globalThis.uniweb?.getComponentMeta?.(componentName) || null\n}\n\n/**\n * Get default param values for a component\n *\n * @param {string} componentName\n * @returns {Object}\n */\nexport function getComponentDefaults(componentName) {\n return globalThis.uniweb?.getComponentDefaults?.(componentName) || {}\n}\n","/**\n * The two helpers the background renderers share — one home, because they are\n * not rendering.\n *\n * ## Why this module exists\n *\n * Backgrounds are an L3 pair by design: `components/Background.jsx` renders\n * with hooks and DOM, `ssr-renderer.js` renders with `React.createElement`\n * only, and the runtime-layer rule (framework `CLAUDE.md` gotcha #2) says that\n * pair is twinned on purpose. ⛔ **What it also says is that only the RENDERING\n * is twinned — \"pure data → L1, one home\" — and two pure helpers had been\n * dragged across the seam with it**: a base-path joiner and a colour-opacity\n * converter, byte-identical in both files.\n *\n * ⚠️ **And the copies had already drifted from the rule they were copying.**\n * The base-path joiner is `@uniweb/core`'s `applyBasePath`, whose own docblock\n * says it moved down to core *\"rather than grow a second copy (the failure\n * `@uniweb/core/route-match` was created to end, after one matcher was\n * implemented twice and the copies diverged)\"*. Two copies grew anyway, in the\n * runtime, and **both were missing its protocol-relative guard** — so an author\n * writing `//cdn.example.com/hero.jpg` on a site with `base: /docs/` got\n * `/docs//cdn.example.com/hero.jpg`. Measured 2026-09-06; the exact garbage\n * `applyBasePath` documents itself as existing to prevent.\n *\n * ⇒ The lesson is narrower than \"don't duplicate\": **when a twinned pair needs a\n * helper, the helper is the thing that must not be twinned.** A twin is a\n * commitment to keep two renderers in step; every pure function pulled inside it\n * silently joins that commitment.\n */\n\nimport { applyBasePath } from '@uniweb/core/base-path'\n\n/**\n * A site-root-relative URL with the deployment base applied.\n *\n * The base comes off the active website rather than being passed in, because\n * both callers render from the singleton and neither has it to hand. The join\n * itself is core's — protocol-relative and absolute URLs pass through, and an\n * already-based path is not based twice.\n *\n * @param {string} url\n * @returns {string}\n */\nexport function siteUrl(url) {\n return applyBasePath(url, globalThis.uniweb?.activeWebsite?.basePath || '')\n}\n\n/**\n * A colour with an alpha applied, for an overlay drawn over a background.\n *\n * Hex and `rgb()` / `rgba()` are converted; anything else — a named colour, a\n * `var(--token)`, `oklch(…)` — is returned unchanged, because a wrong guess\n * here paints the wrong colour rather than failing.\n *\n * @param {string} color\n * @param {number} opacity\n * @returns {string}\n */\nexport function withOpacity(color, opacity) {\n if (typeof color !== 'string' || !color) return color\n if (color.startsWith('#')) {\n const r = parseInt(color.slice(1, 3), 16)\n const g = parseInt(color.slice(3, 5), 16)\n const b = parseInt(color.slice(5, 7), 16)\n return `rgba(${r}, ${g}, ${b}, ${opacity})`\n }\n if (color.startsWith('rgb')) {\n const match = color.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/)\n if (match) return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${opacity})`\n }\n return color\n}\n","/**\n * Default 404 Page Content\n *\n * Single source of truth for the fallback 404 page shown when a site\n * has no custom 404 page defined. Used by:\n * - PageRenderer.jsx (client-side, as React elements)\n * - ssr-renderer.js generate404Html (build-time, as HTML string)\n *\n * The wrapper uses min-height + flex centering so the 404 content\n * renders at the same position regardless of parent layout context.\n * This prevents a visible flash when React hydrates over the SSR content.\n */\n\nimport React from 'react'\n\nconst styles = {\n wrapper: {\n minHeight: '80vh',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n padding: '2rem',\n textAlign: 'center',\n },\n heading: { fontSize: '3rem', fontWeight: 'bold', color: '#1f2937', marginBottom: '1rem' },\n message: { color: '#64748b', marginBottom: '2rem' },\n link: { color: '#3b82f6', textDecoration: 'underline' },\n}\n\n/**\n * React element for client-side rendering (PageRenderer).\n * Reads basePath from the runtime so the homepage link works\n * in subdirectory deployments (e.g., /sites/testproject).\n */\nexport function Default404() {\n const basePath = globalThis.uniweb?.activeWebsite?.basePath || ''\n const homeHref = basePath ? `${basePath}/` : '/'\n return React.createElement('div', { className: 'page-not-found', style: styles.wrapper },\n React.createElement('h1', { style: styles.heading }, '404'),\n React.createElement('p', { style: styles.message }, 'Page not found'),\n React.createElement('a', { href: homeHref, style: styles.link }, 'Go to homepage')\n )\n}\n\n/**\n * Static HTML string for SSR injection (generate404Html).\n *\n * @param {string} [basePath] - Base path prefix for the homepage link (e.g., '/sites/testproject')\n */\nexport function default404Html(basePath = '') {\n const homeHref = basePath ? `${basePath}/` : '/'\n return (\n `<div class=\"page-not-found\" style=\"min-height:80vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;text-align:center\">` +\n `<h1 style=\"font-size:3rem;font-weight:bold;color:#1f2937;margin-bottom:1rem\">404</h1>` +\n `<p style=\"color:#64748b;margin-bottom:2rem\">Page not found</p>` +\n `<a href=\"${homeHref}\" style=\"color:#3b82f6;text-decoration:underline\">Go to homepage</a>` +\n `</div>`\n )\n}\n","/**\n * Layer-2 wiring helpers: runtime ↔ Uniweb singleton.\n *\n * After `createUniweb()` constructs the singleton, the runtime fills a\n * few declared slots on it before the first render — foundation\n * capabilities (`defaultInsets`, `xref.build()`), per-request data\n * hydration into `website.dataStore`, locale-scoped content slicing.\n * This step is identical in every environment (browser SPA, SSG\n * prerender, cloud SSR) because it's plain data manipulation on a JS\n * object: no React rendering happens here, no hooks are called, no DOM\n * is touched, no `react-dom/server` is needed.\n *\n * That's why these helpers live in one file imported by both\n * `setup.js` (browser boot) and `ssr-renderer.js` (SSG/cloud-SSR boot),\n * instead of being duplicated into each. Things that genuinely differ\n * between environments — routing components, icon-cache hydration from\n * the DOM, the per-page render loop — stay in the per-environment\n * entries; these helpers cover only the environment-agnostic L2 work.\n *\n * Keeping this file React-free matters for the SPA bundle: `setup.js`\n * pulls `wire-foundation.js` directly, but it must NOT transitively\n * pull `ssr-renderer.js` (which imports `react-dom/server`). The L2\n * helpers therefore live here, while the L3-composing\n * `initPrerenderForLocale` lives in `ssr-renderer.js`.\n *\n * Adding a new framework-level capability:\n * 1. Read the foundation declaration via `foundation.default.capabilities.<name>`.\n * 2. Apply it to the uniweb singleton (set a slot, call a build hook,\n * register something on `activeWebsite`).\n * 3. Provide a runtime fallback if the capability is one foundations\n * may legitimately not declare (see `FallbackRef`).\n *\n * Foundation export shape contract: the runtime always loads the\n * **built** foundation artifact (`dist/entry.js`) via\n * `loadFoundation()` in `foundation-loader.js`, which does `import(url)`\n * and returns a module namespace. The build pipeline\n * (`@uniweb/build`'s `src/generate-entry.js`) wraps the foundation's\n * source default export under `default.capabilities.*`, so the runtime\n * sees a single canonical shape with no need for fallback chains. This\n * differs from `@uniweb/press` / `@uniweb/unipress`,\n * which DO need to handle a second shape because they're sometimes\n * called from inside a foundation bundle (where the foundation imports\n * its own source as a bare default object).\n */\n\nimport React from 'react'\nimport { deriveCacheKey, resolveDefaultLocale } from '@uniweb/core'\n// Leaf subpaths, not the package root: this file is pulled into the SSR/Worker\n// bundle, and `@uniweb/core` proper drags semantic-parser and theming with it.\nimport { resolveService, readServiceOptions } from '@uniweb/core/services'\nimport Tracker from '@uniweb/core/tracker'\nimport { buildTheme } from '@uniweb/theming'\n\n/**\n * Renders unhandled `[#id]` cross-reference markers as plain text. Used\n * when the active foundation didn't declare its own `<Ref>` via\n * `defaultInsets`. Pure `React.createElement` — safe in every\n * environment, including the hook-free SSR pipeline.\n *\n * Foundations that support cross-references override this by exporting\n * `defaultInsets: { Ref }` (with kit's xref-aware Ref) from their\n * source — the build pipeline carries it through into\n * `default.capabilities.defaultInsets`.\n */\nexport function FallbackRef({ params }) {\n return React.createElement(\n 'span',\n { className: 'xref xref--unhandled' },\n `[${params?.key || '?'}]`,\n )\n}\n\n/**\n * Wire foundation-declared capabilities onto a freshly constructed\n * Uniweb singleton. Called once, after `createUniweb()`, before any\n * rendering. Identical for SPA, SSG, and cloud SSR.\n *\n * @param {import('@uniweb/core').default} uniweb - From createUniweb(...).\n * @param {object} foundation - Loaded foundation module (built shape).\n */\nexport function wireFoundationCapabilities(uniweb, foundation) {\n const caps = foundation?.default?.capabilities || {}\n\n // defaultInsets: framework provides FallbackRef as the floor;\n // foundation overrides win. `getComponent()` on the Uniweb singleton\n // (core/uniweb.js) falls back to defaultInsets[name] when no\n // foundation/extension component matches — that's how `<Ref>` becomes\n // available to every foundation without each one having to register\n // it explicitly.\n uniweb.defaultInsets = { Ref: FallbackRef, ...(caps.defaultInsets || {}) }\n\n // xref: foundations supporting cross-references export\n // `xref.build(website, { foundationKinds })`. The runtime can't\n // import kit directly (kit is bundled into each foundation, not into\n // runtime, so only the foundations that use it pay for it), so it\n // dispatches through the foundation's reference. Foundations without\n // xref skip this entirely; kit's xref module never enters their\n // bundle thanks to tree-shaking at foundation-build time.\n if (caps.xref?.build && uniweb.activeWebsite) {\n caps.xref.build(uniweb.activeWebsite, {\n foundationKinds: caps.xref.kinds || {},\n })\n }\n}\n\n/**\n * Slice a multi-locale site-content payload to one locale.\n *\n * Sites published through the editor ship a single payload that carries\n * all locales nested under `content.locales[locale]` — `pages`, optional\n * `layouts`, and a `config` overlay. The default locale lives at the\n * top level (no nesting). This helper extracts the requested locale's\n * view as a fresh content object the rest of the runtime can consume\n * unchanged.\n *\n * Returns `content` as-is when `locale` is the default, missing, or not\n * present in `content.locales` — callers that already hand us locale-\n * scoped content (e.g., the framework's per-locale SSG path that loads\n * each `dist/{locale}/site-content.json` separately) get pass-through\n * behavior.\n *\n * The shape comes from the editor's publish payload, which is the canonical\n * form for multi-locale content — a server-side renderer consumes it\n * directly. Build-time SSG pre-flattens to one file per locale and so falls\n * into the pass-through case.\n *\n * @param {Object} content - Site content payload, possibly multi-locale.\n * @param {string} locale - Requested locale code.\n * @returns {Object} Content scoped to the requested locale.\n */\nexport function sliceContentForLocale(content, locale) {\n const defaultLang = resolveDefaultLocale(content?.config)\n const locData = content?.locales?.[locale]\n if (!locale || locale === defaultLang || !locData) return content\n return {\n pages: locData.pages,\n layouts: locData.layouts || content.layouts,\n config: {\n ...locData.config,\n i18n: content.config?.i18n,\n activeLocale: locale,\n },\n }\n}\n\n/**\n * Pre-populate a Website's DataStore from build-time / publish-time\n * fetched data so the dispatcher's first probe hits the cache instead\n * of refetching.\n *\n * The value MUST be wrapped as `{ data }`, or `cached.data` reads `undefined`. Three call sites\n * used to inline this loop independently (browser SPA, Node SSG, and a server-side renderer); one\n * of the three used the wrong shape, silently killing prefetched-data reuse wherever it ran. This\n * helper is the one canonical implementation.\n *\n * ⭐ **The KEY is the dispatcher's, not ours** (`fetcher.hydrate`). ⛔ This filed every entry under\n * `deriveCacheKey(entry.config)` until 2026-09-20, which is right for every request the framework's\n * own fetcher answers and wrong for one the site routes to a transport that declares its own\n * `cacheKey` — the documented case being a request whose identity depends on state its address does\n * not carry. Measured: such an entry landed where no `peek` looks, so every prerendered page of that\n * site rendered empty and the browser refetched it, with no error anywhere. One derivation, one\n * home — the dispatcher's, which is where selection happens.\n *\n * @param {import('@uniweb/core').Website} website\n * @param {Array<{config: Object, data: any}>} fetchedData\n */\nexport function hydrateDataStore(website, fetchedData) {\n if (!website?.dataStore || !fetchedData?.length) return\n for (const entry of fetchedData) {\n // A data step's list carries every declared config with an `outcome`; only what was actually\n // fetched enters the store. A list without outcomes (the SSG lane's) is all fetched.\n if (entry.outcome && entry.outcome !== 'fetched') continue\n // `meta` (the depth the records were fetched at) rides along, so the store\n // files them in its record index exactly as a runtime fetch would.\n const value = entry.meta ? { data: entry.data, meta: entry.meta } : { data: entry.data }\n if (typeof website.fetcher?.hydrate === 'function') {\n website.fetcher.hydrate(entry.config, value, { website })\n } else {\n // A graph with no dispatcher — a stub in a test, or a caller assembling one by hand.\n website.dataStore.set(deriveCacheKey(entry.config), value)\n }\n }\n}\n\n/**\n * Make sure the site's theme CSS exists on the graph, generating it from\n * the authored config when nothing upstream did.\n *\n * **The authored theme config is the source of truth in every lane;\n * generated CSS is a cache of it.** `uniweb build` fills that cache and\n * bakes the result into `<head>`, so this is a no-op on the static lane.\n * A lane that serves a site WITHOUT running the framework's build — a\n * backend-hosted SPA, a cloud shell-mode fallback — carries only the\n * authored `theme.yml` (that is the correct thing for a sync wire to\n * carry: `theme.css` is a build artifact, and with two publishers only\n * one of which computes it, shipping it would make a site's styling\n * depend on who published last). Without this helper those lanes render\n * with every semantic token unset — no colours, no backgrounds.\n *\n * Generating here rather than in a publish step is what keeps the\n * three-ingredient contract true: site + foundation + runtime converge\n * to a *styled* page with no fourth actor. It also stays one\n * implementation — the alternative was re-deriving the OKLCH shade math\n * in another language and keeping the two bit-compatible.\n *\n * L2, not L3: this reads and writes graph state and renders nothing, so\n * it has a single home here and both boot paths call it. **The\n * `@uniweb/theming` import is deliberately static.** An SSR isolate\n * loads a fixed modules map and cannot resolve a chunk graph, so the SSR\n * entry must include the generator statically; a lazy `import()` in the\n * browser entry only would mean two mechanisms for one behaviour,\n * drifting independently. Measured cost of the generator: ~4.9 KB gzip.\n *\n * Foundation-declared vars reach us through\n * `capabilities.vars` — emitted into `dist/entry.js` by\n * `@uniweb/build`'s `generate-entry.js`. Before that existed they lived\n * only in `dist/meta/schema.json` and a theme generated outside the\n * build silently lost every one of them.\n *\n * Callers own the \"should I?\" question, because it is environment-\n * specific: the browser entry skips this when the document already\n * carries a prerendered `<style id=\"uniweb-theme\">` (regenerating from\n * an already-processed config is wasted work at best), while the SSR\n * entry always runs it and lets `injectPageContent()` emit the result\n * idempotently.\n *\n * @param {import('@uniweb/core').default} uniweb - From createUniweb(...).\n * @param {object} foundation - Loaded foundation module (built shape).\n */\nexport function ensureThemeCss(uniweb, foundation) {\n const website = uniweb?.activeWebsite\n const themeData = website?.themeData\n if (!themeData || themeData.css) return\n\n const caps = foundation?.default?.capabilities || {}\n try {\n const { config, css, links } = buildTheme(themeData, {\n foundationVars: caps.vars || {},\n base: website.basePath || '/',\n })\n // Merge rather than replace: `config` is the processed superset (it\n // adds `palettes`, normalized `contexts`, resolved `fonts`), so this\n // also gives a build-less lane the same themeData shape the static\n // lane has — Theme.getPalette() and friends start working too.\n Object.assign(themeData, config, { css, links })\n } catch (err) {\n // This runs on the path taken when something upstream has already\n // gone wrong. A degraded render that is still legibly the site beats\n // one that looks broken, but neither is worth a boot crash.\n console.warn('[uniweb] theme CSS generation failed:', err?.message || err)\n }\n}\n\n/**\n * L2: give the site's tracker its destination.\n *\n * Replaces the disabled `Tracker` that `createUniweb` declares (see\n * `core/src/uniweb.js`) with a configured one, when — and only when — a\n * destination resolves. With none, the disabled default stays and every\n * `track()` call in the site remains a silent no-op, which is the default\n * state for the large majority of sites.\n *\n * ⛔ **WHY THE BASE PATH IS PASSED IN RATHER THAN READ OFF THE WEBSITE.**\n * `resolveService` joins a root-relative endpoint to `website.basePath`, and\n * that field is still `''` until `setBasePath()` runs — which happens later,\n * from `RuntimeProvider`. Resolving against the website as-is would silently\n * drop the prefix on every subdirectory deployment, and the symptom would be a\n * collector quietly receiving nothing. So the caller supplies the basename it\n * has already derived, and the lookup is done against that. `resolveService`\n * reads only `.config` and `.basePath`, so a plain object is a complete input.\n *\n * ⚖️ **Not called from the SSR path, deliberately.** The tracker is\n * browser-guarded, so wiring it there would produce a configured object that\n * can never emit — a slot that looks live and is not. The SSR twin has no\n * page-view effect either; suppression is structural rather than a flag.\n *\n * ## `scripts` — a vendor's own script, when the site declares one\n *\n * A second, independent path — vendor tags:\n * nothing is translated between our stream and theirs, and the framework never\n * learns which vendor it is. ⛔ **The loader is INJECTED rather than imported**,\n * because this file is pulled into the SSR/Worker bundle and a script loader is\n * DOM code. The browser entry passes one; the SSR path passes none, so there is\n * no branch to remember.\n *\n * @param {object} uniweb - the singleton\n * @param {object} [options]\n * @param {string} [options.basePath] - the deployment base (router basename)\n * @param {(urls: string[], opts: object) => void} [options.loadScripts] - DOM\n * loader for declared vendor scripts; omitted outside a browser entry\n */\n/**\n * What `tracking.emit` names, when a site names a preset rather than a list.\n *\n * ⭐ **`all` is deliberately ABSENT from this table.** It resolves to `null` —\n * *no narrowing* — so an event added in a later release is included without the\n * site republishing. A literal list would freeze `all` at the moment the site\n * was built and quietly stop meaning \"all\".\n *\n * ⚖️ **`standard` and `all` select the same events today, and that is not a\n * reason to drop one.** They diverge the moment a new automatic event ships:\n * `standard` is a curated set that a release cannot grow behind an operator's\n * back, `all` is the standing yes. The volume surprise is the thing being\n * avoided — a site that never changed should not start sending more.\n *\n * ⛔ **The curated set is the answer for a site that CONFIGURED ITS OWN\n * DESTINATION. It is NOT the answer for a site whose host supplies one** — see\n * `resolveEmit`, which is where absence stopped meaning one thing.\n */\nconst EMIT_PRESETS = {\n minimal: ['page_view'],\n standard: ['page_view', 'outbound_click', 'section_view']\n}\n\n/** The preset a site gets by declaring a destination and nothing else. */\nconst DEFAULT_EMIT = 'standard'\n\n/**\n * The site's own selection, as a list of event names or `null` for no narrowing.\n *\n * ⛔ **An unknown preset name resolves to the DEFAULT, not to nothing.** A typo\n * (`emit: sandard`) must not silently take a site dark: the failure mode of a\n * misread selection has to be \"you got the usual set\", never \"you got none and\n * nothing said so\".\n *\n * ## ⭐ ABSENCE MEANS TWO DIFFERENT THINGS, and this is where they part\n *\n * **A site that configured its own `endpoint` chose it.** Writing no `emit`\n * there means *\"the curated default\"*, and `standard` is exactly right — a\n * later framework release must not grow it behind that operator's back.\n *\n * **A site whose HOST supplies the collector has no endpoint of its own.** The\n * operator's whole relationship is *\"my host does analytics for me\"*, so\n * writing no `emit` there means **\"whatever my host offers\"** — not a list\n * frozen at the framework version the site was built against.\n *\n * ⇒ **Absent `emit` defers to the host's declared list when there is one, and\n * falls back to `standard` when there is not.** Returning `null` is how the\n * deferral is expressed: it is *no site-tier narrowing*, so `Tracker.arms()` is\n * left with the host's list as the only gate.\n *\n * ⭐ **Why this is a fix and not a relaxation.** §4 of the tracking design says\n * *\"the runtime emits what the SITE OWNER buys\"* — and before this, an owner\n * paying a host for analytics received a **framework-frozen subset** of what\n * that host stores and bills them for. The only way to close the gap was to\n * hand-edit YAML and republish, **a dependency with no symptom when forgotten**,\n * which is the precise failure that rule was written to reject.\n *\n * ⛔ **The fallback is NOT decoration — it is the standalone-first guarantee.**\n * A static host, a foreign backend, and any Uniweb backend predating the\n * `events` key all declare no list. Deferring unconditionally would arm *every*\n * event, forever, on exactly the sites the framework exists to serve without a\n * backend.\n *\n * ⚠️ **A host that declares an EMPTY list still means it** — `[]` is a\n * statement, not an absence, and it arms nothing. That is unchanged: `arms()`\n * has always read an empty host list that way. Only `undefined` means \"nothing\n * declared\".\n *\n * @param {string|string[]|undefined} emit - the site's own `tracking.emit`\n * @param {string[]|null} [hostEvents] - the host's declared list, or `null`\n * when the host declared none. **Only consulted when `emit` is absent**;\n * an author who names anything still wins.\n * @returns {string[]|null}\n */\nfunction resolveEmit(emit, hostEvents = null) {\n // ⛔ Absent is the ONLY branch that consults the host — this is a default,\n // never an override. `emit: minimal` on a host offering everything still\n // sends one event.\n if (emit == null) return hostEvents ? null : EMIT_PRESETS[DEFAULT_EMIT]\n if (Array.isArray(emit)) return emit\n if (emit === 'all') return null\n return EMIT_PRESETS[emit] || EMIT_PRESETS[DEFAULT_EMIT]\n}\n\nexport function wireTracker(uniweb, { basePath = '', loadScripts = null } = {}) {\n const website = uniweb?.activeWebsite\n if (!website) return\n\n // A plain lookup target: `resolveService` reads `.config` and `.basePath`\n // only, so this is the whole of what it needs and carries the *correct* base.\n const target = { config: website.config, basePath }\n\n const { url } = resolveService(target, 'tracking')\n const options = readServiceOptions(target, 'tracking')\n\n // Only whether any were declared — normalizing them is the loader's job, and\n // lives behind the loader's dynamic boundary so a site with none never\n // downloads that code either.\n const declaredScripts = options.scripts\n const hasScripts = Array.isArray(declaredScripts) ? declaredScripts.length > 0 : !!declaredScripts\n\n // Nothing declared on either count — keep the disabled default, nothing\n // armed, nothing queued. This is the state of the large majority of sites.\n if (!url && !hasScripts) return\n\n // The two narrowings, resolved here rather than in core: this is per-request\n // config reshaping, which is L2's job (see this file's header).\n //\n // ⛔ **`hostEvents` is read from the HOST tier only** — `config.services\n // .tracking.events`, never the merged view. A site cannot widen what a host\n // declined to store, and reading the merge would let it, silently, by writing\n // its own `events:` key.\n //\n // ⛔ **Absent stays absent.** No `events` from the host means NO NARROWING,\n // never an empty set: a host that sends no list is an older or simpler one,\n // and the other reading takes every site on it dark with every gate saying\n // yes. `?? null` rather than `?? []` is the whole of that guard.\n // ⛔ Each tier is read from ITS OWN key, not from the merged `options`. The\n // merge exists so a site's own option can override a host's — `consent`, say;\n // the address is not an option, and comes from `resolveService` above. These\n // two are not overrides of each other but answers to different questions, and\n // reading either off the merge would let one tier answer the other's — a site\n // writing `events:` would widen past what the host stores, silently.\n const hostTracking = website.config?.services?.tracking\n const siteTracking = website.config?.tracking\n const hostEvents =\n hostTracking && Array.isArray(hostTracking.events) ? hostTracking.events : null\n\n const tracker = new Tracker({\n endpoint: url,\n hostEvents,\n siteEmit: resolveEmit(siteTracking && siteTracking.emit, hostEvents),\n // ⭐ Read off the MERGED view, unlike the two above — and the difference is\n // the point. `events`/`emit` answer different questions per tier, so each is\n // read from its own key; this is one question with two possible answerers,\n // so the ordinary precedence applies: the host declares a batch window that\n // suits its collector, and a site's own `tracking:` overrides it. Absent on\n // both, `Tracker` keeps its default.\n //\n // ⛔ **A field being READABLE is not the same as it being AVAILABLE**, and\n // that is what made this line worth a test rather than a shrug.\n // `readServiceOptions` has always returned this key, so the plan read as\n // finished while nothing wrote the object being read — it would have shipped\n // as *\"we set the interval and it did nothing\"*, with all three lanes' suites\n // green. The value now has to reach `setInterval`, and a test asserts the\n // delay rather than the field.\n flushIntervalMs: options.flushIntervalMs,\n // Opt-in, not the default. Declaring a destination is itself the operator's\n // decision to track; requiring a second affirmative step would be the\n // framework presuming a jurisdiction on their behalf, which is exactly what\n // it must not do. A site that needs the gate asks for it.\n consentRequired: options.consent === 'required',\n debug: !!options.debug\n })\n uniweb.tracking = tracker\n\n if (!loadScripts || !hasScripts) return\n\n // The same suppression the tracker applies to its own events: a server render\n // or a framed authoring preview is not a visit, and a vendor's script must not\n // fire there either. One predicate in core, so the two cannot drift.\n if (!tracker.isLiveDocument()) return\n\n const load = () => loadScripts(declaredScripts, { basePath, debug: !!options.debug })\n if (tracker.consentStatus() === 'granted') load()\n else tracker.onGranted = load\n}\n","/**\n * What the runtime puts on a layout-area wrapper.\n *\n * When a foundation enables view transitions (the default), the runtime gives\n * each layout region a `view-transition-name` so the browser animates them\n * independently — persistent chrome (header, sidebar, footer) morphs in place\n * while the body crossfades. Without per-region names the browser falls back to\n * a single full-page crossfade, which makes the whole layout (chrome included)\n * flicker on every navigation.\n *\n * Naming is only half of it. A `view-transition-name` **makes its element a\n * stacking context**, so the moment the runtime adds these wrappers it has\n * decided how the areas paint relative to one another — and with no `z-index`\n * on them they all sit at `auto` and paint in DOM order, which puts the body\n * over the header on any layout that renders the header first.\n *\n * That is not theoretical. It is the same mechanism `@uniweb/kit`'s `Overlay`\n * exists for (a modal opened from the header, trapped inside `uw-header`), and\n * it made a real docs page's fixed header unclickable while the identical\n * header on the marketing layout was fine — because that layout's markup\n * happened to wrap its header area in `relative z-40`. A framework that\n * creates stacking contexts owes its users an order; leaving it to DOM order\n * means \"does my header work\" is answered by an accident of someone's JSX.\n *\n * So this module resolves BOTH halves of the wrapper — the transition name and\n * the stacking layer — and hands back the finished style. It is pure (no\n * React/DOM) so the SPA renderer (`components/Layout.jsx`) and the SSR renderer\n * (`ssr-renderer.js`) produce identical wrappers, keeping prerendered HTML and\n * the hydrated SPA aligned.\n */\n\n// Namespace so generated names can't collide with `view-transition-name`s a\n// foundation sets inside its own component CSS. The prefix also guarantees a\n// valid CSS <custom-ident> (starts with a letter).\nconst NS = 'uw-'\n\nconst toIdent = (name) => NS + String(name).replace(/[^a-zA-Z0-9_-]/g, '-')\n\n/**\n * Build the effective view-transition-name map for a layout.\n *\n * Default: every rendered area plus the implicit `body` gets a stable,\n * namespaced name (`uw-<area>`, `uw-body`). Same-named areas across layouts\n * therefore share a name and morph between layouts automatically.\n *\n * The layout's `meta.js` `transitions` value overrides this:\n * - an object overrides per region (`{ left: 'sidebar' }` to group across\n * layouts, or `{ left: null }` to opt one region out);\n * - `false` opts the whole layout out (back to the full-page crossfade).\n *\n * @param {string[]} areaNames - Names of the areas rendered for this page (excludes `body`).\n * @param {Object|false|null|undefined} explicit - `layoutMeta.transitions`.\n * @returns {Object|null} region → view-transition-name; `null` when opted out.\n * A region whose value is null/empty in the returned map gets no name.\n */\nexport function resolveLayoutTransitions(areaNames, explicit) {\n if (explicit === false) return null\n\n const transitions = { body: toIdent('body') }\n for (const name of areaNames) transitions[name] = toIdent(name)\n\n return explicit ? { ...transitions, ...explicit } : transitions\n}\n\n/**\n * The stacking layer of each area's wrapper.\n *\n * Default: every area except the body gets `1`, and the body gets nothing —\n * content is the backdrop, chrome is above it. That is the whole of what the\n * framework claims to know, and it is deliberately not more.\n *\n * The body is left unstacked rather than pinned to `0` on purpose. A layer\n * brings `position: relative` with it (see `areaWrapperStyle`), and a\n * positioned body wrapper would become the containing block for every\n * absolutely-positioned descendant on the page — a real behaviour change across\n * every site, to buy an ordering that lifting the chrome already achieves. An\n * unlayered body stays a plain stacking context and paints below anything with\n * a positive z-index, which is exactly the intent.\n *\n * In particular there is no default ordering BETWEEN chrome areas. Area names\n * are free-form (`header`, `footer`, `left` and `right` are conventions the\n * docs promote, but a foundation may define `topbar`, `rail`, `statusbar`,\n * anything), so ranking `header` above `left` would be the framework reading\n * meaning into a string it does not own — and would then behave differently for\n * a layout that spelled the same idea another way. Where two pieces of chrome\n * genuinely overlap, which one wins is a design decision, and the layout says\n * so with `layers`.\n *\n * The shape mirrors `transitions` exactly, so there is one thing to learn:\n * - an object overrides per region (`{ footer: 0 }`, `{ header: 5 }`), and a\n * region may be set to `null` to leave it unstacked;\n * - `false` opts the whole layout out, and the runtime then emits no\n * stacking at all — for a foundation that would rather own it in its own\n * markup, which is exactly what the marketing layout above was doing.\n *\n * Layers do NOT depend on view transitions. \"Chrome paints above content\" is a\n * property of the layout, not of how it animates — and body sections routinely\n * form their own stacking contexts (a section with a background isolates so its\n * background layer stays contained), so a fixed header in an unstacked sibling\n * area is not guaranteed to win against them either way. Tying the two together\n * was what left `DefaultLayout` hand-rolling its own `z-index: 40` on the\n * header: a second mechanism for the same job, which then swallowed `layers`\n * whole — a foundation could set `layers: { header: 0 }` on the default layout\n * and measurably nothing happened.\n *\n * @param {string[]} areaNames - Names of the areas rendered for this page (excludes `body`).\n * @param {Object|false|null|undefined} explicit - `layoutMeta.layers`.\n * @returns {Object} region → z-index. Empty when the layout opts out.\n */\nexport function resolveLayoutLayers(areaNames, explicit) {\n if (explicit === false) return {}\n\n const defaults = {}\n for (const name of areaNames) defaults[name] = 1\n\n return explicit ? { ...defaults, ...explicit } : defaults\n}\n\n/**\n * The finished inline style for one area's wrapper, or `null` when the region\n * needs no wrapper at all.\n *\n * Returning the whole style from one place is the point: the SPA and SSR\n * renderers each build these wrappers, and a rule applied in one and forgotten\n * in the other is invisible until a prerendered page and its hydrated self\n * disagree about what paints on top.\n *\n * `position: relative` rides along with a layer because `z-index` does nothing\n * on a static element. It is set only on regions that carry a layer, which is\n * why the default leaves the body at `0` rather than lifting everything: a\n * positioned body wrapper would become the containing block for every\n * absolutely-positioned descendant on the page, and the ordering does not need\n * it.\n *\n * @param {string} region - Area name, or `body`.\n * @param {Object|null} transitions - region → view-transition-name.\n * @param {Object} layers - region → z-index.\n * @returns {Object|null} Inline style object, or null for no wrapper.\n */\nexport function areaWrapperStyle(region, transitions, layers) {\n const style = {}\n\n const name = transitions?.[region]\n if (name) style.viewTransitionName = name\n\n const layer = layers?.[region]\n if (layer != null) {\n style.position = 'relative'\n style.zIndex = layer\n }\n\n return Object.keys(style).length > 0 ? style : null\n}\n","/**\n * Appearance — site-wide color scheme (light/dark).\n *\n * ONE resolver, reached two ways:\n *\n * 1. SPA boot — `initAppearance()` runs inside initRuntime, after initUniweb()\n * (so website.themeData.appearance is readable) and before\n * createRoot().render(). That position precedes React's first paint, so no\n * section renders with the wrong tokens and then flips, and it covers every\n * delivery mode because all three start() branches funnel into initRuntime.\n *\n * 2. Prerendered HTML — `renderAppearanceBootScript()` serializes the SAME\n * function into a synchronous <head> script. HTML that ships real body\n * content is styled from :root (light) tokens until a bundle loads, so\n * without this a dark visitor sees a flash of light. The script is emitted\n * by injectPageContent() in ssr-renderer.js, which every prerender lane\n * goes through — the framework's SSG and the cloud worker's JIT render\n * alike. Emitting it from a lane-specific injector is how the cloud lane\n * silently missed it once already.\n *\n * Why serialize instead of hand-writing the inline script: the two paths must\n * agree exactly. `applyBootScheme` is therefore written to be SELF-CONTAINED —\n * it references no module-scope binding, only its two arguments and the browser\n * globals it needs — so `Function.prototype.toString()` yields a script that\n * behaves identically to calling it directly. Keep it that way: an import, a\n * module const, or a helper call would survive `toString()` as an undefined\n * identifier at first paint. appearance.test.js pins the equivalence.\n *\n * Environment-neutral by construction. `applyBootScheme` no-ops its DOM writes\n * outside a browser, and `renderAppearanceBootScript` only stringifies — so\n * ssr-renderer.js can import this module in Node and in a server-side isolate.\n *\n * Two writers with independent resolution is the bug this replaced:\n * WebsiteRenderer used to re-apply `appearance.default` from an effect, and\n * because React runs child effects before parent effects it clobbered the\n * visitor's stored preference on every page load — the page came back light\n * while the toggle button still believed it was dark, making the next click a\n * no-op.\n */\n\nimport { hasDarkScheme } from '@uniweb/core'\n\nexport const APPEARANCE_STORAGE_KEY = 'uniweb-appearance'\nexport const DARK_SCHEME_CLASS = 'scheme-dark'\nexport const LIGHT_SCHEME_CLASS = 'scheme-light'\n\n/**\n * Resolve and apply the visitor's color scheme.\n *\n * Precedence: stored visitor preference → OS preference (when the site opts in)\n * → the theme's declared default.\n *\n * SELF-CONTAINED ON PURPOSE — see the module header. This function is both\n * called directly (SPA boot) and serialized with toString() into the pre-paint\n * <script> of prerendered HTML. It must never reference anything outside its own\n * arguments and the browser globals below; the storage key and class names are\n * inlined as literals rather than read from the exported constants for exactly\n * that reason.\n *\n * Written in ES5 so it needs no transpilation in the inline-script form, and\n * every browser access is guarded: Safari private mode throws on localStorage,\n * old webviews lack matchMedia, and Node has no document.\n *\n * @param {boolean} respectSystem - follow prefers-color-scheme when unset\n * @param {'light'|'dark'} fallback - the theme's declared default\n * @returns {'light'|'dark'} the scheme applied\n */\nexport function applyBootScheme(respectSystem, fallback) {\n var stored = null\n try {\n stored = localStorage.getItem('uniweb-appearance')\n } catch (e) {\n // Safari private mode and some embedded webviews throw on access\n }\n\n var hasStored = stored === 'light' || stored === 'dark'\n var scheme = hasStored ? stored : fallback\n\n if (!hasStored && respectSystem) {\n try {\n if (window.matchMedia('(prefers-color-scheme: dark)').matches) scheme = 'dark'\n } catch (e) {\n // No matchMedia — keep the declared default\n }\n }\n\n try {\n var root = document.documentElement\n // Always set an explicit class rather than relying on the absence of one.\n // `default: system` themes emit a `@media (prefers-color-scheme: dark)`\n // block scoped to `:root:not(.scheme-light)`, so forcing light on a dark OS\n // requires `scheme-light` to be present — removing `scheme-dark` alone would\n // leave the media query still applying dark tokens.\n if (scheme === 'dark') {\n root.classList.add('scheme-dark')\n root.classList.remove('scheme-light')\n } else {\n root.classList.add('scheme-light')\n root.classList.remove('scheme-dark')\n }\n } catch (e) {\n // No DOM (Node / prerender) — the resolved scheme is still returned\n }\n\n return scheme\n}\n\n/**\n * Reduce a theme's `appearance:` block to the two arguments applyBootScheme\n * takes, or null when the site can never show dark.\n *\n * THE ONLY PLACE `appearance.*` FIELDS ARE READ. Both the SPA boot and the\n * inline-script emitter go through here, so the two cannot disagree about what\n * `respectSystemPreference` defaults to. They used to: the runtime treated an\n * unset value as false while the script emitter and @uniweb/core's\n * Theme.getAppearance() treated it as true. Those agreed only by the grace of\n * @uniweb/theming's normalizeAppearance() always injecting the key — any path\n * handing raw theme.yml appearance to the runtime would have produced a\n * pre-paint script and a boot resolver that disagree, i.e. the exact\n * flash-then-flip this whole module exists to prevent. Unset means true, which\n * is what the docs promise and what core already did.\n *\n * The null gate is @uniweb/core's hasDarkScheme() — the same predicate\n * @uniweb/theming uses to decide whether `.scheme-dark` CSS is generated at all.\n * Sharing it means we can never apply a scheme that has no matching rules, and\n * a light-only site correctly gets no class and no inline script.\n *\n * @param {Object} [appearance] - the resolved theme.yml `appearance:` block\n * @returns {{respectSystem: boolean, fallback: 'light'|'dark'}|null}\n */\nexport function resolveAppearanceBoot(appearance) {\n if (!appearance || !hasDarkScheme(appearance)) return null\n\n return {\n respectSystem: appearance.respectSystemPreference !== false,\n fallback: appearance.default === 'dark' ? 'dark' : 'light',\n }\n}\n\n/**\n * Resolve and apply the boot scheme in the browser. Called by initRuntime.\n *\n * @param {Object} [appearance] - the resolved theme.yml `appearance:` block\n * @returns {'light'|'dark'|null} the applied scheme, or null when the site has\n * no dark scheme to switch to (nothing is written to the document)\n */\nexport function initAppearance(appearance) {\n const opts = resolveAppearanceBoot(appearance)\n if (!opts) return null\n\n return applyBootScheme(opts.respectSystem, opts.fallback)\n}\n\n/**\n * Emit the pre-paint <script> for prerendered HTML.\n *\n * Returns '' when the site has no dark scheme — a light-only page always renders\n * light, so there is nothing to correct before paint and no reason to ship the\n * bytes. Pure SPA builds don't need it either: the body is empty until the\n * bundle renders and initAppearance() runs before that first render.\n *\n * Only a boolean and a JSON-quoted 'light'/'dark' are interpolated, both derived\n * from resolveAppearanceBoot rather than taken from the theme verbatim, so\n * author-supplied theme.yml values cannot inject script.\n *\n * @param {Object} [appearance] - the resolved theme.yml `appearance:` block\n * @returns {string} a `<script>` tag, or '' when no script is needed\n */\nexport function renderAppearanceBootScript(appearance) {\n const opts = resolveAppearanceBoot(appearance)\n if (!opts) return ''\n\n const call = `(${applyBootScheme.toString()})(${opts.respectSystem}, ${JSON.stringify(opts.fallback)})`\n\n return `<script id=\"uniweb-appearance\">${call}</script>`\n}\n","/**\n * The DOCUMENT TITLE — what a browser tab, a bookmark and a search result show for a page.\n *\n * ONE rule, reached two ways (the shape `appearance.js` has for the color scheme):\n *\n * 1. SPA — `useHeadMeta` sets `document.title` on every navigation.\n * 2. Prerendered HTML — `injectPageContent()` in ssr-renderer.js writes `<title>`, for\n * every prerender lane: the framework's SSG and a host's render alike.\n *\n * ⛔ Until 2026-09-14 each side carried its own rule. The SPA wrote `<page> | <site>` and\n * the prerender wrote `<page>` alone, so every prerendered page changed its title the\n * moment the bundle loaded (`Home` → `Home | Acme`), and a crawler that runs no script\n * indexed the shorter one.\n *\n * `<page title> | <site name>`; the page title alone when the site has no name; the empty\n * string when the page has no title — a caller then leaves the title it already has.\n *\n * @param {string|number|null|undefined} pageTitle - the page's display title (`page.getTitle()`)\n * @param {string|null|undefined} siteName - the site's name (`website.name`)\n * @returns {string}\n */\nexport function documentTitle(pageTitle, siteName) {\n if (!pageTitle) return ''\n const title = String(pageTitle)\n return siteName ? `${title} | ${siteName}` : title\n}\n","/**\n * WHICH PAGE A ROUTE NAMES — the one lookup, importable without a renderer.\n *\n * ⭐ **It is one line, and it is a module because of who imports it.** `ssr-renderer.js` imports\n * `react-dom/server` at module scope, so anything reaching for this lookup through that file drags\n * the React server renderer into its own graph. The render's data step (`load-page-data.js`)\n * fetches and renders nothing, and did exactly that until 2026-09-20 — for `website.getPage`.\n *\n * ⚠️ **The docstring below already said \"no React … safe in a Worker isolate\", and was right.** It\n * was the FILE that made it false, which is the shape to watch for: a claim about a function's\n * dependencies is a claim about its module's imports, and nothing checks the two agree.\n * `tests/data-step-graph.test.js` checks it for the one caller that needs it.\n *\n * @module\n */\n\n/**\n * Resolve a route to the Page that should render it.\n *\n * Exists because `ssr-renderer.js` exported `renderPage(page, …)` and no supported way\n * to *get* a page — so every host rendering server-side wrote its own lookup,\n * and the obvious one (`website.pages.find(p => p.route === route)`) cannot\n * match a dynamic route, because the payload holds `/blog/:id` and the request\n * carries `/blog/1`. One host wrote that lookup three times in three files\n * before the gap was noticed. A renderer that takes a Page owes callers a Page.\n *\n * ⛔ Move it, never copy it: a second lookup here would be the fourth.\n *\n * This is `Website#getPage` — the same seven-step resolution the browser runs,\n * literally the same function, so a server-rendered page and the one hydrating\n * over it cannot disagree. Pure `@uniweb/core`: no React, no DOM, no DataStore\n * required, safe in a Worker isolate.\n *\n * @param {Website} website\n * @param {string} route - The requested path, e.g. `/blog/1`\n * @returns {Page|undefined} The page, or undefined when nothing matches — which\n * is a genuine 404 and the caller's to turn into one.\n */\nexport function resolvePage(website, route) {\n return website.getPage(route)\n}\n","/**\n * SSR Renderer\n *\n * Hook-free rendering pipeline for SSG (build) and server-side SSR.\n * Mirrors BlockRenderer.jsx + Background.jsx using React.createElement\n * directly — no hooks, no JSX, no browser APIs.\n *\n * This is the single source of truth for how blocks render during prerender.\n * When modifying BlockRenderer.jsx or Background.jsx, update this file to match.\n *\n * Exports three layers:\n * 1. Rendering functions (renderBlock, renderBlocks, renderLayout, renderBackground)\n * 2. Initialization (initPrerender, prefetchIcons)\n * 3. Per-page rendering (renderPage, classifyRenderError, injectPageContent, escapeHtml)\n */\n\nimport React from 'react'\nimport { renderToString } from 'react-dom/server'\nimport { createUniweb, resolveDefaultLocale } from '@uniweb/core'\nimport { sectionDomId } from '@uniweb/core/section-id'\nimport { siteUrl, withOpacity } from './background-shared.js'\nimport { routePatternToRegex, isDynamicRoute } from '@uniweb/core/route-match'\nimport { DEFAULT_ICON_BASE, iconUrl } from '@uniweb/core/icon-corpus'\nimport { buildSectionOverrides, FONT_LINKS_MARKER } from '@uniweb/theming'\nimport { prepareProps, getComponentMeta } from './prepare-props.js'\nimport { default404Html } from './default-404.js'\nimport {\n wireFoundationCapabilities,\n sliceContentForLocale,\n hydrateDataStore,\n ensureThemeCss,\n} from './wire-foundation.js'\nimport { resolveLayoutTransitions, resolveLayoutLayers, areaWrapperStyle } from './area-wrappers.js'\nimport { renderAppearanceBootScript } from './appearance.js'\nimport { documentTitle } from './document-title.js'\n\n// Re-export L2 helpers so the public `@uniweb/runtime/ssr` surface\n// carries everything an SSR consumer needs from one entry point.\nexport { sliceContentForLocale, hydrateDataStore }\n\n// ============================================================================\n// Layer 1: Rendering functions\n// ============================================================================\n\n/**\n * Valid color contexts for section theming\n */\nconst VALID_CONTEXTS = ['light', 'medium', 'dark']\n\n/**\n * Build wrapper props from block configuration.\n * Mirrors getWrapperProps in BlockRenderer.jsx.\n */\nexport function getWrapperProps(block) {\n const theme = block.themeName\n const blockClassName = block.state?.className || ''\n\n // Empty themeName = Auto → no context class → inherits tokens from :root\n // Non-empty = Pinned → context class sets tokens directly on the element\n let contextClass = ''\n if (theme && VALID_CONTEXTS.includes(theme)) {\n contextClass = `context-${theme}`\n }\n\n let className = contextClass\n if (blockClassName) {\n className = className ? `${className} ${blockClassName}` : blockClassName\n }\n\n // Normalized once by core (`block.background`) — the same value the SPA renderer reads.\n const background = block.background || {}\n const style = {}\n\n // If background has content, ensure relative positioning and a stacking context\n // so the background's z-index stays contained within this section.\n if (background.mode) {\n style.position = 'relative'\n style.isolation = 'isolate'\n }\n\n // The section's tokens in effect whatever the scheme, inline — as in the SPA renderer\n // (`BlockRenderer.jsx`); the rest of its theme is the page stylesheet's.\n if (block.contextOverrides) {\n for (const [key, value] of Object.entries(block.contextOverrides)) {\n style[`--${key}`] = value\n }\n }\n\n // Same rule as the SPA renderer and the search extractor — @uniweb/core/section-id.\n return { id: sectionDomId(block), style, className, background }\n}\n\n\n/**\n * Render a background element for SSR.\n * Mirrors Background.jsx (color, gradient, image — not video).\n * Video backgrounds require JS for autoplay and are skipped during SSR.\n */\nexport function renderBackground(background) {\n if (!background?.mode) return null\n\n const containerStyle = {\n position: 'absolute',\n inset: '0',\n overflow: 'hidden',\n zIndex: 0,\n }\n\n const children = []\n\n // Color background\n if (background.mode === 'color' && background.color) {\n children.push(\n React.createElement('div', {\n key: 'bg-color',\n className: 'background-color',\n style: { position: 'absolute', inset: '0', backgroundColor: background.color },\n 'aria-hidden': 'true',\n })\n )\n }\n\n // Gradient background (supports string or object with opacity)\n if (background.mode === 'gradient' && background.gradient) {\n const g = background.gradient\n\n let bgValue\n if (typeof g === 'string') {\n bgValue = g\n } else {\n const {\n start = 'transparent',\n end = 'transparent',\n angle = 0,\n startPosition = 0,\n endPosition = 100,\n startOpacity = 1,\n endOpacity = 1,\n } = g\n const startColor = startOpacity < 1 ? withOpacity(start, startOpacity) : start\n const endColor = endOpacity < 1 ? withOpacity(end, endOpacity) : end\n bgValue = `linear-gradient(${angle}deg, ${startColor} ${startPosition}%, ${endColor} ${endPosition}%)`\n }\n\n children.push(\n React.createElement('div', {\n key: 'bg-gradient',\n className: 'background-gradient',\n style: { position: 'absolute', inset: '0', background: bgValue },\n 'aria-hidden': 'true',\n })\n )\n }\n\n // Image background\n if (background.mode === 'image' && background.image?.src) {\n const img = background.image\n children.push(\n React.createElement('div', {\n key: 'bg-image',\n className: 'background-image',\n style: {\n position: 'absolute',\n inset: '0',\n backgroundImage: `url(${siteUrl(img.src)})`,\n backgroundPosition: img.position || 'center',\n backgroundSize: img.size || 'cover',\n backgroundRepeat: 'no-repeat',\n },\n 'aria-hidden': 'true',\n })\n )\n }\n\n // Overlay (gradient or solid)\n if (background.overlay?.enabled) {\n const ov = background.overlay\n let overlayStyle\n\n if (ov.gradient) {\n const g = ov.gradient\n overlayStyle = {\n position: 'absolute', inset: '0', pointerEvents: 'none',\n background: `linear-gradient(${g.angle || 180}deg, ${g.start || 'rgba(0,0,0,0.7)'} ${g.startPosition || 0}%, ${g.end || 'rgba(0,0,0,0)'} ${g.endPosition || 100}%)`,\n opacity: ov.opacity ?? 0.5,\n }\n } else {\n const baseColor = ov.type === 'light' ? '255, 255, 255' : '0, 0, 0'\n overlayStyle = {\n position: 'absolute', inset: '0', pointerEvents: 'none',\n backgroundColor: `rgba(${baseColor}, ${ov.opacity ?? 0.5})`,\n }\n }\n\n children.push(\n React.createElement('div', {\n key: 'bg-overlay',\n className: ov.gradient ? 'background-overlay background-overlay--gradient' : 'background-overlay background-overlay--solid',\n style: overlayStyle,\n 'aria-hidden': 'true',\n })\n )\n }\n\n if (children.length === 0) return null\n\n return React.createElement('div', {\n className: `background background--${background.mode}`,\n style: containerStyle,\n 'aria-hidden': 'true',\n }, ...children)\n}\n\n/**\n * Render a single block for SSR.\n * Mirrors BlockRenderer.jsx but without hooks (no runtime data fetching).\n *\n * Two modes (mirrors client BlockRenderer):\n * - Bare (as=null/false): component only, no wrapper\n * - Section (as='section'/'div'/etc.): full treatment with wrapper, context, background\n *\n * @param {Block} block - Block instance to render\n * @param {Object} [options]\n * @param {string|null} [options.as='section'] - Wrapper element tag, or null/false for bare mode\n * @returns {React.ReactElement}\n */\nexport function renderBlock(block, { as = 'section' } = {}) {\n const Component = block.initComponent()\n\n if (!Component) {\n return React.createElement('div', {\n className: 'block-error',\n style: { padding: '1rem', background: '#fef2f2', color: '#dc2626' },\n }, `Component not found: ${block.type}`)\n }\n\n // Resolve entity data synchronously (SSG has no async): EntityStore fills the keys\n // the component declares (`meta.data`) from the fetches that reach the block.\n const meta = getComponentMeta(block.type)\n const entityStore = block.website?.entityStore\n let entityData = null\n if (entityStore) {\n const resolved = entityStore.resolve(block, meta)\n if (resolved.status === 'ready') entityData = resolved.data\n }\n\n // Build content and params with runtime guarantees.\n // prepareProps handles the full pipeline: entity data merge,\n // foundation content handler invocation, guaranteed content\n // structure, schema application, and param defaults.\n // See prepare-props.js for the pipeline details.\n const prepared = prepareProps(block, meta, entityData)\n const params = prepared.params\n const content = { ...prepared.content, ...block.properties }\n\n const componentProps = { content, params, block }\n\n // Bare mode: component only, no wrapper or section chrome.\n // Used by ChildBlocks for grid cells, tab panels, inline children, insets.\n if (!as) {\n return React.createElement(Component, componentProps)\n }\n\n // Section mode: full treatment with wrapper, context classes, background.\n const { background, ...wrapperProps } = getWrapperProps(block)\n\n // Merge Component.className (static classes declared on the component function)\n const componentClassName = Component.className\n if (componentClassName) {\n wrapperProps.className = wrapperProps.className\n ? `${wrapperProps.className} ${componentClassName}`\n : componentClassName\n }\n\n // Check if component handles its own background\n const hasBackground = background?.mode && meta?.background !== 'self'\n block.hasBackground = hasBackground\n\n // Determine wrapper element:\n // - Explicit as (not 'section') → use as prop directly\n // - Component.as → use component's declared tag (e.g., Header.as = 'header')\n // - fallback → 'section'\n const wrapperTag = as !== 'section' ? as : (Component.as || 'section')\n\n if (hasBackground) {\n return React.createElement(wrapperTag, wrapperProps,\n renderBackground(background),\n React.createElement('div', { style: { position: 'relative', zIndex: 10 } },\n React.createElement(Component, componentProps)\n )\n )\n }\n\n return React.createElement(wrapperTag, wrapperProps,\n React.createElement(Component, componentProps)\n )\n}\n\n/**\n * Render an array of blocks for SSR.\n */\nexport function renderBlocks(blocks) {\n if (!blocks || blocks.length === 0) return null\n return blocks.map((block, index) =>\n React.createElement(React.Fragment, { key: block.id || index },\n renderBlock(block)\n )\n )\n}\n\n/**\n * Render page layout for SSR.\n * Mirrors Layout.jsx but without hooks.\n */\nexport function renderLayout(page, website) {\n const layoutName = page.getLayoutName()\n const RemoteLayout = website.getRemoteLayout(layoutName)\n const layoutMeta = website.getLayoutMeta(layoutName)\n\n const bodyBlocks = page.getBodyBlocks()\n const areas = page.getLayoutAreas()\n\n // Mirror Layout.jsx: wrap body + each area in a thin div carrying its\n // view-transition-name, so the prerendered HTML matches what the SPA hydrates\n // and the browser can animate regions independently on client navigation.\n const areaNames = Object.keys(areas)\n const transitions = website.viewTransitions\n ? resolveLayoutTransitions(areaNames, layoutMeta?.transitions)\n : null\n const layers = resolveLayoutLayers(areaNames, layoutMeta?.layers)\n const wrapArea = (name, element) => {\n const style = areaWrapperStyle(name, transitions, layers)\n return style ? React.createElement('div', { style }, element) : element\n }\n\n const bodyElement = bodyBlocks ? wrapArea('body', renderBlocks(bodyBlocks)) : null\n const areaElements = {}\n for (const [name, blocks] of Object.entries(areas)) {\n areaElements[name] = wrapArea(name, renderBlocks(blocks))\n }\n\n if (RemoteLayout) {\n const params = { ...(layoutMeta?.defaults || {}), ...(page.getLayoutParams() || {}) }\n return React.createElement(RemoteLayout, {\n page, website, params,\n body: bodyElement,\n ...areaElements,\n })\n }\n\n // Default layout — mirror DefaultLayout in Layout.jsx, including its lack of\n // stacking: the area wrappers already carry their layers, and a positioned\n // element here would seal those layers inside it.\n return React.createElement(React.Fragment, null,\n areaElements.header && React.createElement('header', null, areaElements.header),\n bodyElement && React.createElement('main', null, bodyElement),\n areaElements.footer && React.createElement('footer', null, areaElements.footer)\n )\n}\n\n// ============================================================================\n// Layer 2: Initialization\n// ============================================================================\n\n/**\n * Construct a Uniweb singleton scoped to a single locale.\n *\n * Combines the three steps that every SSR consumer (browser SPA, Node\n * SSG, a server-side isolate) needs in the same order: slice the\n * multi-locale content payload, run `initPrerender` (which builds the\n * Website + wires foundation capabilities), then `setActiveLocale` so\n * `website.activeLang` stays in sync with what the page is rendering\n * for. Caller still owns DataStore hydration (per-request data differs\n * between requests; locale construction can be cached).\n *\n * @param {Object} content - Site content payload (possibly multi-locale).\n * @param {Object} foundation - Loaded foundation module.\n * @param {string} locale - Locale code to render in.\n * @param {Array<Object>|Object} [extensionsOrOptions] - Same shape as initPrerender's\n * third arg: an extensions array, or an options object when no extensions.\n * @param {Object} [maybeOptions] - Options object when extensions are passed.\n * @returns {import('@uniweb/core').default} The configured Uniweb singleton.\n */\nexport function initPrerenderForLocale(content, foundation, locale, extensionsOrOptions, maybeOptions) {\n const localeContent = sliceContentForLocale(content, locale)\n const uniweb = initPrerender(localeContent, foundation, extensionsOrOptions, maybeOptions)\n const defaultLang = resolveDefaultLocale(content?.config)\n if (locale && locale !== defaultLang && uniweb.activeWebsite?.setActiveLocale) {\n uniweb.activeWebsite.setActiveLocale(locale)\n }\n return uniweb\n}\n\n/**\n * Create and configure the Uniweb runtime for prerendering.\n *\n * Handles the full initialization sequence in the correct order:\n * createUniweb → setFoundation → capabilities → layoutMeta → basePath → childBlockRenderer.\n *\n * Returns the configured uniweb instance. Consumers can add extras after:\n * - Build: pre-populate DataStore, load extensions\n * - A server-side renderer: (none needed — the payload is complete)\n *\n * NOTE: Does NOT clone content. Cloning is the consumer's responsibility\n * (build modifies content before init; a server-side renderer clones upfront).\n *\n * @param {Object} content - Site content JSON (pages, config, hierarchy)\n * @param {Object} foundation - Loaded foundation module\n * @param {Object} [options]\n * @param {function} [options.onProgress] - Progress callback\n * @returns {Object} Configured uniweb instance\n */\nexport function initPrerender(content, foundation, extensionsOrOptions, maybeOptions) {\n // Backwards-compatible arg shape: (content, foundation, options) or\n // (content, foundation, extensions, options). Extensions must be passed at\n // construction so the Website's FetcherDispatcher sees their routes.\n let extensions = []\n let options = {}\n if (Array.isArray(extensionsOrOptions)) {\n extensions = extensionsOrOptions\n options = maybeOptions || {}\n } else {\n options = extensionsOrOptions || {}\n }\n const { onProgress = () => {} } = options\n\n onProgress('Initializing runtime...')\n // Uniweb constructor wires foundation, capabilities, layoutMeta, handlers,\n // and extensions at construction time — see `@uniweb/core`'s src/uniweb.js.\n const uniweb = createUniweb(content, foundation, extensions)\n\n // Set base path from site config for subdirectory deployments\n if (content.config?.base && uniweb.activeWebsite?.setBasePath) {\n uniweb.activeWebsite.setBasePath(content.config.base)\n }\n\n // Set childBlockRenderer so ChildBlocks/Visual/Render work during prerender.\n // Mirrors the client's ChildBlocks component in PageRenderer.jsx:\n // - default bare rendering (no wrapAs) — component only, no wrapper\n // - pass wrapAs to opt into full section treatment\n uniweb.childBlockRenderer = function InlineChildBlocks({ blocks, from, wrapAs }) {\n const blockList = blocks || from?.childBlocks || []\n return blockList.map((childBlock, index) =>\n React.createElement(React.Fragment, { key: childBlock.id || index },\n renderBlock(childBlock, { as: wrapAs || null })\n )\n )\n }\n\n // The same background a section gets, for kit's `SectionBackground` — a component that\n // paints its own (`background: 'self'`). Mirrors the SPA's `Background` (setup.js).\n uniweb.backgroundRenderer = function InlineBackground(background) {\n return renderBackground(background)\n }\n\n // L2 (singleton wiring): defaultInsets, xref.build(), and any future\n // framework-level capability bridge — shared with setup.js so both\n // boot paths apply the same foundation contract. See\n // wire-foundation.js — its header states the rule for what belongs in\n // that helper vs. here vs. setup.js.\n wireFoundationCapabilities(uniweb, foundation)\n\n // Site-wide theme CSS. Unconditional here: at this point there is no\n // <head> to inspect, and injectPageContent() emits the result\n // idempotently, so a lane that already baked the style tag is unaffected.\n ensureThemeCss(uniweb, foundation)\n\n // Register SSR-safe routing so useRouting()/useActiveRoute() work during prerender.\n // renderPage() calls website.setActivePage() before rendering each page,\n // so activePage.route always reflects the page being rendered.\n const website = uniweb.activeWebsite\n uniweb.routingComponents = {\n useLocation: () => {\n // ⛔ `activePage.route` IS ALREADY ROOT-RELATIVE — `/`, `/about`. This used to\n // return `'/' + route`, so every prerendered page reported a pathname with a\n // doubled leading slash: `//` on the homepage, `//about` elsewhere.\n //\n // It failed silently for as long as it existed, because nothing in the\n // runtime reads pathname — a FOUNDATION does. Measured 2026-09-18: a language\n // switcher passing `useLocation().pathname` to `getLocaleUrl` emitted\n // `href=\"//\"`, which a browser resolves as a PROTOCOL-RELATIVE url (host\n // empty), not as the site root. The SPA was unaffected, so it reproduced only\n // in the built output.\n const route = website?.activePage?.route || '/'\n const pathname = route.startsWith('/') ? route : `/${route}`\n return { pathname, search: '', hash: '', state: null, key: 'default' }\n },\n useParams: () => ({}),\n useNavigate: () => () => {},\n }\n\n return uniweb\n}\n\n/**\n * Pre-fetch icons from CDN and populate the Uniweb icon cache.\n * Stores the cache on siteContent._iconCache for embedding in HTML.\n *\n * @param {Object} siteContent - Site content JSON (mutated: _iconCache added)\n * @param {Object} uniweb - Configured uniweb instance\n * @param {function} [onProgress] - Progress callback\n */\nexport async function prefetchIcons(siteContent, uniweb, onProgress = () => {}) {\n const icons = siteContent.icons?.used || []\n if (icons.length === 0) return\n\n const cdnBase = siteContent.config?.icons?.cdnUrl || DEFAULT_ICON_BASE\n\n onProgress(`Fetching ${icons.length} icons for SSR...`)\n\n const results = await Promise.allSettled(\n icons.map(async (iconRef) => {\n const [family, name] = iconRef.split(':')\n const url = iconUrl(family, name, cdnBase)\n const response = await fetch(url)\n if (!response.ok) throw new Error(`HTTP ${response.status}`)\n const svg = await response.text()\n uniweb.iconCache.set(`${family}:${name}`, svg)\n })\n )\n\n const succeeded = results.filter(r => r.status === 'fulfilled').length\n const failed = results.filter(r => r.status === 'rejected').length\n if (failed > 0) {\n const msg = `Fetched ${succeeded}/${icons.length} icons (${failed} failed)`\n console.warn(`[prerender] ${msg}`)\n onProgress(` ${msg}`)\n }\n\n // Store icon cache on siteContent for embedding in HTML\n if (uniweb.iconCache.size > 0) {\n siteContent._iconCache = Object.fromEntries(uniweb.iconCache)\n }\n}\n\n// ============================================================================\n// Layer 3: Per-page rendering\n// ============================================================================\n\n// ⭐ `resolvePage` LIVES IN ITS OWN LEAF and is re-exported here, so the public\n// `@uniweb/runtime/ssr` surface is unchanged. It is `website.getPage` and needs\n// no renderer; this file imports `react-dom/server` at module scope, so a caller\n// that only wants the lookup used to get React's server renderer with it\n// (`./resolve-page.js`, 2026-09-20).\nexport { resolvePage } from './resolve-page.js'\n\n/**\n * Classify an SSR rendering error.\n *\n * @param {Error} err\n * @returns {{ type: 'hooks'|'null-component'|'unknown', message: string }}\n */\nexport function classifyRenderError(err) {\n const msg = err.message || ''\n\n if (msg.includes('Invalid hook call') || msg.includes('useState') || msg.includes('useEffect')) {\n return {\n type: 'hooks',\n message: 'contains components with React hooks (renders client-side)',\n }\n }\n\n if (msg.includes('Element type is invalid') && msg.includes('null')) {\n return {\n type: 'null-component',\n message: 'a component resolved to null (often hook-related, renders client-side)',\n }\n }\n\n return {\n type: 'unknown',\n message: msg,\n }\n}\n\n/**\n * Render a single page to HTML.\n *\n * Handles the full per-page pipeline:\n * setActivePage → renderLayout → renderToString → error handling → section override CSS.\n *\n * @param {Page} page - Page instance to render\n * @param {Website} website - Website instance\n * @returns {{ renderedContent: string, sectionOverrideCSS: string } | { error: { type: string, message: string } }}\n */\nexport function renderPage(page, website) {\n website.setActivePage(page.route)\n\n // A page that claims content but yields no blocks has not been loaded — it is\n // not an empty page. `Page#bodyBlocks` returns [] when its sections are absent\n // from the payload (split content), on the understanding that a caller loads\n // them first: the SPA does, in PageRenderer and at boot. THIS path never has.\n //\n // Left alone, that renders a structurally valid, completely empty document and\n // reports success — which is the worst shape a failure can take, and it cost a\n // host most of a day chasing a renderer that was doing what it was told.\n // Distinguishing it here is cheap: a content-less container reports\n // hasContent() === false and is correctly empty, so the two never collide.\n if (page.hasContent?.() && page.getBodyBlocks().length === 0) {\n return {\n error: {\n type: 'content-not-loaded',\n message:\n `page \"${page.route}\" declares content but has no loaded sections — ` +\n 'its sections are not in the payload and this renderer does not fetch them',\n },\n }\n }\n\n const element = renderLayout(page, website)\n\n let renderedContent\n try {\n renderedContent = renderToString(element)\n } catch (err) {\n return { error: classifyRenderError(err) }\n }\n\n // Build per-page section override CSS (theme pinning, component vars)\n const appearance = website.themeData?.appearance\n // Every block the page renders — each layout area it shows, a sidebar too; the builder walks\n // their child sections. The SPA renderer passes the same (`PageRenderer.jsx`).\n const sectionOverrideCSS = buildSectionOverrides(page.getAllBlocks(), appearance)\n\n return { renderedContent, sectionOverrideCSS }\n}\n\n// ============================================================================\n// HTML injection\n// ============================================================================\n\n/**\n * Escape HTML special characters.\n */\nexport function escapeHtml(str) {\n if (!str) return ''\n return String(str)\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n\n/**\n * Inject prerendered content into an HTML shell.\n *\n * THE SHARED PRERENDER SEAM. Every lane that turns a Page into HTML calls this:\n * the framework's SSG (@uniweb/build's prerender.js) and the cloud worker's\n * just-in-time render. Anything a page needs *because it was prerendered at all*\n * belongs here, so a new feature reaches both lanes at once.\n *\n * Common operations shared by both build and cloud:\n * - Replace #root div with rendered HTML\n * - Update page title\n * - Add/update meta description\n * - Inject section override CSS\n * - Inject the pre-paint appearance script\n *\n * Build layers its additional injections on top of this return value:\n * __SITE_CONTENT__ JSON, icon cache, theme CSS (build-specific).\n *\n * WHICH SIDE OF THE SEAM? Ask what the injection is derived from. If it needs\n * only the page/website graph — which every lane has — it goes here. If it needs\n * a build-only artifact (the emitted bundle, the collection JSON files, the\n * prefetched icon cache, the compiled theme CSS), it goes in the caller. Getting\n * this wrong is silent: the appearance boot script started life in\n * @uniweb/build's injectBuildData and therefore never reached cloud-rendered\n * pages, which flashed light for every dark-mode visitor.\n *\n * @param {string} html - HTML shell\n * @param {string} renderedContent - React renderToString output\n * @param {Object} page - Page data { title, description, route }\n * @param {Object} [options]\n * @param {string} [options.sectionOverrideCSS] - Per-page section override CSS\n * @returns {string} HTML with injected content\n */\nexport function injectPageContent(html, renderedContent, page, options = {}) {\n let result = html\n\n // Pre-paint appearance script. Prerendered HTML carries real content styled\n // from :root (light) tokens, so a dark visitor would see a flash of light\n // before the bundle hydrates and applies the class. Read off the page's\n // website back-ref rather than a parameter: every lane builds the same graph,\n // so this needs no plumbing and no per-lane opt-in. Idempotent, because\n // @uniweb/build's injectBuildData may run over this HTML afterwards.\n if (!result.includes('id=\"uniweb-appearance\"')) {\n const bootScript = renderAppearanceBootScript(page?.website?.themeData?.appearance)\n if (bootScript) {\n result = result.replace('</head>', ` ${bootScript}\\n</head>`)\n }\n }\n\n // Site-wide theme CSS. Derived from the website graph\n // (`website.themeData`), so it belongs on THIS side of the seam — every\n // lane builds that graph. It lived in @uniweb/build's injectBuildData\n // until 2026-07-28, which meant sites served by any lane that doesn't run\n // the framework's build rendered with every semantic token unset: no\n // colours, no backgrounds, no failure anywhere. That is the same mistake\n // the appearance script above was moved out of, four lines below the\n // comment warning about it — see the note in `@uniweb/build`'s src/prerender.js.\n // Idempotent, so a shell that already carries the tag is left alone.\n const themeData = page?.website?.themeData\n const themeCss = themeData?.css\n if (themeCss && !result.includes('id=\"uniweb-theme\"')) {\n result = result.replace(\n '</head>',\n ` <style id=\"uniweb-theme\">\\n${themeCss}\\n </style>\\n</head>`\n )\n }\n\n // The theme's font <link> tags — same seam, same reasoning. Graph-derived,\n // so a lane that never runs @uniweb/build still gets its webfonts instead of\n // falling back to system faces. Deduped on FONT_LINKS_MARKER rather than an\n // id because <link> tags have none; the marker is owned by @uniweb/theming,\n // which generates the block, so this and @uniweb/build read one literal.\n if (themeData?.links && !result.includes(FONT_LINKS_MARKER)) {\n result = result.replace(\n '</head>',\n ` ${FONT_LINKS_MARKER}\\n${themeData.links}\\n</head>`\n )\n }\n\n // Inject per-page section override CSS before </head>\n if (options.sectionOverrideCSS) {\n const overrideStyle = `<style id=\"uniweb-page-overrides\">\\n${options.sectionOverrideCSS}\\n</style>`\n result = result.replace('</head>', `${overrideStyle}\\n</head>`)\n }\n\n // Replace the empty root div with pre-rendered content\n result = result.replace(\n /<div id=\"root\">[\\s\\S]*?<\\/div>/,\n `<div id=\"root\">${renderedContent}</div>`\n )\n\n // Update the document title — by the rule the SPA sets `document.title` with\n // (`documentTitle`: `<page> | <site>`), so the title a crawler reads here is the one a\n // visitor keeps once the bundle loads. ⛔ Until 2026-09-14 this wrote the page title\n // alone. getTitle() so isIndex pages inherit their parent's title.\n const title = documentTitle(page.getTitle?.() || page.title, page.website?.name)\n if (title) {\n result = result.replace(\n /<title>.*?<\\/title>/,\n `<title>${escapeHtml(title)}</title>`\n )\n }\n\n // Add/update meta description\n if (page.description) {\n const metaDesc = `<meta name=\"description\" content=\"${escapeHtml(page.description)}\">`\n if (result.includes('<meta name=\"description\"')) {\n result = result.replace(/<meta name=\"description\"[^>]*>/, metaDesc)\n } else {\n result = result.replace('</head>', `${metaDesc}\\n</head>`)\n }\n }\n\n // Social / SEO meta from the page's effective head metadata (page seo\n // cascading over site-level seo — see Page.getHeadMeta). The SPA emits these\n // client-side via useHeadMeta; this is the SSR twin, so crawlers and social\n // unfurlers (which don't run JS) get them in the static HTML too.\n const headMeta = page.getHeadMeta?.()\n if (headMeta) {\n const og = headMeta.og || {}\n const keywords = Array.isArray(headMeta.keywords)\n ? headMeta.keywords.join(', ')\n : headMeta.keywords\n const tags = []\n if (keywords) tags.push(`<meta name=\"keywords\" content=\"${escapeHtml(keywords)}\">`)\n if (headMeta.robots) tags.push(`<meta name=\"robots\" content=\"${escapeHtml(headMeta.robots)}\">`)\n if (og.title) tags.push(`<meta property=\"og:title\" content=\"${escapeHtml(og.title)}\">`)\n if (og.description) tags.push(`<meta property=\"og:description\" content=\"${escapeHtml(og.description)}\">`)\n if (og.image) tags.push(`<meta property=\"og:image\" content=\"${escapeHtml(og.image)}\">`)\n if (og.url) tags.push(`<meta property=\"og:url\" content=\"${escapeHtml(og.url)}\">`)\n tags.push('<meta property=\"og:type\" content=\"website\">')\n tags.push(`<meta name=\"twitter:card\" content=\"${og.image ? 'summary_large_image' : 'summary'}\">`)\n if (og.title) tags.push(`<meta name=\"twitter:title\" content=\"${escapeHtml(og.title)}\">`)\n if (og.description) tags.push(`<meta name=\"twitter:description\" content=\"${escapeHtml(og.description)}\">`)\n if (og.image) tags.push(`<meta name=\"twitter:image\" content=\"${escapeHtml(og.image)}\">`)\n if (headMeta.canonical) tags.push(`<link rel=\"canonical\" href=\"${escapeHtml(headMeta.canonical)}\">`)\n if (tags.length) result = result.replace('</head>', `${tags.join('\\n')}\\n</head>`)\n }\n\n return result\n}\n\n// ============================================================================\n// 404 fallback generation\n// ============================================================================\n\n/**\n * Generate 404.html content for static hosting fallback.\n *\n * Serves two purposes on static hosts (GitHub Pages, Cloudflare Pages, etc.):\n * 1. Real 404: pre-rendered custom 404 page content (or blank #root if none defined)\n * 2. Valid dynamic route (e.g. /blog/2): inline script clears #root so SPA renders fresh\n *\n * Flow: static host serves 404.html → inline script runs before React mounts →\n * - dynamic route: clears #root, React renders the page normally\n * - real 404: leaves #root with pre-rendered content, React re-renders same 404 page\n *\n * @param {Object} options\n * @param {string} options.baseHtml - Assembled HTML shell (with site content already injected)\n * @param {Object} options.website - Initialized Website instance (from initPrerender)\n * @param {Object} options.siteContent - Site content object (to find dynamic templates)\n * @returns {{ html: string, hasNotFoundPage: boolean }}\n */\nexport function generate404Html({ baseHtml, website, siteContent }) {\n // Extract patterns for routes that remain as dynamic templates (prerender: false)\n // '/blog/:id' → /^\\/blog\\/([^/]+)$/. Compiled by the shared matcher rather\n // than a second regex built here: this file used to build its own with\n // `:[^/]+`, which disagreed with core's `:(\\w+)` on any param name carrying a\n // non-word character. See @uniweb/core/route-match for the whole story.\n // ⛔ **ROUTE, NOT THE FLAG — a filter must not read absence as \"not parametric\".**\n // This filtered on `p.isDynamic` alone. A producer that marks only the bracket\n // page (or none at all) left every parametric route out of the list, so a cold\n // hit on one showed the 404 body and never cleared #root for the SPA. The flag\n // is a producer's claim; the `:` in the route is the thing itself, and every\n // flagged page has one — so this is a superset and cannot lose a route.\n const dynamicTemplates = siteContent.pages?.filter((p) => p.isDynamic || isDynamicRoute(p.route)) || []\n const routePatterns = dynamicTemplates.map((p) => routePatternToRegex(p.route).regex.source)\n\n let html = baseHtml\n\n // Pre-render the custom 404 page content into #root (if the site defines one),\n // otherwise inject a default 404 message so the page isn't blank before JS loads\n const notFoundPage = website.getNotFoundPage()\n if (notFoundPage) {\n const notFoundResult = renderPage(notFoundPage, website)\n if (notFoundResult && !notFoundResult.error) {\n html = injectPageContent(html, notFoundResult.renderedContent, notFoundPage, {\n sectionOverrideCSS: notFoundResult.sectionOverrideCSS,\n })\n }\n } else {\n const basePath = website.basePath || ''\n html = html.replace(\n /<div id=\"root\">[\\s\\S]*?<\\/div>/,\n `<div id=\"root\">${default404Html(basePath)}</div>`\n )\n }\n\n // Inject inline script: if path matches a dynamic route, clear #root before React mounts\n // so the SPA renders the correct page rather than the 404 content\n if (routePatterns.length > 0) {\n const patternList = routePatterns.map((p) => `/${p}/`).join(',')\n // The path is normalized here rather than by making every pattern accept a\n // trailing slash — same rule the matcher applies, applied in one place.\n const dynamicScript =\n `<script>(function(){` +\n `var p=[${patternList}],r=window.location.pathname.replace(/\\\\/+$/,'')||'/';` +\n `if(p.some(function(x){return x.test(r)})){` +\n `var el=document.getElementById('root');if(el)el.innerHTML='';` +\n `}})()</script>`\n html = html.replace('</body>', `${dynamicScript}\\n</body>`)\n }\n\n return { html, hasNotFoundPage: !!notFoundPage }\n}\n","/**\n * Runtime default fetcher.\n *\n * Used as the FetcherDispatcher's terminal fallback when no foundation\n * transport claims the request. Sites that declare no transport at all —\n * starter/docs/marketing templates hitting /data/*.json — ride on this path\n * with zero config, and so does a site a host serves live.\n *\n * ⭐ It speaks exactly TWO lanes, and takes NO site-level vocabulary for a\n * backend of the author's own:\n *\n * - a compiled file — `path:` under the site's base (`/data/<query>.json`,\n * a per-record file), or a plain JSON `url:` the author wrote;\n * - the host's RECORDS SERVICE — `ask:`, one POST per tick carrying every\n * question the page asked, answered per key (the records contract, as this\n * client reads it).\n *\n * A query's set — `scope:` / `where:` / `sort:` / `limit:` — and the `narrow`\n * a fetch takes of it are evaluated HERE, locally, over what the first lane\n * returns — with `@uniweb/core`'s one evaluator (`evaluateQuery`), the same the\n * build uses — and at the source when asked. Nothing decides that per site: the\n * LANE decides.\n *\n * ⛔ RETIRED 2026-09-04 [Diego]: `fetcher.baseUrl`, `headers`, `envelope`,\n * `supports`, `request.style` / `request.rename` and the `json-body`\n * request-style registry. *\"3rd party endpoints must be supported at the\n * foundation level… making the runtime+core lean.\"* A backend with its own\n * base, headers, wire or query language is a TRANSPORT — a named\n * `{ resolve, cacheKey? }` the foundation (or an extension) registers and\n * the site selects per binding key (`as`) in `fetcher.transports`. The build warns once\n * and drops a retired key from the payload, so an author's backend does not\n * silently stop being reached.\n *\n * A request may carry `method: 'POST'` + `body:` for an endpoint that takes a\n * query in a body (GraphQL, a search endpoint) — an external query's, or its\n * `record:` request's — and `transform:`, a dot-path to the records in ONE\n * response. `{paramName}` placeholders in body strings are substituted from\n * `request.dynamicContext`, so a parametric page's record request can reference\n * its route param. ⛔ `envelope` (the object form of `detail:`) is retired with\n * it, 2026-09-13: a record request's own `transform` is `record.transform`.\n *\n * Exported from a subpath — `@uniweb/runtime/default-fetcher` — for\n * runtime-level callers (the editor's preview iframe, custom runtime\n * harnesses). **Foundations should not import this.** A foundation that\n * wants plain URL + JSON behavior simply omits its own transport; the\n * runtime installs this one automatically.\n *\n * Intentional omission: credentials / secrets. Any value the framework puts\n * into the served HTML is public to the browser. Sites needing private\n * credentials use a deployment-layer proxy — the site fetches a same-origin\n * URL, and a layer in front resolves the credential and forwards upstream.\n */\n\nimport {\n substitutePlaceholders,\n evaluateQuery,\n sortRecords,\n sortToWire,\n deriveCacheKey,\n resolveServiceUrl,\n} from '@uniweb/core'\n\n/**\n * @param {Object} [options]\n * @param {string} [options.basePath=''] - Prepended to local absolute paths\n * for subpath deployments. Remote URLs pass through unchanged.\n * @param {boolean} [options.dev=false] - Dev-mode diagnostics: a bad `sort:`\n * throws instead of delivering the records unsorted.\n * @param {Function|null} [options.fetch=null] - The transport. A host executing\n * fetches outside a browser (an SSR isolate) decides how a site-relative\n * address is dispatched — through its own origin or a service binding — and\n * hands that in. Defaults to the global `fetch`, resolved at call time so a\n * test stub installed later is honoured.\n * @returns {{ cacheKey: (req: Object) => string, resolve: (req: Object, ctx: Object) => Promise<{ data, error?, meta? }> }}\n */\nexport function createDefaultFetcher({ basePath = '', dev = false, fetch: fetchImpl = null } = {}) {\n const doFetch = (input, init) => (fetchImpl || globalThis.fetch)(input, init)\n const pathPrefix = basePath && basePath !== '/' ? basePath.replace(/\\/$/, '') : ''\n\n\n // ⭐ THE RECORDS SERVICE — a batch of the misses, one POST, merged per key.\n //\n // The entity store dispatches every config a page needs in one synchronous\n // loop before awaiting any of them, so a question enqueued here and flushed\n // on the next microtask carries every miss of that page in one body. The\n // batch response is never cached as one: each request gets its own answer,\n // keyed by its own question.\n const askQueues = new Map()\n const askRecords = (request, ctx) => {\n // ⛔ A question needs the query's Model ref. A payload that offers the\n // service and carries no `config.queries` entry for the query cannot ask;\n // that is a producer defect and it is said here, per key, with no request.\n if (typeof request.schema !== 'string' || !request.schema) {\n return Promise.resolve({\n data: null,\n error: `the payload offers the records service but carries no Model ref for query ` +\n `\"${request.query ?? request.as}\" (config.queries) — it cannot be asked`,\n })\n }\n return new Promise((resolve) => {\n const url = resolveServiceUrl(request.ask, pathPrefix)\n let queue = askQueues.get(url)\n if (!queue) {\n queue = []\n askQueues.set(url, queue)\n queueMicrotask(() => {\n askQueues.delete(url)\n flushAsked(url, queue, doFetch)\n })\n }\n queue.push({ request, ctx, resolve })\n })\n }\n\n // ⭐ VIEWS OF ONE FILE SHARE ONE READ IN FLIGHT. Each view — its own `scope`,\n // `where`, `sort` or `limit` — is its own cache entry (`deriveCacheKey` hashes\n // the view) and is cut from the read; views asked together (a page's sections,\n // a host's prefetch) share one request. ⛔ Until 2026-09-11 the view was applied\n // before the dispatcher stored the answer under a key that left the view out, so\n // one file had ONE entry and whoever asked first decided what everyone got\n // (measured). A settled read is not kept: a view asked later reads again (a\n // static host's HTTP cache answers it), so dropping an entry and asking again\n // still reaches the source. The read carries no caller's abort signal — several\n // views may be waiting on it, and one navigating away must not cancel it for\n // the others.\n const reads = new Map()\n const readOnce = (target, init) => {\n const key = `${init.method} ${target} ${init.body ?? ''}`\n let pending = reads.get(key)\n if (!pending) {\n pending = readResponse(doFetch, target, init).finally(() => reads.delete(key))\n reads.set(key, pending)\n }\n return pending\n }\n\n return {\n /**\n * The cache identity is the request's ADDRESS and the view it takes of it —\n * or, when asked of the records service, the QUESTION (`deriveCacheKey`).\n * Two pages declaring different `where:` clauses against one path are two\n * entries, each cut from a read of the file (`readOnce`, above).\n */\n cacheKey(request) {\n return deriveCacheKey(request)\n },\n\n async resolve(request, ctx = {}) {\n if (!request) return { data: null }\n if (request.ask) return askRecords(request, ctx)\n const { path, url, transform, body: rawBody } = request\n\n // Normalize method. Only GET and POST are supported by the default\n // fetcher — mutations (PUT/PATCH/DELETE) are a different feature\n // (optimistic updates, action semantics) and don't belong here.\n let method = (request.method || 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'POST') {\n console.warn(`[default-fetcher] method \"${request.method}\" is not supported — falling back to GET.`)\n method = 'GET'\n }\n\n let target\n if (path) {\n // Local file under public/ — basePath applies for subpath deploys.\n target = pathPrefix && path.startsWith('/') && !path.startsWith('//')\n ? pathPrefix + path\n : path\n } else if (url) {\n // A URL the author wrote, sent exactly as written.\n target = url\n } else {\n return { data: [], error: 'No path, url or ask specified' }\n }\n\n const init = { method }\n\n if (method === 'POST') {\n // Substitute {paramName} placeholders in body strings using the\n // dynamic-route context. The helper expects a flat key→value map;\n // build it from dynamicContext's { paramName, paramValue } shape.\n // Strict-brace matcher: GraphQL selection sets pass through unchanged.\n const dc = request.dynamicContext\n const body = (rawBody !== undefined && rawBody !== null && dc && dc.paramName)\n ? substitutePlaceholders(rawBody, { [dc.paramName]: dc.paramValue }, { encode: false })\n : rawBody\n if (body !== undefined && body !== null) {\n init.headers = { 'Content-Type': 'application/json' }\n init.body = typeof body === 'string' ? body : JSON.stringify(body)\n }\n }\n\n try {\n const response = await readOnce(target, init)\n if (ctx.signal?.aborted) return { data: [], error: 'aborted' }\n\n if (!response.ok) {\n return { data: [], error: `HTTP ${response.status}: ${response.statusText}` }\n }\n\n let data = response.body\n\n // Unwrap the response: the request's own `transform:`, and nothing else — a\n // record request carries its `record.transform`, never the list's.\n if (transform && data !== null && data !== undefined) {\n data = getNestedValue(data, transform)\n }\n\n // Evaluate the query locally. Only applies to array data\n // (filtering/sorting/limiting a single record doesn't make sense).\n // For non-arrays, operators are ignored — the source returned what\n // it returned.\n data = applyOperators(data, request, { dev, locale: sortLocale(request, ctx) })\n\n // ⭐ Say whether WHOLE records were delivered, so the record index can\n // file them — what the config asked for, echoed: briefs when the query\n // has a per-record source, whole records otherwise. (The service reports\n // `whole` per\n // key and overrides this with what it actually served.)\n const whole = typeof request.whole === 'boolean' ? request.whole : undefined\n return whole === undefined ? { data: data ?? [] } : { data: data ?? [], meta: { whole } }\n } catch (error) {\n if (error?.name === 'AbortError') {\n return { data: [], error: 'aborted' }\n }\n return { data: [], error: error?.message || String(error) }\n }\n },\n }\n}\n\n/**\n * One question of a batch, in the records service's own vocabulary\n * (the records contract, §2).\n * ⭐ TWO LEVELS, ruled 2026-09-14 [Diego]:\n *\n * - **the top level is the query as saved** — `schema` (required), `scope` a bare\n * path, `where`, `sort` one key spelled `date` / `-date`, `limit` — and `whole`\n * when the whole record is wanted. Its records are the set, and the top level is\n * the same on every question about it;\n * - **`narrow`** — how this page takes less than the set: `where`, `match` (a\n * parametric page's record), `sort`, `cursor` (to resume a previous answer) and\n * `limit`. Absent when the question asks for the set as it is.\n *\n * ⛔ Until 2026-09-14 the two were one flat question — the page's `where` joined the\n * query's, its `sort` and `limit` replaced the query's — and `match` and `cursor` sat\n * at the top level, where the service no longer takes them. There is no fallback:\n * the service answers an unknown or misplaced field with a `400` for the request.\n *\n * ⭐ A where-object crosses exactly as authored: the evaluator and the service speak\n * one language (`@uniweb/core`'s where evaluator). ⛔ `nin` was respelled `not_in`\n * here until 2026-09-13, when the evaluator took `not_in` as its only spelling.\n * Anything the service does not answer (a retired operator) is sent as written and\n * answered there: never approximated.\n */\nfunction toQuestion(request) {\n const q = { schema: request.schema }\n const where = request.where && typeof request.where === 'object' ? request.where : null\n // ⭐ `scope` crosses as authored. ⛔ A top-level `where.path.under` was respelled\n // into it until 2026-09-11, when that form was retired in favour of `scope:`\n // [Diego] — the build refuses it now, so there is nothing left to respell.\n const scope = typeof request.scope === 'string' && request.scope ? request.scope : null\n if (scope) q.scope = scope\n if (where) q.where = where\n const sort = wireSort(request.sort)\n if (sort) q.sort = sort\n if (typeof request.limit === 'number' && request.limit > 0) q.limit = request.limit\n // ⛔ ONLY WHEN TRUE. The brief is the default and absent means the brief, so\n // sending `whole: false` would be noise on every list question the client makes.\n if (request.whole === true) q.whole = true\n const narrow = toNarrow(request.narrow)\n if (narrow) q.narrow = narrow\n // ⛔ `exhaustive` and `maxPages` deliberately do NOT cross — they are client\n // instructions about how many times to ask, never part of what is being asked.\n return q\n}\n\n/**\n * A question's `narrow`, with only the fields the service takes inside it — any other\n * is a `400` for the whole request there — or `null` when nothing is left.\n *\n * - `where` crosses as authored;\n * - `match` — the record of a parametric page: one key and the URL's value, beside\n * the author's `where`, never merged into it (`buildDetailConfig`);\n * - `sort` in the service's spelling;\n * - ⭐ `cursor` — the ONLY field in a question that is not the author's: opaque, from a\n * previous answer's `cursors`, sent back on the same question and nothing else\n * changed. A cursor is a narrowing like the rest, so the top level stays the query as\n * saved on every page;\n * - `limit`, when it cuts.\n */\nfunction toNarrow(narrow) {\n if (!narrow || typeof narrow !== 'object') return null\n const n = {}\n if (narrow.where && typeof narrow.where === 'object') n.where = narrow.where\n if (narrow.match && typeof narrow.match === 'object') n.match = narrow.match\n const sort = wireSort(narrow.sort)\n if (sort) n.sort = sort\n if (typeof narrow.cursor === 'string' && narrow.cursor) n.cursor = narrow.cursor\n if (typeof narrow.limit === 'number' && narrow.limit > 0) n.limit = narrow.limit\n return Object.keys(n).length > 0 ? n : null\n}\n\n/**\n * A `sort` in the service's spelling — `date` / `-date` — or, for one outside the\n * language, the author's text AS WRITTEN, which the service answers by its own rule\n * (ignored: the author's arrangement). ⛔ Never a throw: this runs while a page's whole\n * batch is being composed, before any request, and until 2026-09-14 a sort `parseSort`\n * refuses — two keys, a bad direction — threw here and left every question of the batch\n * pending forever.\n */\nfunction wireSort(sort) {\n try {\n return sortToWire(sort)\n } catch {\n return typeof sort === 'string' && sort.trim() ? sort : null\n }\n}\n\n/**\n * Send one batch to the records service and hand each question its own answer.\n *\n * The response is `{ data, whole?, cursors?, partial? }` (backend's records-query\n * contract rev D, 2026-09-13), each map absent when empty: `data` answers exactly the\n * keys sent — `[]` when nothing matched, and `[]` too for a question an author got\n * wrong, since the service answers a mistake rather than refusing it; `whole[key]` says\n * a key was served as whole records; `cursors[key]` that more records remain past the\n * question's `narrow.limit` — a query's own `limit` defines the set and never produces\n * one; `partial[key]` that a source of the key did not answer. A key\n * missing from `data` is a protocol violation and is reported as an error, never as\n * silence. ⛔ `errors` and `limits` are not read: the service stopped sending them in\n * the same revision, and this client read both until 2026-09-14.\n *\n * ⭐ `cursors` ARE READ — 2026-09-06 [Diego], reversing the ruling that had them\n * \"received and IGNORED, because framework has no paging concept.\" ⛔ **That ruling\n * described our client and was silently wrong about our USERS:** a `cursors` entry is\n * how the service says there is more, and discarding it meant a hosted list could\n * render part of its records with no way for an author to tell that from the end of\n * the data. **The silent class, on a visitor's page.**\n *\n * Two behaviours, deliberately not one:\n *\n * - **a page render REPORTS** — `meta.partial` rides the answer, and nothing pages\n * automatically. Auto-paging here would put unbounded round trips in front of\n * paint for a section that may only show ten rows.\n * - **an exhaustive caller PAGES** — `request.exhaustive` follows `cursors`\n * until the service stops issuing them. `collectSiteRecords` is the caller\n * that wants it (a corpus is not a page), and `maxPages` bounds the loop so\n * a service that always answers with a cursor cannot spin.\n */\n\n/**\n * Pages an exhaustive request follows before stopping and reporting the answer\n * as partial. A caller sets its own with `request.maxPages`.\n *\n * ⚖️ **20 is a bound, not a target** — it is the value a real caller chose for a\n * real per-request budget (a search index, 20 × 100), taken as the default\n * because any bound prevents a runaway and a low one fails visibly rather than\n * expensively. A caller that knows its budget passes its own.\n */\nconst DEFAULT_MAX_PAGES = 20\nasync function flushAsked(url, queue, doFetch) {\n // One shared page loop: the batch is sent, and any entry that asked to be\n // exhaustive and came back with a cursor is re-sent alone until it is done.\n const pending = new Map()\n const body = {}\n const keys = []\n for (const entry of queue) {\n const base = entry.request.as || 'q'\n let key = base\n for (let n = 2; key in body; n += 1) key = `${base}#${n}`\n keys.push(key)\n body[key] = toQuestion(entry.request)\n }\n let parsed\n try {\n const response = await doFetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!response.ok) {\n // A protocol violation is refused for the WHOLE request with a problem body\n // whose `detail` names the key and the fault (an unknown operator, an empty\n // binding key, a non-BCP-47 locale segment…). Surface that sentence on every\n // key of the batch rather than the bare status: the author reads\n // `block.dataError` and the status alone says nothing they can act on.\n let detail = null\n try {\n const problem = safeParseJSON(await response.text())\n if (problem && typeof problem.detail === 'string' && problem.detail) detail = problem.detail\n } catch { /* an unreadable body falls back to the status line */ }\n const error = detail\n ? `HTTP ${response.status}: ${detail}`\n : `HTTP ${response.status}: ${response.statusText}`\n for (const entry of queue) entry.resolve({ data: null, error })\n return\n }\n parsed = await response.json()\n } catch (error) {\n const message = error?.name === 'AbortError' ? 'aborted' : (error?.message || String(error))\n // ⛔ AN ABORT KEEPS WHAT ARRIVED. On an exhaustive walk every in-flight key\n // fails at once here, so discarding held pages loses the whole corpus rather\n // than one key — the \"rejection that loses every key\" a caller cannot have.\n for (const entry of queue) {\n const held = Array.isArray(entry.collected) && entry.collected.length ? entry.collected : null\n entry.resolve(held\n ? { data: held, error: message, meta: withMeta({ partial: true, pages: entry.page }) }\n : { data: null, error: message })\n }\n return\n }\n const data = parsed && typeof parsed.data === 'object' && parsed.data ? parsed.data : {}\n // ⭐ `whole[key]` is present only for keys delivered as WHOLE entities — so a\n // key's ABSENCE is the brief, and a key present that never asked is the\n // brief-less Model saying so. Absent entirely when every key is briefs.\n const wholes = parsed && typeof parsed.whole === 'object' && parsed.whole ? parsed.whole : {}\n const cursors = parsed && typeof parsed.cursors === 'object' && parsed.cursors ? parsed.cursors : {}\n // ⭐ `partial[key]` — `{ source, code, detail }`: a source of that key did not answer,\n // and the key carries what the others returned. Unread, a degraded key looked complete.\n const degraded = parsed && typeof parsed.partial === 'object' && parsed.partial ? parsed.partial : {}\n queue.forEach((entry, i) => {\n const key = keys[i]\n // ⛔ A FAILURE MUST NOT DISCARD PAGES ALREADY COLLECTED. An exhaustive walk\n // that fails on page 7 has six pages in hand, and a caller that asked for a\n // corpus would rather have them marked partial than lose the key — losing it\n // is indistinguishable from \"this query has no records\".\n const held = Array.isArray(entry.collected) && entry.collected.length ? entry.collected : null\n if (!(key in data)) {\n const error = `the records service answered without the key \"${key}\"`\n entry.resolve(held\n ? { data: held, error, meta: withMeta({ partial: true, pages: entry.page }) }\n : { data: null, error })\n return\n }\n const whole = typeof wholes[key] === 'boolean'\n ? wholes[key]\n : (typeof entry.request.whole === 'boolean' ? entry.request.whole : undefined)\n\n const cursor = typeof cursors[key] === 'string' && cursors[key] ? cursors[key] : null\n const rows = Array.isArray(data[key]) ? data[key] : data[key]\n\n // An exhaustive caller collects the page and asks for the next one.\n if (cursor && entry.request.exhaustive && Array.isArray(rows)) {\n const acc = entry.collected ? entry.collected.concat(rows) : rows.slice()\n const page = (entry.page || 1) + 1\n const cap = typeof entry.request.maxPages === 'number' && entry.request.maxPages > 0\n ? entry.request.maxPages\n : DEFAULT_MAX_PAGES\n if (page <= cap) {\n pending.set(entry, { cursor, collected: acc, page, whole })\n return\n }\n // The caller's own bound, not the service's: report rather than spin.\n entry.resolve({ data: acc, meta: withMeta({ whole, partial: true, pages: cap }) })\n return\n }\n\n const collected = entry.collected ? entry.collected.concat(Array.isArray(rows) ? rows : []) : rows\n const meta = withMeta({\n whole,\n // ⭐ ONE FLAG, MEANING **NOT THE WHOLE POPULATION** — asked for by name, so\n // a caller has one boolean to branch on rather than several signals to\n // combine. It is set in every case that means it:\n // · a cursor came back and this caller does not page (a page render);\n // · a cursor came back and the caller's `maxPages` stopped the walk;\n // · the walk failed or aborted with pages already in hand;\n // · a source of the key did not answer (`partial[key]`), and `unavailable`\n // carries the service's own account of which one.\n // ⛔ Named `truncated` when it shipped in 0.17.0; renamed the same day, before\n // any consumer adopted it, because \"truncated\" says something was cut and this\n // also means \"there is more you did not ask for\".\n partial: (cursor || key in degraded) ? true : undefined,\n unavailable: key in degraded ? degraded[key] : undefined,\n pages: entry.page && entry.page > 1 ? entry.page : undefined,\n })\n entry.resolve(meta ? { data: collected, meta } : { data: collected })\n })\n\n if (pending.size === 0) return\n // Re-ask each unfinished key on its own — the cursor is per key, so a batch\n // would have to correlate several independent positions through one body.\n // ⭐ The cursor rides inside `narrow` and NOTHING ELSE in the question changes: the\n // service resumes only the question a cursor came from, and answers a cursor sent\n // with anything else changed with a `400`.\n await Promise.all([...pending].map(([entry, state]) => {\n const next = {\n ...entry,\n request: { ...entry.request, narrow: { ...(entry.request.narrow || {}), cursor: state.cursor } },\n collected: state.collected,\n page: state.page,\n }\n return flushAsked(url, [next], doFetch)\n }))\n}\n\n/**\n * Read one response and parse its body — the part of a fetch every view of an\n * address shares. A JSON body is parsed as JSON; anything else is tried as JSON\n * and kept as text when it is not. A failed response keeps its text, so a caller\n * can read a message out of it (the records service's problem details).\n *\n * ⛔ A WHOLE HTML DOCUMENT IS NOT DATA — it is a failed read. A host that serves a site\n * answers an address it holds no file for with the site's page, a `200` of `text/html`,\n * and kept as text that page reached a component as a query's records: the component\n * called `.map` on it and the page fell to its error boundary. Measured 2026-09-26 on a\n * site a local backend serves, `/es/data/team.json` answering with the page. A fragment of\n * HTML is still kept as text; only a document — `<!doctype html>` or `<html>` — is refused.\n */\nasync function readResponse(doFetch, target, init) {\n const response = await doFetch(target, init)\n if (!response.ok) {\n let text = null\n try {\n text = await response.text()\n } catch {\n // Body not readable — the status line is all there is.\n }\n return { ok: false, status: response.status, statusText: response.statusText, text }\n }\n const contentType = response.headers.get('content-type') || ''\n if (contentType.includes('application/json')) return { ok: true, body: await response.json() }\n const text = await response.text()\n try {\n return { ok: true, body: JSON.parse(text) }\n } catch {\n if (HTML_DOCUMENT.test(text)) {\n return { ok: false, status: response.status, statusText: 'an HTML page, not data', text: null }\n }\n return { ok: true, body: text }\n }\n}\n\nconst HTML_DOCUMENT = /^\\s*(?:<!doctype\\s+html|<html[\\s>])/i\n\n/** Build a `meta` from the fields that are actually present, or `undefined`. */\nfunction withMeta(fields) {\n const out = {}\n for (const [k, v] of Object.entries(fields)) if (v !== undefined) out[k] = v\n return Object.keys(out).length ? out : undefined\n}\n\n/**\n * The locale a list's texts are collated in: the page's (ruled 2026-09-14 [Diego]).\n * A request asked in a locale carries it; otherwise the caller's — the website's\n * active locale in the browser, the locale a host's prerender renders.\n */\nfunction sortLocale(request, ctx) {\n if (typeof request?.locale === 'string' && request.locale) return request.locale\n if (typeof ctx?.locale === 'string' && ctx.locale) return ctx.locale\n const active = ctx?.website?.getActiveLocale?.()\n return typeof active === 'string' && active ? active : null\n}\n\n/**\n * Evaluate the query over what the source returned — the ONE evaluator,\n * `@uniweb/core`'s `evaluateQuery`, so the browser orders and filters exactly as\n * the build does: the query's set first (`scope` over the branch a compiled record is\n * held with, `where`, `sort`, `limit`), then the fetch's `narrow` of it. What comes back\n * carries no branch — `evaluateQuery` drops it, so no component receives one. Only the\n * sort is this lane's own, for what it does with a bad `sort:` in production.\n */\nfunction applyOperators(data, request, { dev = false, locale = null } = {}) {\n return evaluateQuery(data, request, {\n locale,\n sort: (items, sortExpr, options) => applySort(items, sortExpr, dev, options.locale),\n })\n}\n\n/**\n * ⛔ This was a second sort implementation until 2026-09-04, and it honoured\n * a comma-separated MULTI-KEY sort the language does not have (single-key by\n * ruling). A bad `sort:` is an authoring error: dev throws so it is seen; in\n * production the records are delivered in source order and the reason is\n * logged once — a wrong order is not worth a broken page for a visitor.\n */\nconst warnedBadSorts = new Set()\nfunction applySort(items, sortExpr, dev, locale) {\n try {\n return sortRecords(items, sortExpr, { locale })\n } catch (err) {\n if (dev) throw err\n const key = String(sortExpr)\n if (!warnedBadSorts.has(key)) {\n warnedBadSorts.add(key)\n console.error(`[default-fetcher] ${err.message} Records delivered unsorted.`)\n }\n return items\n }\n}\n\n/**\n * Walk a dotted path into an object. Missing segments short-circuit to\n * `undefined` so callers can distinguish \"present and empty\" from \"not there.\"\n */\nfunction getNestedValue(obj, path) {\n if (!obj || !path) return obj\n let current = obj\n for (const part of path.split('.')) {\n if (current === null || current === undefined) return undefined\n current = current[part]\n }\n return current\n}\n\n/**\n * JSON.parse that returns `undefined` on failure instead of throwing.\n * Used when we want to probe a response body for an error path but don't\n * want a non-JSON body to surface as a parser exception.\n */\nfunction safeParseJSON(text) {\n try {\n return JSON.parse(text)\n } catch {\n return undefined\n }\n}\n","/**\n * THE RENDER'S DATA STEP — the page a visit starts on, fetched by the rule that renders it.\n *\n * ⭐ **Why a step at all.** The server render is synchronous: while it renders, a section reads its\n * data from the cache and never waits for a request (`EntityStore.resolve`). So whatever a page\n * shows has to be there before the render starts. That is this, and nothing else: it is not an\n * optimization, it is how a server-rendered page has content.\n *\n * ⭐ **Why it runs the render's own rule.** It asks, block by block, exactly what the render will\n * look for — `EntityStore.fetch`, the same plan `resolve` reads back (`@uniweb/core/page-data`) —\n * through a dispatcher built from the site's own foundation, so a transport the site chose answers\n * here too. ⛔ Until 2026-09-19 a host's prefetch worked this out from the payload alone: it could\n * not know which keys a component declares, which layout the page draws, or which transport a key\n * is routed to, so it asked for every fetch on every level and still got four cases wrong\n * (`kb/framework/plans/what-a-page-needs.md` §0). A payload cannot answer those; the graph the\n * isolate is about to render with can.\n *\n * ⚖️ **What it still over-asks.** The blocks are the page's — body, the layout areas it uses, their\n * child blocks and insets. Whether a component actually draws its children, or a layout its area,\n * is known only by rendering; a block that declares keys and is never drawn is asked for anyway.\n * Bounded by what such a block declares, and cheaper than rendering the page twice.\n *\n * @module\n */\nimport DataStore, { deriveCacheKey } from '@uniweb/core/datastore'\nimport { resolvePage } from './resolve-page.js'\nimport { getComponentMeta } from './prepare-props.js'\nimport { createDefaultFetcher } from './default-fetcher.js'\n\n/**\n * Every block the page's render can reach: its body and the layout areas it uses (the runtime's own\n * layout choice — the page's `layout:`, else the foundation's default, else the site's default set,\n * minus any area the page hides), plus what those blocks hold.\n */\nfunction blocksOf(page) {\n const out = []\n const walk = (list) => {\n for (const block of list || []) {\n if (!block) continue\n out.push(block)\n walk(block.childBlocks)\n walk(block.insets)\n }\n }\n walk(page.getBodyBlocks?.() ?? page.bodyBlocks)\n for (const area of Object.values(page.getLayoutAreas?.() ?? {})) walk(area)\n return out\n}\n\n/**\n * Fetch what one route's render will read.\n *\n * @param {Object} options\n * @param {Object} options.website - the initialized Website the render will use (`initPrerender`)\n * @param {string|Object} options.route - the route, or an already-resolved Page\n * @param {Function} options.fetch - the transport, per request\n * @param {boolean} [options.dev]\n * @param {string|null} [options.locale] - ⛔ NOT a locale to fetch in: the Website is already\n * locale-sliced (`initPrerenderForLocale`) and every address is resolved in ITS locale. Passing\n * one that disagrees throws, because the alternative is a page prerendered in one language with\n * the data of another, and nothing to see it.\n * @param {'always'|'author'} [options.prerender] - `'always'` asks for everything, which is what an\n * isolate rendering a visit's first page wants; `'author'` honours `prerender: false`.\n * @param {Object} [options.cache] - a `DataStore` to answer from and fill, shared across calls.\n * ⭐ For a caller that runs the step for MANY pages of one site — a static build — where every\n * page asks for the site's and its parent's data and reading each address once is the difference\n * between N reads and N×M. ⛔ Not for a host serving requests: there a page's data is asked\n * fresh, and the default (a cache per call) is what does that. A shared cache changes nothing\n * about what is ASKED or returned — every request is still recorded, hit or miss.\n * @returns {Promise<Array<{ config: Object, outcome: 'fetched'|'failed'|'skipped', data: *, meta?: Object, error?: string }>>}\n * one entry per request the render will make, keyed downstream by `deriveCacheKey(config)`\n */\nexport async function loadPageData({ website, route, fetch, dev = false, locale = null, prerender = 'always', cache = null }) {\n if (prerender !== 'author' && prerender !== 'always') {\n throw new Error(`loadPageData: prerender must be 'author' or 'always', got ${JSON.stringify(prerender)}`)\n }\n const active = website?.getActiveLocale?.() ?? null\n if (locale && active && locale !== active) {\n throw new Error(\n `loadPageData: this Website renders ${JSON.stringify(active)} and the caller asked for ` +\n `${JSON.stringify(locale)}. A Website is locale-sliced — build one per locale ` +\n '(`initPrerenderForLocale`) rather than asking this step for another language.'\n )\n }\n const resolved = typeof route === 'string' ? resolvePage(website, route) : route\n // Nothing matched: a genuine 404, and the caller's to turn into one. Nothing to fetch.\n if (!resolved) return []\n // ⭐ The same promotion the render applies: a content-less folder renders its designated child,\n // and it is that child's data the page needs.\n const page = typeof resolved.getRenderableSelf === 'function' ? resolved.getRenderableSelf() : resolved\n\n // ⛔ A real Page, not page data. `createPageRenderer` tolerates plain data because rendering it\n // still produces a page; here the same tolerance would walk raw sections, find no blocks and no\n // graph behind them, and fetch NOTHING — a clean run, an empty page, no error anywhere. That is\n // the failure this whole step exists to end, so it is refused.\n if (typeof page.getBodyBlocks !== 'function') {\n throw new Error('loadPageData: `route` must be a route or a Page from this Website — page data is not enough to say what its render reads.')\n }\n\n // A cache of this call's own unless the caller shares one: the graph's belongs to whatever it is\n // rendering, and a page's data is asked fresh per request. The site's transports still apply\n // (`dispatcherFor`).\n const dispatcher = website.dispatcherFor({\n dataStore: cache ?? new DataStore(),\n defaultFetcher: createDefaultFetcher({ basePath: website.config?.base || '', dev, fetch }),\n })\n\n /** One entry per request, in the order they were first asked for. */\n const entries = new Map()\n const record = (request, entry) => {\n const key = deriveCacheKey(request)\n if (!entries.has(key)) entries.set(key, entry)\n }\n\n const recording = {\n peek: (request, ctx) => dispatcher.peek(request, ctx),\n peekRecord: (id) => dispatcher.peekRecord(id),\n dispatch: async (request, ctx) => {\n if (prerender === 'author' && request?.prerender === false) {\n // The author deferred this one to the browser and the caller honours that. Present, so a\n // host can count what was declared against what was tried; not fetched, not hydrated.\n record(request, { config: request, outcome: 'skipped', data: null })\n return { data: null }\n }\n const result = await dispatcher.dispatch(request, ctx)\n record(request, result?.error\n ? { config: request, outcome: 'failed', data: null, error: result.error }\n : { config: request, outcome: 'fetched', data: result?.data ?? null, ...(result?.meta ? { meta: result.meta } : {}) })\n return result\n },\n }\n\n const store = website.entityStore\n // ⭐ Every block's first request goes out in the same tick, which is what lets a question door\n // answer a page in one POST. A step that depends on an answer takes a later one, and only its\n // own key waits.\n await Promise.all([\n // The page's own record: asked whether or not a section declares the route key, because the\n // page names itself from it and a URL naming no record is a 404.\n store.fetchPageRecord(page, { dispatcher: recording }),\n ...blocksOf(page).map((block) => store.fetch(block, getComponentMeta(block.type), { dispatcher: recording })),\n ])\n\n return [...entries.values()]\n}\n","/**\n * The composed render entry — resolve a route, render it, inject it into a shell.\n *\n * ⭐ **This exists because framework had two callers of one unshared sequence, not\n * because a consumer asked.** `@uniweb/runtime/ssr` exported every step of the\n * per-page render and never the sequence, so each host assembled it:\n *\n * - `@uniweb/build`'s `prerender.js` — `renderPage` → classify → `injectPageContent`,\n * once per page in its loop.\n * - an SSR isolate prerendering the page a visit starts on (the SPA renders every\n * page after it) — `resolvePage` → `renderPage` → `injectPageContent`, and it wrote\n * the route lookup three times in three files before `resolvePage` was exported at\n * all (see that function's header).\n *\n * Two unlike callers is what makes the interface honest: a build bakes files and an\n * isolate answers a request, so anything only one of them needs stayed out.\n *\n * ⛔ WHAT IS DELIBERATELY NOT IN HERE, and the boundary is the point:\n *\n * - **Shell assembly.** The shell arrives built. The import map, the CDN base and\n * cache headers are host layout, and a runtime that assembled them would be\n * modelling a deployment it cannot see (the host drew this line itself,\n * 2026-09-03: a serve location is read from the payload, never constructed here).\n * - **Init and hydration.** The two lanes differ REALLY here, not incidentally: a\n * build initializes once and hydrates every collection up front, an isolate\n * initializes per locale and prefetches per route. Folding either in would fit\n * one caller and lie to the other. `initPrerender` / `initPrerenderForLocale` /\n * `prefetchPageData` / `hydrateDataStore` stay separate exports, and\n * `prefetchAndHydrate` below is the isolate's two-step, not a general one.\n * - **Anything build-only.** `injectBuildData` stays in the build lane; it is the\n * other half of the head seam and has its own parity guard.\n */\nimport { resolvePage, renderPage, classifyRenderError, injectPageContent } from './ssr-renderer.js'\nimport { hydrateDataStore } from './wire-foundation.js'\nimport { loadPageData } from './load-page-data.js'\n\n/**\n * A renderer bound to one initialized Website and one shell.\n *\n * Create it once per locale (the Website is already locale-sliced by\n * `initPrerenderForLocale`) and call `render` per route or per page.\n *\n * @param {Object} opts\n * @param {Object} opts.website - `uniweb.activeWebsite`, already initialized\n * @param {string} opts.shell - the HTML shell to inject into, taken as given\n * @returns {{ website: Object, render: Function }}\n */\nexport function createPageRenderer({ website, shell }) {\n if (!website) throw new Error('createPageRenderer: `website` is required')\n if (typeof shell !== 'string') throw new Error('createPageRenderer: `shell` must be an HTML string')\n\n /**\n * Render one page.\n *\n * ⭐ Returns an OUTCOME rather than throwing or returning a bare string, because\n * the two callers branch differently on the same three cases and neither wants an\n * exception: a build logs and keeps going so one broken section cannot fail a whole\n * site, an isolate decides a status code and a cache policy. Same reasoning as\n * `prefetchPageData`'s per-entry outcome.\n *\n * @param {string|Object} target - a route (`/blog/1`, resolved through the same\n * matcher the browser uses, so a dynamic route works) or an already-resolved\n * Page, which the build lane already holds from its own loop.\n * @param {Object} [options]\n * @param {Object} [options.inject] - extra options forwarded to `injectPageContent`\n * @returns {{ outcome: 'rendered'|'notFound'|'failed', html: string|null,\n * page: Object|null, error: {type: string, message: string}|null }}\n * `notFound` means the URL names nothing, and `html` says whether there is a page to\n * send for it:\n * - `page: null`, `html: null` — no route matched. Send your own not-found page.\n * - `page` set, `html` set — the page's record does not exist, and `html` is the page\n * rendered in its own not-found state. Send it, with your not-found status.\n * - `page` set, `html: null`, `error` set — that not-found state broke while rendering.\n * Send your own not-found page; `error` says what broke.\n */\n function render(target, { inject = {} } = {}) {\n const resolved = typeof target === 'string' ? resolvePage(website, target) : target\n\n // ⛔ Not an error: nothing matched, which is a genuine 404 and the caller's to\n // turn into one — a build skips it, an isolate serves its 404 page with a 404\n // status. Returning `failed` here would make those indistinguishable.\n if (!resolved) return { outcome: 'notFound', html: null, page: null, error: null }\n\n // ⭐ **THE INDEX-CHILD PROMOTION APPLIES TO BOTH TARGET FORMS** — ruled\n // 2026-09-12 [Diego]. A content-less folder with a designated `isIndex` child\n // renders that child. `Website#getPage` already did this for a ROUTE on its\n // exact-match branches, and nothing did it for an already-resolved Page, so the\n // same page rendered differently depending on which form the caller happened to\n // hold — silently, with no error on either path. `getRenderableSelf` is the one\n // rule for it and now has exactly one caller: here.\n // ⚠️ Guarded on the method rather than assumed: the documented target is a Page,\n // but a caller handing in plain page data should degrade to \"render what I gave\n // you\", not throw.\n const page = typeof resolved.getRenderableSelf === 'function' ? resolved.getRenderableSelf() : resolved\n\n // ⭐ A PAGE CAN RESOLVE AND STILL NAME NOTHING: a parametric page whose record does\n // not exist. Core marks it `notFound` only when the data answered definitively\n // (`Website#_createDynamicPage`), never on a failed or missing fetch. It renders the\n // page's own not-found state — a record miss is not a route miss — and the outcome\n // says `notFound` all the same, so a caller keying its status on the outcome sends a\n // not-found response without reading anything new. Known before rendering, so it\n // holds even when that render breaks. ⛔ Until 2026-09-30 this was `rendered`, and a\n // host that answered by the outcome sent 200 for a URL naming no record.\n const missing = page?.notFound === true\n\n let result\n try {\n result = renderPage(page, website)\n } catch (err) {\n // `renderPage` handles its own errors, but a foundation can throw from\n // module scope in ways it does not catch. Classify rather than propagate,\n // so one page cannot take down a build loop or an isolate's request.\n return { outcome: missing ? 'notFound' : 'failed', html: null, page, error: classifyRenderError(err) }\n }\n\n if (result.error) return { outcome: missing ? 'notFound' : 'failed', html: null, page, error: result.error }\n\n // ⛔ `sectionOverrideCSS` LAST, so a caller's `inject` cannot displace it. It is\n // computed by `renderPage` for this page — theme pinning and component vars —\n // and a caller passing a same-named key would silently drop it, rendering a page\n // that looks fine and is unstyled in exactly the places the author pinned. That\n // is the empty-success shape this module keeps refusing elsewhere; the spread was\n // the other way round for one commit.\n const html = injectPageContent(shell, result.renderedContent, page, {\n ...inject,\n sectionOverrideCSS: result.sectionOverrideCSS,\n })\n return { outcome: missing ? 'notFound' : 'rendered', html, page, error: null }\n }\n\n return { website, render }\n}\n\n/**\n * Prefetch a route's data and hydrate it onto the graph — the isolate's two-step.\n *\n * ⭐ Its whole purpose is that a host stops assembling our structure by hand. It\n * returns the prefetch outcomes rather than swallowing them, because a host reads\n * them to tell \"nothing was tried\" from \"everything tried failed\", which is a\n * different cache decision.\n *\n * ⭐ **Since 2026-09-19 it asks what the RENDER will read, block by block** (`loadPageData`),\n * on the Website it is handed: the keys each component declares, the layout the page actually\n * draws, and the transport the site chose for a key. `content` is no longer read — the graph\n * carries everything — and stays in the signature for callers that pass it.\n * ⛔ `prefetchPageData` works from the payload alone and cannot know any of those; it asks for\n * every fetch on every level instead, and still gets four cases wrong\n * (`kb/framework/plans/what-a-page-needs.md` §0). Prefer this entry.\n *\n * ⛔ The build lane does NOT call this: it hydrates every collection once, before\n * its page loop, from its own executor that honours the author's `prerender:` flag.\n * That difference is why this is a named isolate helper and not a step inside\n * `render`.\n *\n * @returns {Promise<Array<{config: Object, outcome: string, data: any, error?: string}>>}\n */\nexport async function prefetchAndHydrate({ website, content, route, locale = null, fetch = null, dev = false, prerender = 'always' }) {\n // ⛔ Guarded for the same reason `createPageRenderer` is, and it was not for one\n // commit. `hydrateDataStore` no-ops on a graph with no `dataStore`, so a caller\n // passing the wrong object gets a successful-looking prefetch, an unhydrated graph\n // and a page that renders empty — no error anywhere. Fail where the mistake is.\n if (!website?.dataStore) {\n throw new Error('prefetchAndHydrate: `website` must be an initialized Website with a dataStore')\n }\n\n // ⛔ THE TRANSPORT IS REQUIRED HERE, unlike on `prefetchPageData`, and this is the\n // one place the difference matters.\n //\n // A function does NOT survive every isolate boundary. Measured by a host against\n // a real isolate loader, 2026-09-03: passed through an\n // entrypoint's `fetch(Request)` with a JSON body the transport arrives\n // **`undefined`**; passed as an argument to an RPC method it arrives as a callable\n // function and the isolate invokes it. Only the RPC shape carries it.\n //\n // ⚠️ And `undefined` is not where it stops, which is the part their measurement\n // could not see from outside our code. `createDefaultFetcher` resolves the\n // transport as `fetchImpl || globalThis.fetch`, so a transport that failed to cross\n // silently becomes THE ISOLATE'S OWN NETWORK — outside the host's timeout, byte\n // budget and site-relative address resolution. With an absolute address it does not\n // even fail: the request goes out from the wrong place and comes back\n // `outcome: 'fetched'`. A wiring mistake wearing a success.\n //\n // ⇒ So the entry whose only caller crosses that boundary demands a real function\n // rather than defaulting. A Node or browser caller that genuinely wants the global\n // passes `fetch: globalThis.fetch` — one word, and it says so.\n if (typeof fetch !== 'function') {\n throw new Error(\n 'prefetchAndHydrate: `fetch` must be a function. A transport does not survive a ' +\n 'JSON-serialized isolate boundary — pass it as an RPC method argument. ' +\n 'To use the ambient fetch deliberately, pass `fetch: globalThis.fetch`.'\n )\n }\n // ⛔ `locale` is forwarded to be CHECKED, not applied: the Website is locale-sliced already, so a\n // caller whose two disagree gets an error rather than a page prerendered in one language with the\n // data of another.\n const fetched = await loadPageData({ website, route, fetch, dev, locale, prerender })\n hydrateDataStore(website, fetched)\n return fetched\n}\n","/**\n * Every record a site's declared queries return — for a host building something\n * over the whole corpus rather than rendering one page.\n *\n * ## Why this is framework's and not the caller's\n *\n * A host that indexes a site has to ask the same questions the site's pages ask.\n * ⛔ **If it composes its own, the index and the page can disagree**, and the way\n * they disagree is the worst one available: the index offers a result whose page\n * then renders empty. That is the two-answers-to-one-question failure the GET\n * lane was retired for, reintroduced one lane over.\n *\n * ⇒ So the composition is not duplicated here either. This walks the site's\n * `config.queries`, hands each one to `resolveFetchConfigs` — **the same rule a\n * page render uses**, applying each saved query's own `scope` and `where` — and\n * asks through the same client.\n *\n * ## ⭐ Each query is asked AS SAVED — its `limit` included\n *\n * A query defines its SET: the records its `scope` and `where` select, in its\n * `sort`, up to its `limit` (ruled 2026-09-14 [Diego]). A parametric page has a URL\n * for each record of its route query's set and no other, so the set is exactly the\n * population a site's pages reach — a field note older than a query's 100 most recent\n * has no page to index. Asked with no `narrow`, the service answers the set whole in\n * one answer, so a walk has nothing to page; it still follows a cursor if one ever\n * comes back, bounded by `maxPages`.\n *\n * ⛔ **Until 2026-09-14 this dropped the query's `limit`**, on the rule it replaced — a\n * `limit` was the list page's presentation and every record matching `scope` + `where`\n * had a page — which was right under that rule. ⚠️ And that rule's own history: the\n * first version, in 0.17.0, passed `limit` through while claiming the corpus was the\n * population detail pages reach, and the consumer found it (2026-09-06).\n *\n * ## What the caller supplies\n *\n * ⭐ **The transport, and nothing else.** A host outside a browser decides how a\n * site-relative address is dispatched — through its own origin, a service\n * binding, a forwarding table that turns `/_…` into an upstream — and hands that\n * in as `fetch`. **Framework decides what to ask and how to read the answer;\n * the host decides how the bytes travel.** That split is the whole of the\n * boundary here.\n *\n * ⛔ **A site with no records service is not an error.** It has no live records,\n * so this returns empty rather than throwing: a static site's corpus comes from\n * its own compiled artifacts, which the caller already has.\n */\n\nimport { resolveFetchConfigs } from '@uniweb/core/fetch-config'\nimport { resolveRecordsService } from '@uniweb/core/records-service'\nimport { createDefaultFetcher } from './default-fetcher.js'\n\n/**\n * Ask a site's records service for every record its declared queries return.\n *\n * @param {Object} content - the render payload (`config.services`, `config.queries`)\n * @param {Object} options\n * @param {string} options.locale - the locale to ask in; it is a route segment\n * of the service's address, so there is no asking without one\n * @param {Function} options.fetch - the transport, `(url, init) => Response`\n * @param {AbortSignal} [options.signal]\n * @param {string[]} [options.only] - restrict to these query names\n * @param {boolean} [options.whole=false] - what to ask for. `false` is the brief a\n * list shows; **`true` — whole records — is what an index wants**: a brief index\n * cannot match body text the record's own detail page displays, and a reader who\n * finds a word on the page and not in search meets the inconsistency two\n * rankings would produce. The cost is the caller's, and bounded by each query's\n * own `limit`. An external query is never collected — it has no records service\n * to ask.\n * @param {number} [options.maxPages] - the caller's own bound on a walk that pages. A\n * question with no `narrow` is answered whole with no cursor, so today's walk is one\n * request per query; the bound holds should a service page anyway.\n * @returns {Promise<{records: Object, errors: Object|null, meta: Object}>}\n * `records` is keyed by query NAME, each a flat array; `errors` is keyed the\n * same and is null when nothing failed; `meta[name]`, when there is anything to\n * say, carries `{ whole?, pages?, partial?, unavailable? }`.\n *\n * ⭐ **`partial` means NOT THE WHOLE POPULATION**, and a key can be in BOTH\n * `records` and `errors`: a walk that failed or was aborted with pages already\n * in hand keeps them, marked. ⛔ Losing them would be indistinguishable from\n * \"this query has no records\", and on an abort every in-flight key fails at\n * once — so discarding would lose the corpus, not a key.\n */\nexport async function collectSiteRecords(\n content,\n { locale, fetch, signal, only = null, whole = false, maxPages } = {},\n) {\n const config = content?.config\n const services = config?.services ?? null\n const queries = config?.queries ?? null\n\n // No service, no queries, or no locale to ask in ⇒ nothing to collect. Each is\n // an ordinary state of a site, not a fault: the caller reads its own artifacts.\n if (!queries || typeof queries !== 'object') return empty()\n if (!resolveRecordsService(services, locale)) return empty()\n\n const names = Object.keys(queries).filter((n) => (only ? only.includes(n) : true))\n if (names.length === 0) return empty()\n\n // One synthetic source per query — `as` is the query's own name, so the answer\n // comes back under the name the caller asked by.\n const configs = resolveFetchConfigs(\n names.map((name) => ({ query: name, as: name })),\n { services, queries, locale, defaultLocale: config?.defaultLanguage ?? locale },\n )\n\n const fetcher = createDefaultFetcher({ fetch, basePath: config?.base ?? '' })\n const records = {}\n const errors = {}\n const meta = {}\n\n await Promise.all([...configs].map(async ([name, cfg]) => {\n // ⛔ Only a config the service answers. A query that resolved to a compiled\n // `path` has no live lane, and reading that file is the caller's business,\n // not ours — it is in the site's own URL space and they already serve it.\n if (!cfg.ask) return\n // The query as saved — the set its pages reach, its `limit` included; see the header.\n const asked = { ...cfg, whole, exhaustive: true }\n if (typeof maxPages === 'number' && maxPages > 0) asked.maxPages = maxPages\n\n const result = await fetcher.resolve(asked, { signal })\n if (result?.error) errors[name] = result.error\n // ⭐ Data and an error are not exclusive: a partial walk reports both, and\n // the caller decides whether partial is usable. Only a walk that collected\n // nothing leaves the key out of `records` entirely.\n if (Array.isArray(result?.data)) records[name] = result.data\n else if (!result?.error) records[name] = []\n if (result?.meta) meta[name] = result.meta\n }))\n\n return { records, errors: Object.keys(errors).length ? errors : null, meta }\n}\n\nfunction empty() {\n return { records: {}, errors: null, meta: {} }\n}\n"],"names":["text","fetch","deriveCacheKey"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/runtime",
3
- "version": "0.28.3",
3
+ "version": "0.28.5",
4
4
  "description": "Minimal runtime for loading Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,7 +44,7 @@
44
44
  "esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0",
45
45
  "vite": "^7.3.1",
46
46
  "vitest": "^4.1.7",
47
- "@uniweb/build": "0.71.0"
47
+ "@uniweb/build": "0.74.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "react": "^19.0.0",
@@ -65,6 +65,13 @@ export function createPageRenderer({ website, shell }) {
65
65
  * @param {Object} [options.inject] - extra options forwarded to `injectPageContent`
66
66
  * @returns {{ outcome: 'rendered'|'notFound'|'failed', html: string|null,
67
67
  * page: Object|null, error: {type: string, message: string}|null }}
68
+ * `notFound` means the URL names nothing, and `html` says whether there is a page to
69
+ * send for it:
70
+ * - `page: null`, `html: null` — no route matched. Send your own not-found page.
71
+ * - `page` set, `html` set — the page's record does not exist, and `html` is the page
72
+ * rendered in its own not-found state. Send it, with your not-found status.
73
+ * - `page` set, `html: null`, `error` set — that not-found state broke while rendering.
74
+ * Send your own not-found page; `error` says what broke.
68
75
  */
69
76
  function render(target, { inject = {} } = {}) {
70
77
  const resolved = typeof target === 'string' ? resolvePage(website, target) : target
@@ -86,6 +93,16 @@ export function createPageRenderer({ website, shell }) {
86
93
  // you", not throw.
87
94
  const page = typeof resolved.getRenderableSelf === 'function' ? resolved.getRenderableSelf() : resolved
88
95
 
96
+ // ⭐ A PAGE CAN RESOLVE AND STILL NAME NOTHING: a parametric page whose record does
97
+ // not exist. Core marks it `notFound` only when the data answered definitively
98
+ // (`Website#_createDynamicPage`), never on a failed or missing fetch. It renders the
99
+ // page's own not-found state — a record miss is not a route miss — and the outcome
100
+ // says `notFound` all the same, so a caller keying its status on the outcome sends a
101
+ // not-found response without reading anything new. Known before rendering, so it
102
+ // holds even when that render breaks. ⛔ Until 2026-09-30 this was `rendered`, and a
103
+ // host that answered by the outcome sent 200 for a URL naming no record.
104
+ const missing = page?.notFound === true
105
+
89
106
  let result
90
107
  try {
91
108
  result = renderPage(page, website)
@@ -93,10 +110,10 @@ export function createPageRenderer({ website, shell }) {
93
110
  // `renderPage` handles its own errors, but a foundation can throw from
94
111
  // module scope in ways it does not catch. Classify rather than propagate,
95
112
  // so one page cannot take down a build loop or an isolate's request.
96
- return { outcome: 'failed', html: null, page, error: classifyRenderError(err) }
113
+ return { outcome: missing ? 'notFound' : 'failed', html: null, page, error: classifyRenderError(err) }
97
114
  }
98
115
 
99
- if (result.error) return { outcome: 'failed', html: null, page, error: result.error }
116
+ if (result.error) return { outcome: missing ? 'notFound' : 'failed', html: null, page, error: result.error }
100
117
 
101
118
  // ⛔ `sectionOverrideCSS` LAST, so a caller's `inject` cannot displace it. It is
102
119
  // computed by `renderPage` for this page — theme pinning and component vars —
@@ -108,7 +125,7 @@ export function createPageRenderer({ website, shell }) {
108
125
  ...inject,
109
126
  sectionOverrideCSS: result.sectionOverrideCSS,
110
127
  })
111
- return { outcome: 'rendered', html, page, error: null }
128
+ return { outcome: missing ? 'notFound' : 'rendered', html, page, error: null }
112
129
  }
113
130
 
114
131
  return { website, render }
@@ -28,6 +28,10 @@ function guaranteeItemStructure(item) {
28
28
  lists: item.lists || [],
29
29
  icons: item.icons || [],
30
30
  videos: item.videos || [],
31
+ // ⛔ An entry's own insets were left out until 2026-09-29, as `tables` was: the
32
+ // parser delivered them and this object, built key by key, did not carry them.
33
+ insets: item.insets || [],
34
+ media: item.media || [],
31
35
  snippets: item.snippets || [],
32
36
  buttons: item.buttons || [],
33
37
  data: item.data || {},
@@ -37,6 +41,7 @@ function guaranteeItemStructure(item) {
37
41
  quotes: item.quotes || [],
38
42
  headings: item.headings || [],
39
43
  ...(item.math && item.math.length ? { math: item.math } : {}),
44
+ ...(item.tables && item.tables.length ? { tables: item.tables } : {}),
40
45
  }
41
46
  }
42
47
 
@@ -65,6 +70,9 @@ export function guaranteeContentStructure(parsedContent) {
65
70
  icons: content.icons || [],
66
71
  videos: content.videos || [],
67
72
  insets: content.insets || [],
73
+ // The section's visual media — images, videos and insets on their own line — in
74
+ // the order written, each with its `kind`: what a `media` slot holds (2026-09-29).
75
+ media: content.media || [],
68
76
  snippets: content.snippets || [],
69
77
  buttons: content.buttons || [],
70
78
  data: content.data || {},
@@ -78,7 +86,12 @@ export function guaranteeContentStructure(parsedContent) {
78
86
  // use them don't pay the allocation cost. Foundations that need them
79
87
  // should check for presence (content.math?.length) or use
80
88
  // content.sequence for in-order rendering.
89
+ //
90
+ // ⛔ `tables` was not passed through until 2026-09-29: the parser delivered it and
91
+ // this object, built key by key, left it out — so a component declaring `tables`
92
+ // received nothing, while `math`, its twin, arrived.
81
93
  ...(content.math && content.math.length ? { math: content.math } : {}),
94
+ ...(content.tables && content.tables.length ? { tables: content.tables } : {}),
82
95
 
83
96
  // Items with guaranteed structure
84
97
  items: (content.items || []).map(guaranteeItemStructure),
@@ -346,8 +359,8 @@ function assembleData(block, meta, entityData) {
346
359
  }
347
360
 
348
361
  /**
349
- * Say once, in dev, that a tagged data block did not reach its section's component — its
350
- * key is not declared. The block is still in `content.sequence`, where a component that
362
+ * Say once, in dev, that a tagged block — a data block or a concept block — did not reach its
363
+ * section's component: its key is not declared. The block is still in `content.sequence`, where a component that
351
364
  * renders the sequence finds it.
352
365
  */
353
366
  const warnedBlocks = new Set()
@@ -355,12 +368,16 @@ function warnUndeclaredBlocks(block, declared) {
355
368
  if (!block.website?.entityStore?.dev) return
356
369
  const keys = new Set(declared.map(([key]) => key))
357
370
  for (const item of block.parsedContent?.sequence || []) {
358
- if (item?.type !== 'dataBlock' || typeof item.tag !== 'string' || keys.has(item.tag)) continue
371
+ // A data block (```yaml:tag) and a concept block (```md:tag) both land under their tag, so
372
+ // both need the key declared. ⛔ Until 2026-09-29 only a data block was reported, and an
373
+ // undeclared concept block left content.data without a word.
374
+ const kind = item?.type === 'dataBlock' ? 'data block' : item?.type === 'concept_block' ? 'concept block' : null
375
+ if (!kind || typeof item.tag !== 'string' || keys.has(item.tag)) continue
359
376
  const memo = `${block.type}::${item.tag}`
360
377
  if (warnedBlocks.has(memo)) continue
361
378
  warnedBlocks.add(memo)
362
379
  console.warn(
363
- `[uniweb] ${block.type}: the \`${item.tag}\` data block is not in content.data — ${block.type} does not ` +
380
+ `[uniweb] ${block.type}: the \`${item.tag}\` ${kind} is not in content.data — ${block.type} does not ` +
364
381
  `declare \`${item.tag}\` in its meta.js \`data:\`. It stays in content.sequence.`
365
382
  )
366
383
  }