@uniweb/runtime 0.12.3 → 0.12.4
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.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr.js","sources":["../src/prepare-props.js","../../core/src/route-match.js","../../core/src/icon-corpus.js","../src/default-404.js","../src/wire-foundation.js","../src/area-wrappers.js","../src/appearance.js","../src/ssr-renderer.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.\n if (Array.isArray(fieldDef.enum)) {\n if (result[field] !== undefined && !fieldDef.enum.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 `childCollection` is set, value may be `{ [childCollection]: [...] }`\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 collectionKey = schema.childCollection\n\n if (collectionKey && value && typeof value === 'object' && !Array.isArray(value)) {\n const arr = Array.isArray(value[collectionKey]) ? value[collectionKey] : []\n return {\n ...value,\n [collectionKey]: 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-066d.\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 * Merge entity data onto a block's parsedContent.data.\n *\n * Section-level data already on the block (from prerender fetches via\n * blockData.parsedContent.data in the Block constructor) takes priority;\n * entity data only fills missing keys. Mutates `block.parsedContent.data`\n * in place so the vanilla JS layer holds the assembled data and\n * subsequent reads see the same shape.\n */\nfunction mergeEntityData(block, entityData) {\n if (!entityData) return\n const current = block.parsedContent.data || {}\n let changed = false\n const merged = { ...current }\n for (const key of Object.keys(entityData)) {\n if (merged[key] === undefined) {\n merged[key] = entityData[key]\n changed = true\n }\n }\n if (changed) {\n block.parsedContent.data = merged\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. Merge entity data (resolved by EntityStore) onto\n * `block.parsedContent.data`.\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 mergeEntityData(block, entityData)\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 * Dynamic route patterns — the ONE home for how `/blog/:id` matches a path.\n *\n * Why this module exists. The rule was implemented twice and the two copies\n * disagreed. `Website#_matchDynamicRoute` built the pattern with `:(\\w+)`;\n * `generate404Html` in `@uniweb/runtime`'s SSR renderer built it with\n * `:[^/]+` and allowed an optional trailing slash. For `:id` they agree, so\n * nothing failed — but for a param name carrying a non-word character\n * (`/blog/:post-id`) the first matched only `post` and left `-id` as a\n * literal, while the second consumed the whole name. Two answers to one\n * question, neither wrong on the routes anyone had tried.\n *\n * That is already bad inside one repo. It is worse across them: a host that\n * renders a page server-side has to decide *which* page a path names, and the\n * runtime then hydrates over that decision in the browser. If the two matchers\n * disagree by a single route, the server renders page A and hydration replaces\n * it with page B — silently, and only on the paths that have a pattern, which\n * are exactly the interesting ones. So this is a cross-boundary contract, not\n * an implementation detail, and it is exported rather than merely shared.\n *\n * Zero-dependency leaf, like `./data-paths.js` and `./locale-config.js`, so a\n * consumer that must not pull core's graph — an edge worker, a build step —\n * can import the subpath `@uniweb/core/route-match` directly.\n *\n * ## The syntax, in full\n *\n * `:param` is the only construct. There are deliberately **no** catch-alls\n * (`*`), **no** optional segments (`?`), and **no** regex constraints — a\n * pattern is not a regular expression, and regex metacharacters in a route are\n * escaped to literals before any substitution happens. Matching is anchored,\n * case-sensitive, and a param captures exactly one non-empty path segment.\n *\n * ## What this module does NOT decide\n *\n * Matching a pattern means *the route exists*. It says nothing about whether\n * the record behind it exists — that is a data question the caller answers\n * later, and a matched pattern with no backing record is a rendered\n * not-found page rather than a route miss. Anything deciding a 404 purely from\n * this module can only answer the first question.\n */\n\n/**\n * Characters allowed in a param NAME — word characters plus the hyphen, so a\n * `[post-id]` route folder round-trips.\n *\n * Deliberately not `[^/]+`: a greedy name would swallow a literal suffix in the\n * same segment, so `/files/:name.json` would capture `name.json` as the param\n * name and leave nothing to match the extension.\n */\nconst PARAM_NAME = '[A-Za-z0-9_-]+'\n\n/** Regex metacharacters that must survive as literals. `-` is not one of them. */\nconst REGEX_SPECIALS = /[.*+?^${}()|[\\]\\\\]/g\n\n/**\n * Normalize a route for comparison: collapse a trailing slash, treat an empty\n * route as the root.\n *\n * `/about/` and `/about` are the same route; `/` stays `/`.\n *\n * @param {string} route\n * @returns {string}\n */\nexport function normalizeRoute(route) {\n if (typeof route !== 'string' || route === '') return '/'\n return route === '/' ? '/' : route.replace(/\\/+$/, '') || '/'\n}\n\n/**\n * Whether a route is a dynamic template rather than a concrete path.\n *\n * @param {string} route\n * @returns {boolean}\n */\nexport function isDynamicRoute(route) {\n return typeof route === 'string' && route.includes(':')\n}\n\n/**\n * Compile a route pattern to an anchored regex plus its param names.\n *\n * Exported for callers that match one pattern against many paths and want to\n * compile once — an edge worker checking every request against a site's\n * patterns, for instance.\n *\n * @param {string} pattern - e.g. `/blog/:id`\n * @returns {{ regex: RegExp, paramNames: string[] }}\n */\nexport function routePatternToRegex(pattern) {\n const paramNames = []\n const source = normalizeRoute(pattern)\n // Escape first: a `.` in a route is a literal `.`, not \"any character\".\n .replace(REGEX_SPECIALS, '\\\\$&')\n // Then each `:name` becomes one non-empty segment capture.\n .replace(new RegExp(`:(${PARAM_NAME})`, 'g'), (_, name) => {\n paramNames.push(name)\n return '([^/]+)'\n })\n\n return { regex: new RegExp(`^${source}$`), paramNames }\n}\n\n/**\n * Decode a value that arrived from a URL, falling back to the raw input.\n *\n * Guarded rather than bare, for two independent reasons:\n *\n * A `%` that is not an escape is legitimate content — `/100%-Guide` authored by\n * hand, or a value that has already been decoded once — and `decodeURIComponent`\n * throws `URIError` on those. Falling back to the input keeps such a route\n * matching exactly as well as it did before.\n *\n * And the input is attacker-controlled: `/blog/%zz` is a URL anyone can paste or\n * link. This module is called by hosts that resolve a path to a page *per\n * request*, where a throw out of the matcher is a visitor-triggerable 500 rather\n * than a client-side error. A malformed escape is not a reason to lose an\n * otherwise-good match, so the fallback is the raw capture rather than a miss —\n * a route miss would turn a typo'd escape into a 404 on a page that exists.\n *\n * @param {string} value\n * @returns {string}\n */\nexport function decodeRouteValue(value) {\n if (typeof value !== 'string' || !value.includes('%')) return value\n try {\n return decodeURIComponent(value)\n } catch {\n return value\n }\n}\n\n/**\n * Match a concrete path against a route pattern.\n *\n * ```js\n * matchDynamicRoute('/blog/:slug', '/blog/my-post') // → { params: { slug: 'my-post' } }\n * matchDynamicRoute('/blog/:slug', '/blog/a/b') // → null (a param is one segment)\n * matchDynamicRoute('/blog/:slug', '/blog/') // → null (a param is non-empty)\n * ```\n *\n * Captured values are decoded, so a path carries percent encoding and the param\n * does not. A malformed escape falls back to the raw capture rather than\n * throwing — see `decodeRouteValue`. This function does not throw.\n *\n * @param {string} pattern - Route pattern with `:param` placeholders\n * @param {string} path - Concrete path to match\n * @returns {{ params: Record<string,string> } | null}\n */\nexport function matchDynamicRoute(pattern, path) {\n const { regex, paramNames } = routePatternToRegex(pattern)\n const match = normalizeRoute(path).match(regex)\n if (!match) return null\n\n const params = {}\n paramNames.forEach((name, i) => {\n params[name] = decodeRouteValue(match[i + 1])\n })\n return { params }\n}\n\n/**\n * Strip a locale prefix from a route.\n *\n * Pages are stored with unprefixed routes — the locale is a URL concern, not\n * part of a page's identity — so a lookup has to remove it first. The default\n * locale carries no prefix, which is why it is a no-op there.\n *\n * `/fr` and `/fr/` both mean the locale's home page.\n *\n * @param {string} route\n * @param {string|null} activeLocale\n * @param {string|null} defaultLocale\n * @returns {string}\n */\nexport function stripLocalePrefix(route, activeLocale, defaultLocale) {\n if (typeof route !== 'string') return '/'\n if (!activeLocale || activeLocale === defaultLocale) return route\n\n const prefix = `/${activeLocale}`\n if (route === prefix || route === `${prefix}/`) return '/'\n if (route.startsWith(`${prefix}/`)) return route.slice(prefix.length)\n return route\n}\n","/**\n * The icon corpus — its default origin and its filename rule.\n *\n * ## Why this is one module and not five constants\n *\n * An icon referenced by `library` + `name` is **our own asset**, not the site's.\n * We publish the families, we document them, and `@uniweb/icons`'\n * `scripts/build-cdn.js` writes the files. So unlike a site asset — whose URL\n * pattern the HOST declares because the bytes are in the host's store — the\n * layout here is ours to name, and a default origin is the correct answer\n * rather than a guessed one.\n *\n * That makes this a **writer/reader pair**, which is the part that needs a\n * single definition:\n *\n * writer @uniweb/icons scripts/build-cdn.js emits cdn/{family}/{family}-{name}.svg\n * readers @uniweb/runtime setup.js browser resolution\n * @uniweb/runtime ssr-renderer.js prerender + Worker isolate prefetch\n * @uniweb/icons src/resolver.js local-then-CDN resolution\n *\n * Before 2026-08-17 the origin was spelled out in three of those and the\n * filename rule in all four. A writer and its readers drifting is the exact\n * defect `@uniweb/core/route-match` exists to prevent, and the one the runtime\n * channel's bridge-filename helper prevents by construction. Same treatment\n * here: one helper, no second spelling.\n *\n * ## ⛔ Keep this a LEAF — zero imports\n *\n * `ssr-renderer.js` is bundled into the SSR isolate that runs in a Cloudflare\n * Worker, so anything it reaches must import nothing: no `node:*`, no DOM, no\n * `@uniweb/core` root (which pulls semantic-parser and theming). That is the\n * same constraint `route-match` and `locale-config` carry, and the reason this\n * lives in core rather than in `@uniweb/icons` — a Worker cannot take a package\n * whose value is ~3,200 icon modules behind a dynamic import, and `@uniweb/runtime`\n * depends on core already.\n *\n * A host may override the ORIGIN — a mirror of this corpus is a legitimate\n * deployment choice, and on a hosted site the base comes from the payload the\n * host serves. It may not override the LAYOUT: a mirror mirrors. Re-deriving\n * filenames instead of copying them is what produced two incompatible spellings\n * of the same corpus once already.\n *\n * @module @uniweb/core/icon-corpus\n */\n\n/**\n * Where the framework publishes its own icon corpus.\n *\n * Not a fallback for a missing host address — it is the address of OUR artifact,\n * and it is what makes `` work in a project with no backend at all.\n * A host that mirrors the corpus supplies its own origin on the payload.\n */\nexport const DEFAULT_ICON_BASE = 'https://uniweb.github.io/icons'\n\n/**\n * The corpus path for one icon, relative to any origin serving it.\n *\n * `{family}/{family}-{name}.svg` — the family repeats deliberately: the\n * directory groups, and the filename prefix keeps ids unique across families so\n * a name alone is never ambiguous.\n *\n * @param {string} family - short family code (`lu`, `hi2`, `fa6`)\n * @param {string} name - icon id within that family (`house`, `a-arrow-down`)\n * @returns {string} e.g. `lu/lu-house.svg`\n */\nexport function iconPath(family, name) {\n return `${family}/${family}-${name}.svg`\n}\n\n/**\n * The full URL for one icon against a serving origin.\n *\n * @param {string} family - short family code\n * @param {string} name - icon id within that family\n * @param {string} [base] - serving origin; defaults to the framework's own\n * @returns {string}\n */\nexport function iconUrl(family, name, base = DEFAULT_ICON_BASE) {\n return `${String(base).replace(/\\/+$/, '')}/${iconPath(family, name)}`\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 * (`framework/build/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. See\n * `framework/CLAUDE.md` \"Three-Layer Runtime Model\" for the rationale\n * and for how this 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 — see CLAUDE.md gotcha #9 on tree-shaking), 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\n * production canonical for multi-locale content (the Cloudflare Worker\n * SSR path consumes it directly). Build-time SSG pre-flattens to one\n * file per locale and so falls 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 cache key MUST go through `deriveCacheKey(entry.config)` and the\n * value MUST be wrapped as `{ data }` — otherwise the dispatcher's\n * lookup at `_dataStore.get(deriveCacheKey(request))` misses every\n * time and `cached.data` reads `undefined`. Three call sites used to\n * inline this loop independently (browser SPA, Node SSG, Cloudflare\n * Worker SSR); the Cloudflare one was using the wrong shape, silently\n * killing prefetched-data reuse in production. This helper is the one\n * canonical implementation.\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 website.dataStore.set(deriveCacheKey(entry.config), { data: entry.data })\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 */\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 const tracker = new Tracker({\n endpoint: url,\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 Cloudflare 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 * SSR Renderer\n *\n * Hook-free rendering pipeline for SSG (build) and cloud SSR (unicloud).\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 { routePatternToRegex } 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'\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 const { background = {} } = block.standardOptions\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 // Apply context overrides as inline CSS custom properties\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 * Convert hex/rgb color to rgba with opacity.\n * Mirrors withOpacity() in Background.jsx.\n */\nfunction withOpacity(color, opacity) {\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) {\n return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${opacity})`\n }\n }\n return color\n}\n\n/**\n * Resolve a URL against the site's base path.\n * Mirrors resolveUrl() in Background.jsx.\n */\nfunction resolveUrl(url) {\n if (!url || !url.startsWith('/')) return url\n const basePath = globalThis.uniweb?.activeWebsite?.basePath || ''\n if (!basePath) return url\n if (url.startsWith(basePath + '/') || url === basePath) return url\n return basePath + url\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(${resolveUrl(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 inherited entity data synchronously (SSG has no async).\n // EntityStore walks page/site hierarchy to find data matching meta.inheritData.\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, Cloudflare Worker SSR) 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 * - Unicloud: (none needed — payload is complete)\n *\n * NOTE: Does NOT clone content. Cloning is the consumer's responsibility\n * (build modifies content before init; unicloud 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 framework/core/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 // 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 for the contract and CLAUDE.md \"Three-Layer\n // Runtime Model\" for the rule about what belongs in this helper vs.\n // 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 const route = website?.activePage?.route || ''\n return { pathname: '/' + route, 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/**\n * Classify an SSR rendering error.\n *\n * @param {Error} err\n * @returns {{ type: 'hooks'|'null-component'|'unknown', message: string }}\n */\n/**\n * Resolve a route to the Page that should render it.\n *\n * Exists because this module 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 * 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\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 const sectionOverrideCSS = buildSectionOverrides(page.getPageBlocks(), 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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\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 build/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 page title (use getTitle() so isIndex pages inherit parent title)\n const pageTitle = page.getTitle?.() || page.title\n if (pageTitle) {\n result = result.replace(\n /<title>.*?<\\/title>/,\n `<title>${escapeHtml(pageTitle)}</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 const dynamicTemplates = siteContent.pages?.filter((p) => p.isDynamic) || []\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"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ssr.js","sources":["../src/prepare-props.js","../../core/src/route-match.js","../../core/src/icon-corpus.js","../src/default-404.js","../src/wire-foundation.js","../src/area-wrappers.js","../src/appearance.js","../src/ssr-renderer.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.\n if (Array.isArray(fieldDef.enum)) {\n if (result[field] !== undefined && !fieldDef.enum.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 `childCollection` is set, value may be `{ [childCollection]: [...] }`\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 collectionKey = schema.childCollection\n\n if (collectionKey && value && typeof value === 'object' && !Array.isArray(value)) {\n const arr = Array.isArray(value[collectionKey]) ? value[collectionKey] : []\n return {\n ...value,\n [collectionKey]: 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-066d.\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 * Merge entity data onto a block's parsedContent.data.\n *\n * Section-level data already on the block (from prerender fetches via\n * blockData.parsedContent.data in the Block constructor) takes priority;\n * entity data only fills missing keys. Mutates `block.parsedContent.data`\n * in place so the vanilla JS layer holds the assembled data and\n * subsequent reads see the same shape.\n */\nfunction mergeEntityData(block, entityData) {\n if (!entityData) return\n const current = block.parsedContent.data || {}\n let changed = false\n const merged = { ...current }\n for (const key of Object.keys(entityData)) {\n if (merged[key] === undefined) {\n merged[key] = entityData[key]\n changed = true\n }\n }\n if (changed) {\n block.parsedContent.data = merged\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. Merge entity data (resolved by EntityStore) onto\n * `block.parsedContent.data`.\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 mergeEntityData(block, entityData)\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 * Dynamic route patterns — the ONE home for how `/blog/:id` matches a path.\n *\n * Why this module exists. The rule was implemented twice and the two copies\n * disagreed. `Website#_matchDynamicRoute` built the pattern with `:(\\w+)`;\n * `generate404Html` in `@uniweb/runtime`'s SSR renderer built it with\n * `:[^/]+` and allowed an optional trailing slash. For `:id` they agree, so\n * nothing failed — but for a param name carrying a non-word character\n * (`/blog/:post-id`) the first matched only `post` and left `-id` as a\n * literal, while the second consumed the whole name. Two answers to one\n * question, neither wrong on the routes anyone had tried.\n *\n * That is already bad inside one repo. It is worse across them: a host that\n * renders a page server-side has to decide *which* page a path names, and the\n * runtime then hydrates over that decision in the browser. If the two matchers\n * disagree by a single route, the server renders page A and hydration replaces\n * it with page B — silently, and only on the paths that have a pattern, which\n * are exactly the interesting ones. So this is a cross-boundary contract, not\n * an implementation detail, and it is exported rather than merely shared.\n *\n * Zero-dependency leaf, like `./data-paths.js` and `./locale-config.js`, so a\n * consumer that must not pull core's graph — an edge worker, a build step —\n * can import the subpath `@uniweb/core/route-match` directly.\n *\n * ## The syntax, in full\n *\n * `:param` is the only construct. There are deliberately **no** catch-alls\n * (`*`), **no** optional segments (`?`), and **no** regex constraints — a\n * pattern is not a regular expression, and regex metacharacters in a route are\n * escaped to literals before any substitution happens. Matching is anchored,\n * case-sensitive, and a param captures exactly one non-empty path segment.\n *\n * ## What this module does NOT decide\n *\n * Matching a pattern means *the route exists*. It says nothing about whether\n * the record behind it exists — that is a data question the caller answers\n * later, and a matched pattern with no backing record is a rendered\n * not-found page rather than a route miss. Anything deciding a 404 purely from\n * this module can only answer the first question.\n */\n\n/**\n * Characters allowed in a param NAME — word characters plus the hyphen, so a\n * `[post-id]` route folder round-trips.\n *\n * Deliberately not `[^/]+`: a greedy name would swallow a literal suffix in the\n * same segment, so `/files/:name.json` would capture `name.json` as the param\n * name and leave nothing to match the extension.\n */\nconst PARAM_NAME = '[A-Za-z0-9_-]+'\n\n/** Regex metacharacters that must survive as literals. `-` is not one of them. */\nconst REGEX_SPECIALS = /[.*+?^${}()|[\\]\\\\]/g\n\n/**\n * Normalize a route for comparison: collapse a trailing slash, treat an empty\n * route as the root.\n *\n * `/about/` and `/about` are the same route; `/` stays `/`.\n *\n * @param {string} route\n * @returns {string}\n */\nexport function normalizeRoute(route) {\n if (typeof route !== 'string' || route === '') return '/'\n return route === '/' ? '/' : route.replace(/\\/+$/, '') || '/'\n}\n\n/**\n * Whether a route is a dynamic template rather than a concrete path.\n *\n * @param {string} route\n * @returns {boolean}\n */\nexport function isDynamicRoute(route) {\n return typeof route === 'string' && route.includes(':')\n}\n\n/**\n * Compile a route pattern to an anchored regex plus its param names.\n *\n * Exported for callers that match one pattern against many paths and want to\n * compile once — an edge worker checking every request against a site's\n * patterns, for instance.\n *\n * @param {string} pattern - e.g. `/blog/:id`\n * @returns {{ regex: RegExp, paramNames: string[] }}\n */\nexport function routePatternToRegex(pattern) {\n const paramNames = []\n const source = normalizeRoute(pattern)\n // Escape first: a `.` in a route is a literal `.`, not \"any character\".\n .replace(REGEX_SPECIALS, '\\\\$&')\n // Then each `:name` becomes one non-empty segment capture.\n .replace(new RegExp(`:(${PARAM_NAME})`, 'g'), (_, name) => {\n paramNames.push(name)\n return '([^/]+)'\n })\n\n return { regex: new RegExp(`^${source}$`), paramNames }\n}\n\n/**\n * Decode a value that arrived from a URL, falling back to the raw input.\n *\n * Guarded rather than bare, for two independent reasons:\n *\n * A `%` that is not an escape is legitimate content — `/100%-Guide` authored by\n * hand, or a value that has already been decoded once — and `decodeURIComponent`\n * throws `URIError` on those. Falling back to the input keeps such a route\n * matching exactly as well as it did before.\n *\n * And the input is attacker-controlled: `/blog/%zz` is a URL anyone can paste or\n * link. This module is called by hosts that resolve a path to a page *per\n * request*, where a throw out of the matcher is a visitor-triggerable 500 rather\n * than a client-side error. A malformed escape is not a reason to lose an\n * otherwise-good match, so the fallback is the raw capture rather than a miss —\n * a route miss would turn a typo'd escape into a 404 on a page that exists.\n *\n * @param {string} value\n * @returns {string}\n */\nexport function decodeRouteValue(value) {\n if (typeof value !== 'string' || !value.includes('%')) return value\n try {\n return decodeURIComponent(value)\n } catch {\n return value\n }\n}\n\n/**\n * Match a concrete path against a route pattern.\n *\n * ```js\n * matchDynamicRoute('/blog/:slug', '/blog/my-post') // → { params: { slug: 'my-post' } }\n * matchDynamicRoute('/blog/:slug', '/blog/a/b') // → null (a param is one segment)\n * matchDynamicRoute('/blog/:slug', '/blog/') // → null (a param is non-empty)\n * ```\n *\n * Captured values are decoded, so a path carries percent encoding and the param\n * does not. A malformed escape falls back to the raw capture rather than\n * throwing — see `decodeRouteValue`. This function does not throw.\n *\n * @param {string} pattern - Route pattern with `:param` placeholders\n * @param {string} path - Concrete path to match\n * @returns {{ params: Record<string,string> } | null}\n */\nexport function matchDynamicRoute(pattern, path) {\n const { regex, paramNames } = routePatternToRegex(pattern)\n const match = normalizeRoute(path).match(regex)\n if (!match) return null\n\n const params = {}\n paramNames.forEach((name, i) => {\n params[name] = decodeRouteValue(match[i + 1])\n })\n return { params }\n}\n\n/**\n * Strip a locale prefix from a route.\n *\n * Pages are stored with unprefixed routes — the locale is a URL concern, not\n * part of a page's identity — so a lookup has to remove it first. The default\n * locale carries no prefix, which is why it is a no-op there.\n *\n * `/fr` and `/fr/` both mean the locale's home page.\n *\n * @param {string} route\n * @param {string|null} activeLocale\n * @param {string|null} defaultLocale\n * @returns {string}\n */\nexport function stripLocalePrefix(route, activeLocale, defaultLocale) {\n if (typeof route !== 'string') return '/'\n if (!activeLocale || activeLocale === defaultLocale) return route\n\n const prefix = `/${activeLocale}`\n if (route === prefix || route === `${prefix}/`) return '/'\n if (route.startsWith(`${prefix}/`)) return route.slice(prefix.length)\n return route\n}\n","/**\n * The icon corpus — its default origin and its filename rule.\n *\n * ## Why this is one module and not five constants\n *\n * An icon referenced by `library` + `name` is **our own asset**, not the site's.\n * We publish the families, we document them, and `@uniweb/icons`'\n * `scripts/build-cdn.js` writes the files. So unlike a site asset — whose URL\n * pattern the HOST declares because the bytes are in the host's store — the\n * layout here is ours to name, and a default origin is the correct answer\n * rather than a guessed one.\n *\n * That makes this a **writer/reader pair**, which is the part that needs a\n * single definition:\n *\n * writer @uniweb/icons scripts/build-cdn.js emits cdn/{family}/{family}-{name}.svg\n * readers @uniweb/runtime setup.js browser resolution\n * @uniweb/runtime ssr-renderer.js prerender + Worker isolate prefetch\n * @uniweb/icons src/resolver.js local-then-CDN resolution\n *\n * Before 2026-08-17 the origin was spelled out in three of those and the\n * filename rule in all four. A writer and its readers drifting is the exact\n * defect `@uniweb/core/route-match` exists to prevent, and the one the runtime\n * channel's bridge-filename helper prevents by construction. Same treatment\n * here: one helper, no second spelling.\n *\n * ## ⛔ Keep this a LEAF — zero imports\n *\n * `ssr-renderer.js` is bundled into the SSR isolate that runs in a Cloudflare\n * Worker, so anything it reaches must import nothing: no `node:*`, no DOM, no\n * `@uniweb/core` root (which pulls semantic-parser and theming). That is the\n * same constraint `route-match` and `locale-config` carry, and the reason this\n * lives in core rather than in `@uniweb/icons` — a Worker cannot take a package\n * whose value is ~3,200 icon modules behind a dynamic import, and `@uniweb/runtime`\n * depends on core already.\n *\n * A host may override the ORIGIN — a mirror of this corpus is a legitimate\n * deployment choice, and on a hosted site the base comes from the payload the\n * host serves. It may not override the LAYOUT: a mirror mirrors. Re-deriving\n * filenames instead of copying them is what produced two incompatible spellings\n * of the same corpus once already.\n *\n * @module @uniweb/core/icon-corpus\n */\n\n/**\n * Where the framework publishes its own icon corpus.\n *\n * Not a fallback for a missing host address — it is the address of OUR artifact,\n * and it is what makes `` work in a project with no backend at all.\n * A host that mirrors the corpus supplies its own origin on the payload.\n */\nexport const DEFAULT_ICON_BASE = 'https://uniweb.github.io/icons'\n\n/**\n * The corpus path for one icon, relative to any origin serving it.\n *\n * `{family}/{family}-{name}.svg` — the family repeats deliberately: the\n * directory groups, and the filename prefix keeps ids unique across families so\n * a name alone is never ambiguous.\n *\n * @param {string} family - short family code (`lu`, `hi2`, `fa6`)\n * @param {string} name - icon id within that family (`house`, `a-arrow-down`)\n * @returns {string} e.g. `lu/lu-house.svg`\n */\nexport function iconPath(family, name) {\n return `${family}/${family}-${name}.svg`\n}\n\n/**\n * The full URL for one icon against a serving origin.\n *\n * @param {string} family - short family code\n * @param {string} name - icon id within that family\n * @param {string} [base] - serving origin; defaults to the framework's own\n * @returns {string}\n */\nexport function iconUrl(family, name, base = DEFAULT_ICON_BASE) {\n return `${String(base).replace(/\\/+$/, '')}/${iconPath(family, name)}`\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 * (`framework/build/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. See\n * `framework/CLAUDE.md` \"Three-Layer Runtime Model\" for the rationale\n * and for how this 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 — see CLAUDE.md gotcha #9 on tree-shaking), 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\n * production canonical for multi-locale content (the Cloudflare Worker\n * SSR path consumes it directly). Build-time SSG pre-flattens to one\n * file per locale and so falls 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 cache key MUST go through `deriveCacheKey(entry.config)` and the\n * value MUST be wrapped as `{ data }` — otherwise the dispatcher's\n * lookup at `_dataStore.get(deriveCacheKey(request))` misses every\n * time and `cached.data` reads `undefined`. Three call sites used to\n * inline this loop independently (browser SPA, Node SSG, Cloudflare\n * Worker SSR); the Cloudflare one was using the wrong shape, silently\n * killing prefetched-data reuse in production. This helper is the one\n * canonical implementation.\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 website.dataStore.set(deriveCacheKey(entry.config), { data: entry.data })\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 */\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 * @param {string|string[]|undefined} emit\n * @returns {string[]|null}\n */\nfunction resolveEmit(emit) {\n if (emit == null) return 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 can override a host's `consent` or `endpoint`; 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),\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 Cloudflare 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 * SSR Renderer\n *\n * Hook-free rendering pipeline for SSG (build) and cloud SSR (unicloud).\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 { routePatternToRegex } 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'\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 const { background = {} } = block.standardOptions\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 // Apply context overrides as inline CSS custom properties\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 * Convert hex/rgb color to rgba with opacity.\n * Mirrors withOpacity() in Background.jsx.\n */\nfunction withOpacity(color, opacity) {\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) {\n return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${opacity})`\n }\n }\n return color\n}\n\n/**\n * Resolve a URL against the site's base path.\n * Mirrors resolveUrl() in Background.jsx.\n */\nfunction resolveUrl(url) {\n if (!url || !url.startsWith('/')) return url\n const basePath = globalThis.uniweb?.activeWebsite?.basePath || ''\n if (!basePath) return url\n if (url.startsWith(basePath + '/') || url === basePath) return url\n return basePath + url\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(${resolveUrl(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 inherited entity data synchronously (SSG has no async).\n // EntityStore walks page/site hierarchy to find data matching meta.inheritData.\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, Cloudflare Worker SSR) 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 * - Unicloud: (none needed — payload is complete)\n *\n * NOTE: Does NOT clone content. Cloning is the consumer's responsibility\n * (build modifies content before init; unicloud 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 framework/core/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 // 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 for the contract and CLAUDE.md \"Three-Layer\n // Runtime Model\" for the rule about what belongs in this helper vs.\n // 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 const route = website?.activePage?.route || ''\n return { pathname: '/' + route, 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/**\n * Classify an SSR rendering error.\n *\n * @param {Error} err\n * @returns {{ type: 'hooks'|'null-component'|'unknown', message: string }}\n */\n/**\n * Resolve a route to the Page that should render it.\n *\n * Exists because this module 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 * 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\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 const sectionOverrideCSS = buildSectionOverrides(page.getPageBlocks(), 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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\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 build/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 page title (use getTitle() so isIndex pages inherit parent title)\n const pageTitle = page.getTitle?.() || page.title\n if (pageTitle) {\n result = result.replace(\n /<title>.*?<\\/title>/,\n `<title>${escapeHtml(pageTitle)}</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 const dynamicTemplates = siteContent.pages?.filter((p) => p.isDynamic) || []\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"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/runtime",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"description": "Minimal runtime for loading Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"node": ">=20.19"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@uniweb/core": "^0.10.
|
|
39
|
+
"@uniweb/core": "^0.10.2",
|
|
40
40
|
"@uniweb/theming": "^0.1.15"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The listeners a TRACKED document arms — grouped by WHEN THEY ARM.
|
|
3
|
+
*
|
|
4
|
+
* ⭐ **The grouping rule, which is what this file is really for.** Everything
|
|
5
|
+
* here needs exactly `tracking.arms(<event>)` with no further condition: a
|
|
6
|
+
* destination exists, this is a live document, and neither the host nor the site
|
|
7
|
+
* narrowed the event away. Today that is `outbound_click` alone; the next
|
|
8
|
+
* emitter meeting the same condition belongs in this chunk, and one meeting a
|
|
9
|
+
* different condition does not.
|
|
10
|
+
*
|
|
11
|
+
* ⛔ **`section-views.js` stays separate** — its condition is narrower (a page
|
|
12
|
+
* may override, so it is armed per page rather than per document). ⛔ **And
|
|
13
|
+
* `script-loader.js` stays separate on a different axis entirely** — a site can
|
|
14
|
+
* declare vendor scripts with **no endpoint at all**, and it is fetched only
|
|
15
|
+
* *after consent is granted*, so folding it in would pull a vendor loader down
|
|
16
|
+
* for a visitor who then declines.
|
|
3
17
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
18
|
+
* ⚖️ **Measured 2026-08-19, so the rule is not an aesthetic.** Merging all three
|
|
19
|
+
* gzips to 1255 B against 621 / 446 / 599 apart. That saves 411 B for a site
|
|
20
|
+
* using every one of them and costs ~640 B for the two commonest shapes —
|
|
21
|
+
* endpoint-only, and vendor-scripts-only. **The merge optimises the rarest site
|
|
22
|
+
* and penalises the usual ones**, which is the general reason to group by
|
|
23
|
+
* condition rather than by topic.
|
|
24
|
+
*
|
|
25
|
+
* ⚖️ Everything here runs from a `useEffect`, after paint. None of it is on the
|
|
26
|
+
* critical path, so request count is a minor term and correctness of the
|
|
27
|
+
* condition is the major one.
|
|
28
|
+
*
|
|
29
|
+
* @module @uniweb/runtime/document-tracking
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/* ------------------------------------------------------------------ *
|
|
33
|
+
* outbound_click — where a visitor goes when they leave.
|
|
7
34
|
*
|
|
8
35
|
* ## ⛔ The HOSTNAME is the event. The URL never leaves the page.
|
|
9
36
|
*
|
|
@@ -22,20 +49,16 @@
|
|
|
22
49
|
* ## No page-level opt-in, deliberately
|
|
23
50
|
*
|
|
24
51
|
* `section_view` needs one (`trackSections`) because its dimension is unbounded
|
|
25
|
-
* — a site's section types run to the hundreds and hosting capped the
|
|
26
|
-
* **An outbound hostname is bounded by how many external sites a
|
|
27
|
-
* which is small and does not grow with the site.
|
|
28
|
-
* `tracking.isEnabled()`: declaring a destination is already the operator's
|
|
29
|
-
* decision to measure.
|
|
52
|
+
* — a site's section types run to the hundreds and hosting capped the
|
|
53
|
+
* cardinality. **An outbound hostname is bounded by how many external sites a
|
|
54
|
+
* page links to**, which is small and does not grow with the site.
|
|
30
55
|
*
|
|
31
56
|
* ⛔ **And a page flag would not be free — it would need a DELIVERY PROJECTION**
|
|
32
|
-
* (the stored flag → the runtime payload),
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* @module @uniweb/runtime/outbound-clicks
|
|
38
|
-
*/
|
|
57
|
+
* (the stored flag → the runtime payload), a distinct item owned by a different
|
|
58
|
+
* lane and invisible from this one. That gap shipped once already and cost a day
|
|
59
|
+
* of a working emitter reporting nothing. **Not worth paying for a dimension
|
|
60
|
+
* that was never at risk.**
|
|
61
|
+
* ------------------------------------------------------------------ */
|
|
39
62
|
|
|
40
63
|
/**
|
|
41
64
|
* Protocols worth reporting as *traffic leaving the site*.
|
|
@@ -50,7 +73,7 @@ const REPORTED_PROTOCOLS = new Set(['http:', 'https:'])
|
|
|
50
73
|
/**
|
|
51
74
|
* The hostname this click leaves for, or `null` if it does not leave.
|
|
52
75
|
*
|
|
53
|
-
* Exported for the tests: the whole privacy claim of this
|
|
76
|
+
* Exported for the tests: the whole privacy claim of this half is that only a
|
|
54
77
|
* hostname is ever produced, and that is worth asserting directly rather than
|
|
55
78
|
* through a listener.
|
|
56
79
|
*
|
|
@@ -73,7 +96,7 @@ export function outboundHostname(href, here) {
|
|
|
73
96
|
if (url.hostname === here.hostname) return null
|
|
74
97
|
// `url.hostname` — never `url.host` (which appends a port), never `url.href`,
|
|
75
98
|
// and nothing derived from `search` or `pathname`. This return is the
|
|
76
|
-
// enforcement point named in the
|
|
99
|
+
// enforcement point named in the section header above.
|
|
77
100
|
return url.hostname || null
|
|
78
101
|
}
|
|
79
102
|
|
|
@@ -113,5 +136,3 @@ export function observeOutboundClicks(tracking) {
|
|
|
113
136
|
document.removeEventListener('auxclick', onClick, true)
|
|
114
137
|
}
|
|
115
138
|
}
|
|
116
|
-
|
|
117
|
-
export default observeOutboundClicks
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* dynamic import, so a site with no tracking destination downloads none of it —
|
|
6
6
|
* the same arrangement as `useSectionViews`.
|
|
7
7
|
*
|
|
8
|
-
* ⛔ **One
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `useSectionViews` there is no per-page
|
|
12
|
-
*
|
|
8
|
+
* ⛔ **One call, three questions.** `tracking.arms('outbound_click')` asks all of
|
|
9
|
+
* them: is there somewhere to send in a live document (so a framed authoring
|
|
10
|
+
* preview arms nothing), will the host consume this row, and did the site
|
|
11
|
+
* select it. Unlike `useSectionViews` there is no per-page override to pass — see the section header
|
|
12
|
+
* in `document-tracking.js` for why a page-level opt-in would cost a delivery
|
|
13
13
|
* projection and buy nothing.
|
|
14
14
|
*
|
|
15
15
|
* ## Armed ONCE for the document, not per page
|
|
@@ -37,14 +37,14 @@ import { useEffect } from 'react'
|
|
|
37
37
|
export function useOutboundClicks() {
|
|
38
38
|
useEffect(() => {
|
|
39
39
|
const tracking = globalThis.uniweb?.tracking
|
|
40
|
-
if (!tracking?.
|
|
40
|
+
if (!tracking?.arms?.('outbound_click')) return
|
|
41
41
|
|
|
42
42
|
// `cancelled` guards the gap between asking for the module and getting it —
|
|
43
43
|
// a fast unmount would otherwise install a listener nothing ever removes.
|
|
44
44
|
let cancelled = false
|
|
45
45
|
let stop = null
|
|
46
46
|
|
|
47
|
-
import('../
|
|
47
|
+
import('../document-tracking.js')
|
|
48
48
|
.then((m) => {
|
|
49
49
|
if (cancelled) return
|
|
50
50
|
stop = m.observeOutboundClicks(tracking)
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* same arrangement `script-loader.js` has, and the reason the opt-in is a
|
|
7
7
|
* declaration rather than a call a foundation makes.
|
|
8
8
|
*
|
|
9
|
-
* ⛔ **
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* ⛔ **One gate that resolves three tiers.** `arms('section_view',
|
|
10
|
+
* page.trackSections)` asks whether there is somewhere to send in a live
|
|
11
|
+
* document, whether the host will store the row, and then whose answer wins:
|
|
12
|
+
* the page's if it stated one, otherwise the site's `tracking.emit`. A page may
|
|
13
|
+
* widen what its own site configured and may not conjure a row the host
|
|
14
|
+
* declined — see `Tracker.arms`.
|
|
15
15
|
*
|
|
16
16
|
* ## SSR
|
|
17
17
|
*
|
|
@@ -51,8 +51,11 @@ export function useSectionViews(ready = true) {
|
|
|
51
51
|
if (!ready) return
|
|
52
52
|
const uniweb = globalThis.uniweb
|
|
53
53
|
const page = uniweb?.activeWebsite?.activePage
|
|
54
|
-
if (!page
|
|
55
|
-
|
|
54
|
+
if (!page) return
|
|
55
|
+
// One call, and the page's answer rides in as the override: `undefined`
|
|
56
|
+
// defers to the site's `tracking.emit`, `true`/`false` overrule it, and
|
|
57
|
+
// neither can escape the host's own list. See `Tracker.arms`.
|
|
58
|
+
if (!uniweb.tracking?.arms?.('section_view', page.trackSections)) return
|
|
56
59
|
|
|
57
60
|
// `cancelled` guards the gap between asking for the module and getting it:
|
|
58
61
|
// a fast navigation can unmount this effect first, and observing then would
|
package/src/wire-foundation.js
CHANGED
|
@@ -275,6 +275,46 @@ export function ensureThemeCss(uniweb, foundation) {
|
|
|
275
275
|
* @param {(urls: string[], opts: object) => void} [options.loadScripts] - DOM
|
|
276
276
|
* loader for declared vendor scripts; omitted outside a browser entry
|
|
277
277
|
*/
|
|
278
|
+
/**
|
|
279
|
+
* What `tracking.emit` names, when a site names a preset rather than a list.
|
|
280
|
+
*
|
|
281
|
+
* ⭐ **`all` is deliberately ABSENT from this table.** It resolves to `null` —
|
|
282
|
+
* *no narrowing* — so an event added in a later release is included without the
|
|
283
|
+
* site republishing. A literal list would freeze `all` at the moment the site
|
|
284
|
+
* was built and quietly stop meaning "all".
|
|
285
|
+
*
|
|
286
|
+
* ⚖️ **`standard` and `all` select the same events today, and that is not a
|
|
287
|
+
* reason to drop one.** They diverge the moment a new automatic event ships:
|
|
288
|
+
* `standard` is a curated set that a release cannot grow behind an operator's
|
|
289
|
+
* back, `all` is the standing yes. The volume surprise is the thing being
|
|
290
|
+
* avoided — a site that never changed should not start sending more.
|
|
291
|
+
*/
|
|
292
|
+
const EMIT_PRESETS = {
|
|
293
|
+
minimal: ['page_view'],
|
|
294
|
+
standard: ['page_view', 'outbound_click', 'section_view']
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** The preset a site gets by declaring a destination and nothing else. */
|
|
298
|
+
const DEFAULT_EMIT = 'standard'
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The site's own selection, as a list of event names or `null` for no narrowing.
|
|
302
|
+
*
|
|
303
|
+
* ⛔ **An unknown preset name resolves to the DEFAULT, not to nothing.** A typo
|
|
304
|
+
* (`emit: sandard`) must not silently take a site dark: the failure mode of a
|
|
305
|
+
* misread selection has to be "you got the usual set", never "you got none and
|
|
306
|
+
* nothing said so".
|
|
307
|
+
*
|
|
308
|
+
* @param {string|string[]|undefined} emit
|
|
309
|
+
* @returns {string[]|null}
|
|
310
|
+
*/
|
|
311
|
+
function resolveEmit(emit) {
|
|
312
|
+
if (emit == null) return EMIT_PRESETS[DEFAULT_EMIT]
|
|
313
|
+
if (Array.isArray(emit)) return emit
|
|
314
|
+
if (emit === 'all') return null
|
|
315
|
+
return EMIT_PRESETS[emit] || EMIT_PRESETS[DEFAULT_EMIT]
|
|
316
|
+
}
|
|
317
|
+
|
|
278
318
|
export function wireTracker(uniweb, { basePath = '', loadScripts = null } = {}) {
|
|
279
319
|
const website = uniweb?.activeWebsite
|
|
280
320
|
if (!website) return
|
|
@@ -296,8 +336,32 @@ export function wireTracker(uniweb, { basePath = '', loadScripts = null } = {})
|
|
|
296
336
|
// armed, nothing queued. This is the state of the large majority of sites.
|
|
297
337
|
if (!url && !hasScripts) return
|
|
298
338
|
|
|
339
|
+
// The two narrowings, resolved here rather than in core: this is per-request
|
|
340
|
+
// config reshaping, which is L2's job (see this file's header).
|
|
341
|
+
//
|
|
342
|
+
// ⛔ **`hostEvents` is read from the HOST tier only** — `config.services
|
|
343
|
+
// .tracking.events`, never the merged view. A site cannot widen what a host
|
|
344
|
+
// declined to store, and reading the merge would let it, silently, by writing
|
|
345
|
+
// its own `events:` key.
|
|
346
|
+
//
|
|
347
|
+
// ⛔ **Absent stays absent.** No `events` from the host means NO NARROWING,
|
|
348
|
+
// never an empty set: a host that sends no list is an older or simpler one,
|
|
349
|
+
// and the other reading takes every site on it dark with every gate saying
|
|
350
|
+
// yes. `?? null` rather than `?? []` is the whole of that guard.
|
|
351
|
+
// ⛔ Each tier is read from ITS OWN key, not from the merged `options`. The
|
|
352
|
+
// merge exists so a site can override a host's `consent` or `endpoint`; these
|
|
353
|
+
// two are not overrides of each other but answers to different questions, and
|
|
354
|
+
// reading either off the merge would let one tier answer the other's — a site
|
|
355
|
+
// writing `events:` would widen past what the host stores, silently.
|
|
356
|
+
const hostTracking = website.config?.services?.tracking
|
|
357
|
+
const siteTracking = website.config?.tracking
|
|
358
|
+
const hostEvents =
|
|
359
|
+
hostTracking && Array.isArray(hostTracking.events) ? hostTracking.events : null
|
|
360
|
+
|
|
299
361
|
const tracker = new Tracker({
|
|
300
362
|
endpoint: url,
|
|
363
|
+
hostEvents,
|
|
364
|
+
siteEmit: resolveEmit(siteTracking && siteTracking.emit),
|
|
301
365
|
// Opt-in, not the default. Declaring a destination is itself the operator's
|
|
302
366
|
// decision to track; requiring a second affirmative step would be the
|
|
303
367
|
// framework presuming a jurisdiction on their behalf, which is exactly what
|