@uniweb/build 0.74.2 → 0.74.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/package.json +7 -7
- package/src/hosts/cloudflare-pages.js +8 -1
- package/src/prerender.js +20 -10
- package/src/site/content-collector.js +4 -5
- package/src/uwx/site-project.js +15 -1
- package/src/uwx/site.js +15 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.74.
|
|
3
|
+
"version": "0.74.4",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,15 +57,15 @@
|
|
|
57
57
|
"js-yaml": "^4.1.0",
|
|
58
58
|
"sharp": "^0.35.3",
|
|
59
59
|
"yaml": "^2.5.0",
|
|
60
|
-
"@uniweb/schemas": "^0.10.1",
|
|
61
|
-
"@uniweb/projections": "^0.7.11",
|
|
62
|
-
"@uniweb/content-writer": "^0.3.4",
|
|
63
60
|
"@uniweb/content-reader": "^1.2.8",
|
|
61
|
+
"@uniweb/projections": "^0.7.11",
|
|
64
62
|
"@uniweb/theming": "^0.2.0",
|
|
63
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
64
|
+
"@uniweb/schemas": "^0.10.1",
|
|
65
65
|
"@uniweb/semantic-parser": "^1.5.1"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@uniweb/runtime": "^0.28.
|
|
68
|
+
"@uniweb/runtime": "^0.28.7"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"@tailwindcss/vite": "^4.0.0",
|
|
75
75
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
76
76
|
"vite-plugin-svgr": "^4.0.0",
|
|
77
|
-
"@uniweb/
|
|
78
|
-
"@uniweb/
|
|
77
|
+
"@uniweb/core": "^0.35.2",
|
|
78
|
+
"@uniweb/runtime": "^0.28.7"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"vite": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
31
31
|
import { existsSync } from 'node:fs'
|
|
32
32
|
import { join } from 'node:path'
|
|
33
|
+
import { resolveRoute } from '@uniweb/core/resolve-route'
|
|
33
34
|
|
|
34
35
|
import { DeployError, spawnTool, readCredential, credentialHint } from './deploy-utils.js'
|
|
35
36
|
import {
|
|
@@ -61,7 +62,13 @@ async function collectRoutingEntries(localeConfigs) {
|
|
|
61
62
|
const prefix = localeConfig.routePrefix || ''
|
|
62
63
|
for (const page of siteContent.pages || []) {
|
|
63
64
|
if (page.redirect) {
|
|
64
|
-
|
|
65
|
+
// Where the app sends a visitor of this locale (`resolveRoute`): a page of the site in their
|
|
66
|
+
// language, anything else as written. ⛔ Until 2026-09-30 the target was written raw, so
|
|
67
|
+
// `/fr/old` redirected a French reader to the English page.
|
|
68
|
+
const route = `${prefix}${page.route}`
|
|
69
|
+
const resolved = resolveRoute(siteContent, route, { activeLocale: prefix ? prefix.slice(1) : undefined })
|
|
70
|
+
const location = resolved.kind === 'redirect' ? resolved.location : page.redirect
|
|
71
|
+
entries.push(`${route} ${location} 302`)
|
|
65
72
|
}
|
|
66
73
|
if (page.rewrite) {
|
|
67
74
|
entries.push(`${prefix}${page.route}/* ${page.rewrite}/:splat 200`)
|
package/src/prerender.js
CHANGED
|
@@ -279,10 +279,11 @@ export async function executeAllFetches(siteContent, siteDir, onProgress, locale
|
|
|
279
279
|
*
|
|
280
280
|
* Emitting the canonical route raw sent a reader who asked for `/fr/<container>`
|
|
281
281
|
* to the DEFAULT-locale page — a language switch they never asked for, from a
|
|
282
|
-
* URL that was correct. The
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
282
|
+
* URL that was correct. The in-app redirect had the identical bug and was fixed
|
|
283
|
+
* first. ⭐ Since 2026-09-30 that one is `resolveRoute`'s (`@uniweb/core/resolve-route`,
|
|
284
|
+
* `localeUrl`); this is the static twin of its destination — the same prefix and
|
|
285
|
+
* translated slug, with the deployment base this pass writes under — and the two
|
|
286
|
+
* must agree or a container behaves differently on a cold load than in the app.
|
|
286
287
|
*
|
|
287
288
|
* The DECISION to redirect stays canonical (`target !== page.route`) — only the
|
|
288
289
|
* destination is localized. Comparing a localized destination against a
|
|
@@ -1002,10 +1003,18 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
1002
1003
|
const translatedPageRoute = isDefault ? page.route : website.translateRoute(page.route, locale)
|
|
1003
1004
|
const outputRoute = routePrefix + translatedPageRoute
|
|
1004
1005
|
|
|
1005
|
-
// Redirect pages: emit a redirect HTML instead of rendering content
|
|
1006
|
+
// Redirect pages: emit a redirect HTML instead of rendering content — to where the app sends
|
|
1007
|
+
// a visitor of this locale (`resolveRoute`): a page of the site in their language, anything
|
|
1008
|
+
// else as written. ⛔ Until 2026-09-30 the stub wrote the target raw, so a French reader was
|
|
1009
|
+
// sent to the English page and a relative target resolved against the stub's own URL.
|
|
1006
1010
|
if (page.redirect) {
|
|
1007
|
-
|
|
1008
|
-
const
|
|
1011
|
+
const resolved = website.resolveRoute(page.route)
|
|
1012
|
+
const location = resolved.kind === 'redirect' ? resolved.location : page.redirect
|
|
1013
|
+
const target = location.startsWith('/') && !location.startsWith('//')
|
|
1014
|
+
? (website.basePath || '') + location
|
|
1015
|
+
: location
|
|
1016
|
+
onProgress(` Redirect ${outputRoute} → ${target}`)
|
|
1017
|
+
const redirectHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${target}"><link rel="canonical" href="${target}"><title>Redirecting...</title></head><body><p>Redirecting to <a href="${target}">${target}</a></p></body></html>`
|
|
1009
1018
|
const outputPath = getOutputPath(distDir, outputRoute)
|
|
1010
1019
|
await mkdir(dirname(outputPath), { recursive: true })
|
|
1011
1020
|
await writeFile(outputPath, redirectHtml)
|
|
@@ -1024,9 +1033,10 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
1024
1033
|
continue
|
|
1025
1034
|
}
|
|
1026
1035
|
|
|
1027
|
-
// Content-less containers: auto-redirect to first descendant with content
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1036
|
+
// Content-less containers: auto-redirect to first descendant with content —
|
|
1037
|
+
// `getNavigableRoute()`, which is `resolveRoute`'s landing rule
|
|
1038
|
+
// (`@uniweb/core/resolve-route`), so the stub goes where the app would.
|
|
1039
|
+
// A stub so the redirect works without JS (via <meta http-equiv="refresh">).
|
|
1030
1040
|
if (!page.hasContent()) {
|
|
1031
1041
|
const target = page.getNavigableRoute()
|
|
1032
1042
|
if (target && target !== page.route) {
|
|
@@ -38,6 +38,7 @@ import { parseFetchConfig, toFetchList } from './data-fetcher.js'
|
|
|
38
38
|
import { resolveExtensionUrls } from './extension-urls.js'
|
|
39
39
|
import { buildTheme, extractFoundationVars } from '../theme/index.js'
|
|
40
40
|
import { resolveDefaultLocale, resolvePublishableLocales, validateLanguageConfig, pageRouteQuery, currentFor } from '@uniweb/core'
|
|
41
|
+
import { authoredRedirectTarget } from '@uniweb/core/resolve-route'
|
|
41
42
|
import { parseFrontmatter } from '../utils/frontmatter.js'
|
|
42
43
|
import { parseGrid } from '@uniweb/schemas/grid'
|
|
43
44
|
|
|
@@ -1333,11 +1334,9 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
1333
1334
|
if (pageConfig.redirect) {
|
|
1334
1335
|
const route = isIndex ? parentRoute
|
|
1335
1336
|
: parentRoute === '/' ? `/${pageName}` : `${parentRoute}/${pageName}`
|
|
1336
|
-
|
|
1337
|
-
//
|
|
1338
|
-
const target =
|
|
1339
|
-
? redirect
|
|
1340
|
-
: (route === '/' ? `/${redirect}` : `${route}/${redirect}`)
|
|
1337
|
+
// A relative redirect resolves against the page's own route — core's rule, which a host
|
|
1338
|
+
// resolving a pushed payload applies too (`@uniweb/core/resolve-route`).
|
|
1339
|
+
const target = authoredRedirectTarget(route, pageConfig.redirect)
|
|
1341
1340
|
|
|
1342
1341
|
return {
|
|
1343
1342
|
page: {
|
package/src/uwx/site-project.js
CHANGED
|
@@ -839,7 +839,7 @@ function writePagesTree(pages, pagesDir, sourceLocale, report, ctx, routePrefix
|
|
|
839
839
|
// ⚖️ A pull adds and changes these, and never removes one: a page pushed by a CLI from before carries
|
|
840
840
|
// none, and reading that as "none" would delete what the author wrote.
|
|
841
841
|
function pageSlugToYml(record, canon, existing, sourceLocale, ctx) {
|
|
842
|
-
if (record.is_dynamic) return
|
|
842
|
+
if (record.is_dynamic) return dynamicOwnSlug(record, existing, sourceLocale)
|
|
843
843
|
const targets = localizedTargets(record.slug, sourceLocale)
|
|
844
844
|
const own = isPlainRecord(existing?.slug)
|
|
845
845
|
if (!own && ctx.routes?.siteMap && !record.is_index) {
|
|
@@ -851,6 +851,20 @@ function pageSlugToYml(record, canon, existing, sourceLocale, ctx) {
|
|
|
851
851
|
return own && stableJson(next) === stableJson(existing.slug) ? null : next
|
|
852
852
|
}
|
|
853
853
|
|
|
854
|
+
// A parametric page's folder is named by its param (`pageDirName`), so its OWN slug — where a page
|
|
855
|
+
// made in an app gives it one of its own (`slug: detail`, `param_name: id`) — has no other place on
|
|
856
|
+
// disk. It is kept in `page.yml` as a plain string, which the build never reads for a route (only a
|
|
857
|
+
// per-language map names URL segments) and the push sends back as the page's slug. A string written
|
|
858
|
+
// here before is rewritten to the stored slug, so it is never left stale.
|
|
859
|
+
// ⛔ Until 2026-09-30 a pull dropped it, and the next push sent the param in its place: an authored
|
|
860
|
+
// value lost on a round trip (`uwx-format.md` § THE ROUND-TRIP LAW).
|
|
861
|
+
function dynamicOwnSlug(record, existing, sourceLocale) {
|
|
862
|
+
const own = unwrapLocalized(record.slug, sourceLocale)
|
|
863
|
+
if (typeof own !== 'string' || !own || own === '...path') return null
|
|
864
|
+
if (own !== (record.param_name || own)) return own
|
|
865
|
+
return typeof existing?.slug === 'string' ? own : null
|
|
866
|
+
}
|
|
867
|
+
|
|
854
868
|
// The locales of a pulled localized `slug` other than the source one.
|
|
855
869
|
function localizedTargets(slug, sourceLocale) {
|
|
856
870
|
if (!isPlainRecord(slug)) return {}
|
package/src/uwx/site.js
CHANGED
|
@@ -224,7 +224,15 @@ function localizeRouteSlugs(pages, siteYml, sourceLocale) {
|
|
|
224
224
|
export function buildRouteOf(record, parentRoute, sourceLocale) {
|
|
225
225
|
if (record?.is_index) return parentRoute
|
|
226
226
|
const slug = unwrapLocalized(record?.slug, sourceLocale)
|
|
227
|
-
|
|
227
|
+
// A parametric page's segment is its PARAM — the name the pull gives its folder, `[param_name]`
|
|
228
|
+
// (`site-project.js::pageDirName`), which is the route the build then reads. A page pushed from
|
|
229
|
+
// files has one word for both; a page made in an app may not (`slug: detail`, `param_name: id`).
|
|
230
|
+
// ⛔ Until 2026-09-30 this used the slug, so such a page was addressed `/…/:detail` while its
|
|
231
|
+
// folder built `/…/:id`, and its translated title and description were filed under a route no
|
|
232
|
+
// build reads.
|
|
233
|
+
const segment = record?.is_dynamic
|
|
234
|
+
? (slug === CATCH_ALL_MARKER ? ':path*' : `:${record.param_name || slug}`)
|
|
235
|
+
: slug
|
|
228
236
|
return parentRoute === '/' ? `/${segment}` : `${parentRoute}/${segment}`
|
|
229
237
|
}
|
|
230
238
|
|
|
@@ -783,6 +791,12 @@ async function walkPagesNested(ctx, dirPath, parentSlugPath, inheritedMode, pare
|
|
|
783
791
|
// A page's own localized URL segments, for `localizeRouteSlugs` — under a symbol, so they
|
|
784
792
|
// are never serialized as a field of the record.
|
|
785
793
|
if (!dyn && isPlainRecord(f.config?.slug)) record[AUTHORED_SLUG] = f.config.slug
|
|
794
|
+
// A parametric page's OWN slug, where it is not its param — a page made in an app — is kept in
|
|
795
|
+
// its page.yml by the pull as a plain string (`site-project.js::dynamicOwnSlug`), since its folder
|
|
796
|
+
// is named by its param. The route, the folder and the page's identity stay the param's.
|
|
797
|
+
if (dyn && dyn[1] !== CATCH_ALL_MARKER && typeof f.config?.slug === 'string' && f.config.slug) {
|
|
798
|
+
record.slug = { ...record.slug, [sourceLocale]: f.config.slug }
|
|
799
|
+
}
|
|
786
800
|
|
|
787
801
|
if (mode === 'page') {
|
|
788
802
|
const sections = await collectPageSectionsNested(f.path, siteRoot, f.config)
|