@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 +8 -8
- package/src/prerender.js +29 -14
- package/src/site/plugin.js +8 -3
- package/src/site/record-projections.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "1.9.
|
|
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/
|
|
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.
|
|
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/
|
|
77
|
-
"@uniweb/
|
|
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,
|
package/src/site/plugin.js
CHANGED
|
@@ -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
|
|
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'
|