@uniweb/build 0.20.0 → 0.21.0
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 +5 -5
- package/src/prerender.js +41 -2
- package/src/site/plugin.js +49 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -59,14 +59,14 @@
|
|
|
59
59
|
"js-yaml": "^4.1.0",
|
|
60
60
|
"sharp": "^0.35.3",
|
|
61
61
|
"yaml": "^2.5.0",
|
|
62
|
-
"@uniweb/content-writer": "^0.3.3",
|
|
63
|
-
"@uniweb/projections": "^0.2.8",
|
|
64
62
|
"@uniweb/schemas": "^0.2.10",
|
|
65
|
-
"@uniweb/
|
|
63
|
+
"@uniweb/projections": "^0.3.0",
|
|
64
|
+
"@uniweb/theming": "^0.1.15",
|
|
65
|
+
"@uniweb/content-writer": "^0.3.3"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@uniweb/content-reader": "^1.2.2",
|
|
69
68
|
"@uniweb/schemas": "^0.2.10",
|
|
69
|
+
"@uniweb/content-reader": "^1.2.2",
|
|
70
70
|
"@uniweb/semantic-parser": "^1.2.2",
|
|
71
71
|
"@uniweb/runtime": "^0.11.7"
|
|
72
72
|
},
|
package/src/prerender.js
CHANGED
|
@@ -148,6 +148,41 @@ async function executeAllFetches(siteContent, siteDir, onProgress, localeInfo) {
|
|
|
148
148
|
* @param {function} onProgress - Progress callback
|
|
149
149
|
* @returns {Array} Expanded pages array with dynamic pages replaced by concrete instances
|
|
150
150
|
*/
|
|
151
|
+
/**
|
|
152
|
+
* Where a content-less container's redirect stub should point, in THIS locale.
|
|
153
|
+
*
|
|
154
|
+
* A folder with no body of its own gets a `<meta http-equiv="refresh">` stub so
|
|
155
|
+
* the redirect works without JS. `page.getNavigableRoute()` answers in CANONICAL
|
|
156
|
+
* routes — right, because the hierarchy is canonical and "does this folder have
|
|
157
|
+
* a page of its own?" is not a language question. But the stub is per-locale, so
|
|
158
|
+
* the destination has to be localized exactly the way `outputRoute` is: translate
|
|
159
|
+
* the slug, then re-apply the locale prefix.
|
|
160
|
+
*
|
|
161
|
+
* Emitting the canonical route raw sent a reader who asked for `/fr/<container>`
|
|
162
|
+
* to the DEFAULT-locale page — a language switch they never asked for, from a
|
|
163
|
+
* URL that was correct. The runtime's copy of this redirect (PageRenderer.jsx)
|
|
164
|
+
* had the identical bug and was fixed first; this is its twin, and the two must
|
|
165
|
+
* agree or a container behaves differently on a cold load than on an in-app
|
|
166
|
+
* navigation.
|
|
167
|
+
*
|
|
168
|
+
* The DECISION to redirect stays canonical (`target !== page.route`) — only the
|
|
169
|
+
* destination is localized. Comparing a localized destination against a
|
|
170
|
+
* canonical route would make a folder with an index child redirect to itself.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} target - Canonical route from getNavigableRoute()
|
|
173
|
+
* @param {Object} ctx
|
|
174
|
+
* @param {Object} ctx.website - Website instance (for translateRoute / basePath)
|
|
175
|
+
* @param {string} ctx.locale - Locale being rendered
|
|
176
|
+
* @param {boolean} ctx.isDefault - Is this the site's default locale?
|
|
177
|
+
* @param {string} ctx.routePrefix - Locale prefix for this pass ('' or '/fr')
|
|
178
|
+
* @returns {string} Absolute path for the stub's refresh + canonical link
|
|
179
|
+
*/
|
|
180
|
+
export function localizeRedirectTarget(target, { website, locale, isDefault, routePrefix = '' }) {
|
|
181
|
+
const localized = isDefault ? target : routePrefix + website.translateRoute(target, locale)
|
|
182
|
+
const withSlash = localized.startsWith('/') ? localized : '/' + localized
|
|
183
|
+
return (website.basePath || '') + withSlash
|
|
184
|
+
}
|
|
185
|
+
|
|
151
186
|
export function expandDynamicPages(pages, pageFetchedData, onProgress) {
|
|
152
187
|
const expandedPages = []
|
|
153
188
|
|
|
@@ -704,8 +739,12 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
704
739
|
if (!page.hasContent()) {
|
|
705
740
|
const target = page.getNavigableRoute()
|
|
706
741
|
if (target && target !== page.route) {
|
|
707
|
-
const
|
|
708
|
-
|
|
742
|
+
const targetPath = localizeRedirectTarget(target, {
|
|
743
|
+
website,
|
|
744
|
+
locale,
|
|
745
|
+
isDefault,
|
|
746
|
+
routePrefix
|
|
747
|
+
})
|
|
709
748
|
onProgress(` Auto-redirect ${outputRoute} → ${targetPath}`)
|
|
710
749
|
const redirectHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${targetPath}"><link rel="canonical" href="${targetPath}"><title>Redirecting...</title></head><body><p>Redirecting to <a href="${targetPath}">${targetPath}</a></p></body></html>`
|
|
711
750
|
const outputPath = getOutputPath(distDir, outputRoute)
|
package/src/site/plugin.js
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
pageMarkdownFilename,
|
|
44
44
|
branchIndexFilename,
|
|
45
45
|
applyRouteTranslation,
|
|
46
|
+
partitionKnowledgePages,
|
|
46
47
|
INDEX_FILENAME
|
|
47
48
|
} from '@uniweb/projections'
|
|
48
49
|
import { collectSiteContent, mountEntriesOf } from './content-collector.js'
|
|
@@ -505,6 +506,51 @@ export function siteContentPlugin(options = {}) {
|
|
|
505
506
|
robots: seo.robots || {}
|
|
506
507
|
}
|
|
507
508
|
|
|
509
|
+
// Warn once per build, not once per collection — the dev server re-collects
|
|
510
|
+
// on every content change and would otherwise repeat this on every keystroke.
|
|
511
|
+
let warnedAboutKnowledge = false
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Collect the site, then drop `knowledge:` pages.
|
|
515
|
+
*
|
|
516
|
+
* ⛔ **This is the bundle lane, and the bundle lane has no agent endpoint.**
|
|
517
|
+
* `uniweb export` and `uniweb deploy --host <adapter>` both run this pipeline
|
|
518
|
+
* (`cli/src/commands/export.js`, `deploy.js`), and their destinations are
|
|
519
|
+
* static hosts — no worker, no backend, nothing that can gate a request. A
|
|
520
|
+
* `knowledge:` page is content the author marked for an agent and not for a
|
|
521
|
+
* visitor; here there is no agent to serve it to and no gate to withhold it
|
|
522
|
+
* with, so shipping it is disclosure with **no consumer on the other side**.
|
|
523
|
+
*
|
|
524
|
+
* Measured 2026-08-13, before this existed: a knowledge page became
|
|
525
|
+
* `dist/kb/index.html` — a public route — and its body rode in the
|
|
526
|
+
* `__SITE_CONTENT__` of *every* prerendered page including `404.html`,
|
|
527
|
+
* because that script carries the whole site.
|
|
528
|
+
*
|
|
529
|
+
* ⚠️ **Deliberately NOT inside `collectSiteContent`.** The link lane
|
|
530
|
+
* (`build-site-data.js`) calls that same collector and **must keep** these
|
|
531
|
+
* pages: it hands `site-content.json` to a backend that derives an agent
|
|
532
|
+
* corpus from it and gates access to that corpus. Same flag, two correct
|
|
533
|
+
* answers, decided by what is behind the destination — so the subtraction
|
|
534
|
+
* belongs to the pipeline, not to the collector.
|
|
535
|
+
*/
|
|
536
|
+
async function collectForBundle(path, options) {
|
|
537
|
+
const collected = await collectSiteContent(path, options)
|
|
538
|
+
const { knowledgePages, renderedPages } = partitionKnowledgePages(collected.pages || [])
|
|
539
|
+
if (!knowledgePages.length) return collected
|
|
540
|
+
|
|
541
|
+
if (!warnedAboutKnowledge) {
|
|
542
|
+
warnedAboutKnowledge = true
|
|
543
|
+
const routes = knowledgePages.map(page => page.route).join(', ')
|
|
544
|
+
console.warn(
|
|
545
|
+
`[site-content] Dropped ${knowledgePages.length} knowledge page(s) from this build: ${routes}\n` +
|
|
546
|
+
` This build targets a host that serves files only, so there is no agent endpoint to read them ` +
|
|
547
|
+
`and no gate to keep them from visitors. Use \`uniweb deploy\` to make them agent-readable.`
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return { ...collected, pages: renderedPages }
|
|
552
|
+
}
|
|
553
|
+
|
|
508
554
|
let siteContent = null
|
|
509
555
|
let resolvedSitePath = null
|
|
510
556
|
let resolvedPublicDir = null
|
|
@@ -738,7 +784,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
738
784
|
if (!isProduction) {
|
|
739
785
|
try {
|
|
740
786
|
// Do an early content collection to get the collections config
|
|
741
|
-
const earlyContent = await
|
|
787
|
+
const earlyContent = await collectForBundle(resolvedSitePath, { foundationPath })
|
|
742
788
|
collectionsConfig = earlyContent.config?.collections
|
|
743
789
|
|
|
744
790
|
// Resolve content directory paths from site.yml paths: group
|
|
@@ -788,7 +834,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
788
834
|
// pages stay in the graph so in-progress drafts remain previewable.
|
|
789
835
|
// strict on a production build: a mount that contributes no pages is a
|
|
790
836
|
// warning while you author and a broken deploy once you ship.
|
|
791
|
-
siteContent = await
|
|
837
|
+
siteContent = await collectForBundle(resolvedSitePath, { foundationPath, dropUnpublished: isProduction, base: basePath, strict: isProduction })
|
|
792
838
|
headHtml = await loadHeadHtml()
|
|
793
839
|
console.log(`[site-content] Collected ${siteContent.pages?.length || 0} pages`)
|
|
794
840
|
|
|
@@ -841,7 +887,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
841
887
|
rebuildTimeout = setTimeout(async () => {
|
|
842
888
|
console.log('[site-content] Content changed, rebuilding...')
|
|
843
889
|
try {
|
|
844
|
-
siteContent = await
|
|
890
|
+
siteContent = await collectForBundle(resolvedSitePath, { foundationPath, base: basePath })
|
|
845
891
|
headHtml = await loadHeadHtml()
|
|
846
892
|
// Execute fetches for the updated content
|
|
847
893
|
await executeDevFetches(siteContent, resolvedSitePath)
|