@uniweb/build 1.9.0 → 1.9.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -56,15 +56,15 @@
56
56
  "js-yaml": "^4.1.0",
57
57
  "sharp": "^0.35.3",
58
58
  "yaml": "^2.5.0",
59
- "@uniweb/projections": "~1.2.0",
60
- "@uniweb/theming": "~1.2.1",
61
- "@uniweb/schemas": "~1.6.5",
62
59
  "@uniweb/content-reader": "~1.5.6",
60
+ "@uniweb/projections": "~1.2.0",
63
61
  "@uniweb/content-writer": "~1.3.0",
64
- "@uniweb/semantic-parser": "~1.13.0"
62
+ "@uniweb/schemas": "~1.6.5",
63
+ "@uniweb/semantic-parser": "~1.13.0",
64
+ "@uniweb/theming": "~1.2.1"
65
65
  },
66
66
  "optionalDependencies": {
67
- "@uniweb/runtime": "~1.2.0"
67
+ "@uniweb/runtime": "~1.2.2"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -73,8 +73,8 @@
73
73
  "@tailwindcss/vite": "^4.0.0",
74
74
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
75
75
  "vite-plugin-svgr": "^4.0.0",
76
- "@uniweb/core": "~1.4.5",
77
- "@uniweb/runtime": "~1.2.0"
76
+ "@uniweb/runtime": "~1.2.2",
77
+ "@uniweb/core": "~1.4.6"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "vite": {
package/src/prerender.js CHANGED
@@ -988,20 +988,30 @@ export async function prerenderSite(siteDir, options = {}) {
988
988
  })
989
989
  const buildDataCache = new DataStore()
990
990
 
991
- // Build-specific: pre-populate DataStore so EntityStore can resolve data during prerender.
992
- // hydrateDataStore handles cache-key derivation + value-shape wrapping
993
- // — same helper used by the browser SPA boot and by the Cloudflare
994
- // Worker SSR isolate, so all three render paths agree on shape.
995
- // ⛔ `siteContent.fetchedData`, not the raw list: only it holds the route-bound
996
- // views, and an expanded page rendered without them paints "not found" (measured).
997
- hydrateDataStore(uniweb.activeWebsite, siteContent.fetchedData)
998
-
999
991
  // Pre-fetch icons for SSR embedding
1000
992
  await prefetchIcons(siteContent, uniweb, onProgress)
1001
993
 
1002
994
  // Pre-render each page
1003
995
  const website = uniweb.activeWebsite
1004
996
 
997
+ /**
998
+ * ⭐ A PAGE RENDERS AGAINST WHAT IT CARRIES — the entries its HTML embeds, which are all the
999
+ * browser's store holds when it boots on that page (`initUniweb` hydrates from them). The
1000
+ * browser renders over the prerendered page, so a render that reads more paints content the
1001
+ * browser does not have, and the browser replaces it with a loading state at boot.
1002
+ *
1003
+ * ⛔ Until 2026-10-25 the store was hydrated once for the whole locale and then took every
1004
+ * page's answers in turn, so a page could render data that only other pages carried. A
1005
+ * section can read the store directly (kit's `useFetched`, `useWholeRecord`), beyond what its
1006
+ * page declares: a page showing records whole through `useWholeRecord` prerendered the ones
1007
+ * the record pages before it had fetched, carried none of them, and showed "loading…" at
1008
+ * boot (measured).
1009
+ */
1010
+ const holdOnly = (carried) => {
1011
+ website.dataStore.clear()
1012
+ hydrateDataStore(website, carried)
1013
+ }
1014
+
1005
1015
  for (const page of website.pages) {
1006
1016
  // Skip dynamic template pages — they exist in the content for runtime
1007
1017
  // route matching but can't be pre-rendered (no concrete route)
@@ -1085,7 +1095,6 @@ export async function prerenderSite(siteDir, options = {}) {
1085
1095
  prerender: 'author',
1086
1096
  cache: buildDataCache,
1087
1097
  })
1088
- hydrateDataStore(website, asked)
1089
1098
  // Filed under this page, the way a route-bound view always has been: `scopeFetchedData`
1090
1099
  // embeds it in this page's HTML and nowhere else, and one address embedded twice collapses.
1091
1100
  for (const entry of asked) {
@@ -1093,6 +1102,15 @@ export async function prerenderSite(siteDir, options = {}) {
1093
1102
  siteContent.fetchedData.push({ ...entry, _scope: page.route, _routeBound: true })
1094
1103
  }
1095
1104
 
1105
+ // scopeRoutes mirrors the runtime data cascade (page → page.parent → site)
1106
+ // so split-mode pages embed only the collection data their first render reads.
1107
+ // An expanded page's own fetch was read under its TEMPLATE's route — the
1108
+ // route it had when the fetches ran — so that route is in its cascade too.
1109
+ const scopeRoutes = new Set([page.route, page.parent?.route, page.dynamicContext?.templateRoute].filter(Boolean))
1110
+ // The same call `injectBuildData` makes below for this page's HTML, so the render holds what
1111
+ // the HTML embeds — the page's own data step's answers among them.
1112
+ holdOnly(scopeFetchedData(siteContent.fetchedData, splitContent ? scopeRoutes : null, page.route))
1113
+
1096
1114
  onProgress(`Rendering ${outputRoute}...`)
1097
1115
 
1098
1116
  // ⭐ `renderer.render` IS the sequence this loop used to spell out —
@@ -1133,11 +1151,6 @@ export async function prerenderSite(siteDir, options = {}) {
1133
1151
  let html = result.html
1134
1152
 
1135
1153
  // Build-specific: theme CSS, __SITE_CONTENT__, icon cache.
1136
- // scopeRoutes mirrors the runtime data cascade (page → page.parent → site)
1137
- // so split-mode pages embed only the collection data their first render reads.
1138
- // An expanded page's own fetch was read under its TEMPLATE's route — the
1139
- // route it had when the fetches ran — so that route is in its cascade too.
1140
- const scopeRoutes = new Set([page.route, page.parent?.route, page.dynamicContext?.templateRoute].filter(Boolean))
1141
1154
  html = injectBuildData(html, siteContent, {
1142
1155
  splitContent,
1143
1156
  currentRoute: page.route,
@@ -1162,6 +1175,8 @@ export async function prerenderSite(siteDir, options = {}) {
1162
1175
  currentRoute: null, // 404 has no current page — manifest only
1163
1176
  scopeRoutes: new Set(), // SPA fallback carries site-level fetched data only
1164
1177
  })
1178
+ // The 404 page renders against what 404.html carries, as every page above does.
1179
+ holdOnly(scopeFetchedData(siteContent.fetchedData, splitContent ? new Set() : null, null))
1165
1180
  const { html: notFoundHtml, hasNotFoundPage } = generate404Html({
1166
1181
  baseHtml: fallbackBaseHtml,
1167
1182
  website,
@@ -305,7 +305,6 @@ import { mountDevApi } from '../dev/api-mount.js'
305
305
  * @param {boolean} [options.assets.pdfThumbnails=true] - Generate thumbnails for PDFs (requires pdf-lib)
306
306
  * @param {Object} [options.search] - Search index configuration
307
307
  * @param {boolean} [options.search.enabled=true] - Generate search index (uses site.yml config by default)
308
- * @param {string} [options.search.filename='search-index.json'] - Search index filename
309
308
  * @param {string} [options.foundationPath] - Path to foundation directory (for loading theme vars)
310
309
  */
311
310
  export function siteContentPlugin(options = {}) {
@@ -1080,7 +1079,10 @@ export function siteContentPlugin(options = {}) {
1080
1079
  // ⛔ No sitemap.xml or robots.txt here: the prerender writes them from the pages it
1081
1080
  // renders (`./seo-files.js`), and a dev server renders none ahead of a visit.
1082
1081
 
1083
- // Serve search-index.json in dev mode (supports locale prefixes)
1082
+ // Serve search-index.json in dev mode, under a language's prefix too.
1083
+ // ⚠️ Made from the default language's content whatever the prefix, and with no records for
1084
+ // another language: the build's `/fr/search-index.json` is in French, this one is not
1085
+ // (measured 2026-10-25).
1084
1086
  const searchIndexMatch = req.url.match(new RegExp(`^(?:\\/(${LOCALE_RE}))?\\/search-index\\.json$`))
1085
1087
  if (searchIndexMatch && siteContent) {
1086
1088
  const searchEnabled = searchPluginConfig.enabled !== false && searchDeclaredOn(siteContent)
@@ -1111,7 +1113,10 @@ export function siteContentPlugin(options = {}) {
1111
1113
  // Without this the artifacts can only be inspected by deploying, and
1112
1114
  // `uniweb doctor`'s address warning describes something invisible
1113
1115
  // locally. Both routes reuse the build's generators, so what a
1114
- // developer sees here is what ships.
1116
+ // developer sees here is what ships — for the default language.
1117
+ // ⚠️ Not for another one (measured 2026-10-25): the build writes it no `llms.txt` or `.md`,
1118
+ // and the `.md` route below finds a page by its source route, so the translated links this
1119
+ // `llms.txt` gives (`/fr/a-propos.md`) are answered with the page shell.
1115
1120
  if (siteContent) {
1116
1121
  const agents = resolveAgentsConfig(siteContent.config)
1117
1122
  const url = req.url.split('?')[0]
@@ -8,6 +8,10 @@
8
8
  * ⛔ Until 2026-10-25 the static build made its projections from the site's pages alone: llms.txt
9
9
  * and the search index held none of a site's books, talks or posts, while a host's held all of them
10
10
  * (measured on a site with 16 record pages: 0 in llms.txt, 0 in the search index).
11
+ *
12
+ * ⚠️ For the default language only. Another language's search index is still made from its pages
13
+ * alone, before its records are translated (`i18n/index.js::buildLocalizedContent`), and the build
14
+ * writes that language no llms.txt or `.md` (measured 2026-10-25 on a site in three languages).
11
15
  */
12
16
 
13
17
  import { readFileSync } from 'node:fs'