@uniweb/build 0.14.10 → 0.14.11
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/site/content-collector.js +136 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.11",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
"js-yaml": "^4.1.0",
|
|
60
60
|
"sharp": "^0.33.2",
|
|
61
61
|
"yaml": "^2.5.0",
|
|
62
|
-
"@uniweb/
|
|
63
|
-
"@uniweb/
|
|
62
|
+
"@uniweb/content-writer": "0.2.5",
|
|
63
|
+
"@uniweb/theming": "0.1.3"
|
|
64
64
|
},
|
|
65
65
|
"optionalDependencies": {
|
|
66
|
-
"@uniweb/
|
|
66
|
+
"@uniweb/schemas": "0.2.3",
|
|
67
67
|
"@uniweb/content-reader": "1.1.12",
|
|
68
|
-
"@uniweb/
|
|
68
|
+
"@uniweb/runtime": "0.8.16"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -1234,6 +1234,12 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
1234
1234
|
title: pageConfig.title || extractH1(hierarchicalSections[0]?.content) || prettifySlug(pageName),
|
|
1235
1235
|
description: pageConfig.description || '',
|
|
1236
1236
|
label: pageConfig.label || null, // Short label for navigation (defaults to title)
|
|
1237
|
+
// Localized URL slug overrides: { <locale>: <segment> }. Compiled into
|
|
1238
|
+
// config.i18n.routeTranslations by collectSiteContent, then stripped from
|
|
1239
|
+
// the page payload (build is authoritative; runtime hydrates the map).
|
|
1240
|
+
...(pageConfig.slug && typeof pageConfig.slug === 'object' && !Array.isArray(pageConfig.slug)
|
|
1241
|
+
? { slug: pageConfig.slug }
|
|
1242
|
+
: {}),
|
|
1237
1243
|
lastModified: lastModified?.toISOString(),
|
|
1238
1244
|
|
|
1239
1245
|
// Dynamic route metadata
|
|
@@ -2133,11 +2139,33 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2133
2139
|
const intelligenceConfig = await readYamlFile(join(sitePath, 'intelligence.yml'))
|
|
2134
2140
|
const hasIntelligence = intelligenceConfig && Object.keys(intelligenceConfig).length > 0
|
|
2135
2141
|
|
|
2142
|
+
// Compile per-page `slug:` maps into config.i18n.routeTranslations
|
|
2143
|
+
// (canonical route → per-locale display route). Producer-only: the runtime
|
|
2144
|
+
// (@uniweb/core website.js) and the sitemap generator already consume this
|
|
2145
|
+
// map; nothing else produces it on the file-based lane. The default locale
|
|
2146
|
+
// keeps the canonical (folder-based) route. Strip the build-time `slug` from
|
|
2147
|
+
// pages afterward — the runtime hydrates the precomputed map, not per-page
|
|
2148
|
+
// slugs (see "Minimize Runtime Payload").
|
|
2149
|
+
const defaultLocale =
|
|
2150
|
+
siteConfig.defaultLanguage ||
|
|
2151
|
+
(Array.isArray(siteConfig.languages) ? siteConfig.languages[0] : null) ||
|
|
2152
|
+
'en'
|
|
2153
|
+
const routeTranslations = buildRouteTranslations(pages, {
|
|
2154
|
+
defaultLocale,
|
|
2155
|
+
languages: Array.isArray(siteConfig.languages) ? siteConfig.languages : null,
|
|
2156
|
+
})
|
|
2157
|
+
for (const page of pages) {
|
|
2158
|
+
if (page.slug) delete page.slug
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2136
2161
|
return {
|
|
2137
2162
|
config: {
|
|
2138
2163
|
...siteConfig,
|
|
2139
2164
|
fetch: parseFetchConfig(siteConfig.fetch),
|
|
2140
2165
|
...(hasIntelligence && { intelligence: intelligenceConfig }),
|
|
2166
|
+
...(routeTranslations
|
|
2167
|
+
? { i18n: { ...(siteConfig.i18n || {}), routeTranslations } }
|
|
2168
|
+
: {}),
|
|
2141
2169
|
},
|
|
2142
2170
|
theme: {
|
|
2143
2171
|
...processedTheme,
|
|
@@ -2162,7 +2190,115 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2162
2190
|
// than the flattened collector output but reuses these primitives so
|
|
2163
2191
|
// markdown→ProseMirror, ordering, and mode detection stay consistent with a
|
|
2164
2192
|
// normal build. Additive: no existing behavior changes.
|
|
2193
|
+
/**
|
|
2194
|
+
* Whether a value is a usable single URL path segment (no slashes, no
|
|
2195
|
+
* whitespace). Localized slug segments must satisfy this to be applied.
|
|
2196
|
+
*/
|
|
2197
|
+
function isValidSlugSegment(s) {
|
|
2198
|
+
return typeof s === 'string' && s.length > 0 && !s.includes('/') && !/\s/.test(s)
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
/**
|
|
2202
|
+
* Compose the full localized display route for a canonical route in a locale,
|
|
2203
|
+
* substituting the localized segment for any ancestor (or self) that declares
|
|
2204
|
+
* one in `slugByRoute`. Falls back to the canonical segment where no valid
|
|
2205
|
+
* localized slug exists. E.g. with /blog→blogue and /blog/my-post→mon-article,
|
|
2206
|
+
* `/blog/my-post` (fr) → `/blogue/mon-article`.
|
|
2207
|
+
*/
|
|
2208
|
+
function composeLocalizedRoute(canonicalRoute, locale, slugByRoute) {
|
|
2209
|
+
if (!canonicalRoute || canonicalRoute === '/') return canonicalRoute || '/'
|
|
2210
|
+
const segments = canonicalRoute.split('/').filter(Boolean)
|
|
2211
|
+
let cumulative = ''
|
|
2212
|
+
const out = []
|
|
2213
|
+
for (const seg of segments) {
|
|
2214
|
+
cumulative += `/${seg}`
|
|
2215
|
+
const localized = slugByRoute.get(cumulative)?.[locale]
|
|
2216
|
+
out.push(isValidSlugSegment(localized) ? localized : seg)
|
|
2217
|
+
}
|
|
2218
|
+
return '/' + out.join('/')
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
/**
|
|
2222
|
+
* Build `config.i18n.routeTranslations` from per-page `slug:` maps.
|
|
2223
|
+
*
|
|
2224
|
+
* Author surface: a page declares `slug: { <locale>: <segment> }` in its
|
|
2225
|
+
* page.yml. The canonical route stays the folder name; for each non-default
|
|
2226
|
+
* locale the localized URL substitutes the declared segment(s). The runtime
|
|
2227
|
+
* (`@uniweb/core` website.js: translateRoute/getLocaleUrl/getPageHierarchy) and
|
|
2228
|
+
* the build sitemap already consume this map — this is its only producer on the
|
|
2229
|
+
* file-based lane. Children without their own slug inherit a localized ancestor
|
|
2230
|
+
* via the runtime's prefix-cascade, so only pages that declare a slug get an
|
|
2231
|
+
* explicit entry.
|
|
2232
|
+
*
|
|
2233
|
+
* Pure + non-mutating. Returns `{ [locale]: { [canonicalRoute]: displayRoute } }`,
|
|
2234
|
+
* or null when no page declares a usable localized slug.
|
|
2235
|
+
*
|
|
2236
|
+
* @param {Array} pages - Flat page list (each with canonical `route` + optional `slug`).
|
|
2237
|
+
* @param {Object} [opts]
|
|
2238
|
+
* @param {string} [opts.defaultLocale='en'] - Default locale; its routes are the
|
|
2239
|
+
* canonical keys, so default-locale slugs are skipped (renaming the default
|
|
2240
|
+
* URL away from the folder name is a separate, heavier concern).
|
|
2241
|
+
* @param {string[]|null} [opts.languages=null] - Declared site locales; when an
|
|
2242
|
+
* array, slug locales outside it are skipped with a warning. null = no filter.
|
|
2243
|
+
* @returns {Object|null}
|
|
2244
|
+
*/
|
|
2245
|
+
function buildRouteTranslations(pages, { defaultLocale = 'en', languages = null } = {}) {
|
|
2246
|
+
if (!Array.isArray(pages)) return null
|
|
2247
|
+
|
|
2248
|
+
// Index canonical route → { locale: segment } for pages declaring a slug map.
|
|
2249
|
+
const slugByRoute = new Map()
|
|
2250
|
+
for (const page of pages) {
|
|
2251
|
+
const slug = page?.slug
|
|
2252
|
+
if (slug && typeof slug === 'object' && !Array.isArray(slug) && page.route) {
|
|
2253
|
+
slugByRoute.set(page.route, slug)
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
if (slugByRoute.size === 0) return null
|
|
2257
|
+
|
|
2258
|
+
const langSet = Array.isArray(languages) ? new Set(languages) : null
|
|
2259
|
+
const result = {}
|
|
2260
|
+
|
|
2261
|
+
for (const [canonicalRoute, localeMap] of slugByRoute) {
|
|
2262
|
+
for (const [locale, segment] of Object.entries(localeMap)) {
|
|
2263
|
+
// Default locale keeps the canonical (folder-based) route.
|
|
2264
|
+
if (locale === defaultLocale) continue
|
|
2265
|
+
if (langSet && !langSet.has(locale)) {
|
|
2266
|
+
console.warn(
|
|
2267
|
+
`[content-collector] slug locale '${locale}' on '${canonicalRoute}' is not in site languages — skipping`
|
|
2268
|
+
)
|
|
2269
|
+
continue
|
|
2270
|
+
}
|
|
2271
|
+
if (!isValidSlugSegment(segment)) {
|
|
2272
|
+
console.warn(
|
|
2273
|
+
`[content-collector] invalid slug '${segment}' for '${canonicalRoute}' (${locale}) — must be a single URL-safe path segment`
|
|
2274
|
+
)
|
|
2275
|
+
continue
|
|
2276
|
+
}
|
|
2277
|
+
const display = composeLocalizedRoute(canonicalRoute, locale, slugByRoute)
|
|
2278
|
+
;(result[locale] ||= {})[canonicalRoute] = display
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// Warn on within-locale display collisions (two canonical routes → same URL).
|
|
2283
|
+
for (const [locale, map] of Object.entries(result)) {
|
|
2284
|
+
const seen = new Map()
|
|
2285
|
+
for (const [canon, disp] of Object.entries(map)) {
|
|
2286
|
+
const prev = seen.get(disp)
|
|
2287
|
+
if (prev) {
|
|
2288
|
+
console.warn(
|
|
2289
|
+
`[content-collector] localized route collision in '${locale}': '${canon}' and '${prev}' both map to '${disp}'`
|
|
2290
|
+
)
|
|
2291
|
+
} else {
|
|
2292
|
+
seen.set(disp, canon)
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
return Object.keys(result).length > 0 ? result : null
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2165
2300
|
export {
|
|
2301
|
+
buildRouteTranslations,
|
|
2166
2302
|
extractItemName,
|
|
2167
2303
|
parseWildcardArray,
|
|
2168
2304
|
applyWildcardOrder,
|