@uniweb/build 0.8.25 → 0.8.26
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 +6 -6
- package/src/i18n/index.js +2 -1
- package/src/prerender.js +44 -1
- package/src/site/content-collector.js +116 -47
- package/src/site/plugin.js +7 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.26",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0",
|
|
52
52
|
"js-yaml": "^4.1.0",
|
|
53
53
|
"sharp": "^0.33.2",
|
|
54
|
-
"@uniweb/theming": "0.1.
|
|
54
|
+
"@uniweb/theming": "0.1.3"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"@uniweb/runtime": "0.6.
|
|
58
|
-
"@uniweb/
|
|
59
|
-
"@uniweb/
|
|
57
|
+
"@uniweb/runtime": "0.6.22",
|
|
58
|
+
"@uniweb/schemas": "0.2.1",
|
|
59
|
+
"@uniweb/content-reader": "1.1.4"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"@tailwindcss/vite": "^4.0.0",
|
|
66
66
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
67
67
|
"vite-plugin-svgr": "^4.0.0",
|
|
68
|
-
"@uniweb/core": "0.5.
|
|
68
|
+
"@uniweb/core": "0.5.16"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
71
71
|
"vite": {
|
package/src/i18n/index.js
CHANGED
|
@@ -154,7 +154,8 @@ async function resolveLocales(configLocales, localesPath) {
|
|
|
154
154
|
if (configLocales.includes('*')) {
|
|
155
155
|
return getAvailableLocales(localesPath)
|
|
156
156
|
}
|
|
157
|
-
|
|
157
|
+
// Normalize: support both string codes and objects ({code, label})
|
|
158
|
+
return configLocales.map(l => typeof l === 'string' ? l : l.code)
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
// String value '*' means all available
|
package/src/prerender.js
CHANGED
|
@@ -270,7 +270,7 @@ async function discoverLocaleContents(distDir, defaultContent) {
|
|
|
270
270
|
if (!statSync(entryPath).isDirectory()) continue
|
|
271
271
|
|
|
272
272
|
// Check if this looks like a locale code (2-3 letter code)
|
|
273
|
-
if (!/^[a-z]{2,3}(
|
|
273
|
+
if (!/^[a-z]{2,3}(?:-[A-Za-z]{2,4})?$/.test(entry)) continue
|
|
274
274
|
|
|
275
275
|
// Check if it has a site-content.json
|
|
276
276
|
const localeContentPath = join(entryPath, 'site-content.json')
|
|
@@ -510,6 +510,23 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
510
510
|
const translatedPageRoute = isDefault ? page.route : website.translateRoute(page.route, locale)
|
|
511
511
|
const outputRoute = routePrefix + translatedPageRoute
|
|
512
512
|
|
|
513
|
+
// Redirect pages: emit a redirect HTML instead of rendering content
|
|
514
|
+
if (page.redirect) {
|
|
515
|
+
onProgress(` Redirect ${outputRoute} → ${page.redirect}`)
|
|
516
|
+
const redirectHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${page.redirect}"><link rel="canonical" href="${page.redirect}"><title>Redirecting...</title></head><body><p>Redirecting to <a href="${page.redirect}">${page.redirect}</a></p></body></html>`
|
|
517
|
+
const outputPath = getOutputPath(distDir, outputRoute)
|
|
518
|
+
await mkdir(dirname(outputPath), { recursive: true })
|
|
519
|
+
await writeFile(outputPath, redirectHtml)
|
|
520
|
+
renderedFiles.push(outputPath)
|
|
521
|
+
continue
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Rewrite pages: served by an external site, skip rendering entirely
|
|
525
|
+
if (page.rewrite) {
|
|
526
|
+
onProgress(` Rewrite ${outputRoute} → ${page.rewrite}`)
|
|
527
|
+
continue
|
|
528
|
+
}
|
|
529
|
+
|
|
513
530
|
onProgress(`Rendering ${outputRoute}...`)
|
|
514
531
|
|
|
515
532
|
const result = renderPage(page, website)
|
|
@@ -562,6 +579,32 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
562
579
|
onProgress(` → ${routePrefix || ''}404.html (${fallbackNote})`)
|
|
563
580
|
}
|
|
564
581
|
|
|
582
|
+
// Generate _redirects file for Cloudflare Pages / Netlify
|
|
583
|
+
// Format: source destination status
|
|
584
|
+
// 302 = redirect (browser URL changes)
|
|
585
|
+
// 200 = rewrite/proxy (browser URL stays, host proxies transparently)
|
|
586
|
+
const routingEntries = []
|
|
587
|
+
for (const localeConfig of localeConfigs) {
|
|
588
|
+
const siteContent = JSON.parse(await readFile(localeConfig.contentPath, 'utf8'))
|
|
589
|
+
const prefix = localeConfig.routePrefix || ''
|
|
590
|
+
for (const page of siteContent.pages || []) {
|
|
591
|
+
if (page.redirect) {
|
|
592
|
+
routingEntries.push(`${prefix}${page.route} ${page.redirect} 302`)
|
|
593
|
+
}
|
|
594
|
+
if (page.rewrite) {
|
|
595
|
+
routingEntries.push(`${prefix}${page.route}/* ${page.rewrite}/:splat 200`)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (routingEntries.length > 0) {
|
|
600
|
+
const redirectsPath = join(distDir, '_redirects')
|
|
601
|
+
// Append to existing _redirects if the developer maintains one
|
|
602
|
+
const existing = existsSync(redirectsPath) ? await readFile(redirectsPath, 'utf8') : ''
|
|
603
|
+
const generated = `# Auto-generated from page.yml redirect: and rewrite: declarations\n${routingEntries.join('\n')}\n`
|
|
604
|
+
await writeFile(redirectsPath, existing ? `${existing.trimEnd()}\n\n${generated}` : generated)
|
|
605
|
+
onProgress(`Generated _redirects (${routingEntries.length} entries)`)
|
|
606
|
+
}
|
|
607
|
+
|
|
565
608
|
onProgress(`\nPre-rendered ${renderedFiles.length} pages across ${localeConfigs.length} locale(s)`)
|
|
566
609
|
|
|
567
610
|
return {
|
|
@@ -387,6 +387,34 @@ function resolveMounts(pathsConfig, sitePath, pagesPath) {
|
|
|
387
387
|
* - Simple: "1", "2", "3"
|
|
388
388
|
* - Decimal ordering: "1.5" (between 1 and 2), "2.5" (between 2 and 3)
|
|
389
389
|
*/
|
|
390
|
+
/**
|
|
391
|
+
* Extract the first H1 text from a ProseMirror document.
|
|
392
|
+
* Returns null if no H1 is found.
|
|
393
|
+
*/
|
|
394
|
+
function extractH1(proseMirrorDoc) {
|
|
395
|
+
const nodes = proseMirrorDoc?.content || proseMirrorDoc
|
|
396
|
+
if (!Array.isArray(nodes)) return null
|
|
397
|
+
|
|
398
|
+
for (const node of nodes) {
|
|
399
|
+
if (node.type === 'heading' && node.attrs?.level === 1 && node.content) {
|
|
400
|
+
return node.content.map(n => n.text || '').join('').trim() || null
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return null
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Prettify a slug into a human-readable title.
|
|
408
|
+
* Strips numeric prefixes, replaces hyphens with spaces, title-cases.
|
|
409
|
+
*/
|
|
410
|
+
function prettifySlug(slug) {
|
|
411
|
+
const stripped = slug.replace(/^\d+[-.]?\s*/, '')
|
|
412
|
+
return stripped
|
|
413
|
+
.split('-')
|
|
414
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
415
|
+
.join(' ')
|
|
416
|
+
}
|
|
417
|
+
|
|
390
418
|
function parseNumericPrefix(filename) {
|
|
391
419
|
const match = filename.match(/^(\d+(?:\.\d+)*)-?(.*)$/)
|
|
392
420
|
if (match) {
|
|
@@ -587,7 +615,7 @@ async function processFileAsPage(filePath, fileName, siteRoot, parentRoute) {
|
|
|
587
615
|
sourcePath: null,
|
|
588
616
|
id: null,
|
|
589
617
|
isIndex: false,
|
|
590
|
-
title: pageName,
|
|
618
|
+
title: extractH1(section.content) || prettifySlug(pageName),
|
|
591
619
|
description: '',
|
|
592
620
|
label: null,
|
|
593
621
|
lastModified: fileStat.mtime?.toISOString() || null,
|
|
@@ -875,6 +903,65 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
875
903
|
// they're just filtered from navigation. This allows direct linking to hidden pages.
|
|
876
904
|
// if (pageConfig.hidden) return null
|
|
877
905
|
|
|
906
|
+
// Redirect pages have no content — resolve target and return early
|
|
907
|
+
if (pageConfig.redirect) {
|
|
908
|
+
const route = isIndex ? parentRoute
|
|
909
|
+
: parentRoute === '/' ? `/${pageName}` : `${parentRoute}/${pageName}`
|
|
910
|
+
const redirect = pageConfig.redirect
|
|
911
|
+
// Resolve relative redirects against the page's own route
|
|
912
|
+
const target = redirect.startsWith('/') || redirect.startsWith('http')
|
|
913
|
+
? redirect
|
|
914
|
+
: (route === '/' ? `/${redirect}` : `${route}/${redirect}`)
|
|
915
|
+
|
|
916
|
+
return {
|
|
917
|
+
page: {
|
|
918
|
+
route,
|
|
919
|
+
sourcePath: parentRoute === '/' ? `/${pageName}` : `${parentRoute}/${pageName}`,
|
|
920
|
+
id: pageConfig.id || null,
|
|
921
|
+
isIndex,
|
|
922
|
+
title: pageConfig.title || prettifySlug(pageName),
|
|
923
|
+
description: pageConfig.description || '',
|
|
924
|
+
label: pageConfig.label || null,
|
|
925
|
+
hidden: pageConfig.hidden || false,
|
|
926
|
+
hideInHeader: pageConfig.hideInHeader || false,
|
|
927
|
+
hideInFooter: pageConfig.hideInFooter || false,
|
|
928
|
+
layout: {},
|
|
929
|
+
seo: { noindex: true },
|
|
930
|
+
redirect: target,
|
|
931
|
+
sections: []
|
|
932
|
+
},
|
|
933
|
+
assetCollection: { assets: {}, hasExplicitPoster: new Set(), hasExplicitPreview: new Set() },
|
|
934
|
+
iconCollection: { icons: new Set(), bySource: new Map() }
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Rewrite pages proxy to an external site — no content, no HTML output
|
|
939
|
+
if (pageConfig.rewrite) {
|
|
940
|
+
const route = isIndex ? parentRoute
|
|
941
|
+
: parentRoute === '/' ? `/${pageName}` : `${parentRoute}/${pageName}`
|
|
942
|
+
|
|
943
|
+
return {
|
|
944
|
+
page: {
|
|
945
|
+
route,
|
|
946
|
+
sourcePath: parentRoute === '/' ? `/${pageName}` : `${parentRoute}/${pageName}`,
|
|
947
|
+
id: pageConfig.id || null,
|
|
948
|
+
isIndex,
|
|
949
|
+
title: pageConfig.title || prettifySlug(pageName),
|
|
950
|
+
description: pageConfig.description || '',
|
|
951
|
+
label: pageConfig.label || null,
|
|
952
|
+
hidden: pageConfig.hidden || false,
|
|
953
|
+
hideInHeader: pageConfig.hideInHeader || false,
|
|
954
|
+
hideInFooter: pageConfig.hideInFooter || false,
|
|
955
|
+
layout: {},
|
|
956
|
+
seo: { noindex: true },
|
|
957
|
+
rewrite: pageConfig.rewrite,
|
|
958
|
+
sections: []
|
|
959
|
+
},
|
|
960
|
+
assetCollection: { assets: {}, hasExplicitPoster: new Set(), hasExplicitPreview: new Set() },
|
|
961
|
+
iconCollection: { icons: new Set(), bySource: new Map() }
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
878
965
|
let hierarchicalSections = []
|
|
879
966
|
let pageAssetCollection = {
|
|
880
967
|
assets: {},
|
|
@@ -1103,7 +1190,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
1103
1190
|
sourcePath, // Original folder-based path (for ancestor checking in navigation)
|
|
1104
1191
|
id: pageConfig.id || null, // Stable page ID for page: links (survives reorganization)
|
|
1105
1192
|
isIndex, // Marks this page as the index for its parent route
|
|
1106
|
-
title: pageConfig.title || pageName,
|
|
1193
|
+
title: pageConfig.title || extractH1(hierarchicalSections[0]?.content) || prettifySlug(pageName),
|
|
1107
1194
|
description: pageConfig.description || '',
|
|
1108
1195
|
label: pageConfig.label || null, // Short label for navigation (defaults to title)
|
|
1109
1196
|
lastModified: lastModified?.toISOString(),
|
|
@@ -1657,8 +1744,14 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1657
1744
|
* @param {string} foundationPath - Path to foundation directory
|
|
1658
1745
|
* @returns {Promise<Object>} Foundation variables or empty object
|
|
1659
1746
|
*/
|
|
1660
|
-
|
|
1661
|
-
|
|
1747
|
+
/**
|
|
1748
|
+
* Load foundation schema data needed by the content collector.
|
|
1749
|
+
*
|
|
1750
|
+
* @param {string} foundationPath - Path to foundation directory
|
|
1751
|
+
* @returns {Promise<{ vars: Object, layoutNames: Set<string> }>}
|
|
1752
|
+
*/
|
|
1753
|
+
async function loadFoundationInfo(foundationPath) {
|
|
1754
|
+
if (!foundationPath) return { vars: {}, layoutNames: new Set() }
|
|
1662
1755
|
|
|
1663
1756
|
// Try dist/meta/schema.json first (built foundation), then root schema.json
|
|
1664
1757
|
const distSchemaPath = join(foundationPath, 'dist', 'meta', 'schema.json')
|
|
@@ -1667,17 +1760,20 @@ async function loadFoundationVars(foundationPath) {
|
|
|
1667
1760
|
const schemaPath = existsSync(distSchemaPath) ? distSchemaPath : rootSchemaPath
|
|
1668
1761
|
|
|
1669
1762
|
if (!existsSync(schemaPath)) {
|
|
1670
|
-
return {}
|
|
1763
|
+
return { vars: {}, layoutNames: new Set() }
|
|
1671
1764
|
}
|
|
1672
1765
|
|
|
1673
1766
|
try {
|
|
1674
1767
|
const schemaContent = await readFile(schemaPath, 'utf8')
|
|
1675
1768
|
const schema = JSON.parse(schemaContent)
|
|
1676
1769
|
// Foundation config is in _self, support both 'vars' (new) and 'themeVars' (legacy)
|
|
1677
|
-
|
|
1770
|
+
const vars = schema._self?.vars || schema._self?.themeVars || schema.themeVars || {}
|
|
1771
|
+
// Layout names from _layouts (keys are layout component names)
|
|
1772
|
+
const layoutNames = new Set(schema._layouts ? Object.keys(schema._layouts) : [])
|
|
1773
|
+
return { vars, layoutNames }
|
|
1678
1774
|
} catch (err) {
|
|
1679
1775
|
console.warn('[content-collector] Failed to load foundation schema:', err.message)
|
|
1680
|
-
return {}
|
|
1776
|
+
return { vars: {}, layoutNames: new Set() }
|
|
1681
1777
|
}
|
|
1682
1778
|
}
|
|
1683
1779
|
|
|
@@ -1744,63 +1840,36 @@ async function collectAreasFromDir(dir, siteRoot, routePrefix = '/layout') {
|
|
|
1744
1840
|
return result
|
|
1745
1841
|
}
|
|
1746
1842
|
|
|
1747
|
-
/**
|
|
1748
|
-
* Check if a directory looks like a named layout (contains area-like .md files or area subdirs)
|
|
1749
|
-
* vs an area in folder form (contains section content processed by processPage).
|
|
1750
|
-
*
|
|
1751
|
-
* Heuristic: if a directory contains .md files at the top level AND no page.yml,
|
|
1752
|
-
* it's a named layout (those .md files are its area definitions).
|
|
1753
|
-
* If it has page.yml or looks like a page directory, it's an area folder.
|
|
1754
|
-
*
|
|
1755
|
-
* @param {string} dirPath - Path to the directory
|
|
1756
|
-
* @returns {Promise<boolean>} True if this looks like a named layout directory
|
|
1757
|
-
*/
|
|
1758
|
-
async function isNamedLayoutDir(dirPath) {
|
|
1759
|
-
// If it has page.yml, it's an area folder (processPage will handle it)
|
|
1760
|
-
if (existsSync(join(dirPath, 'page.yml'))) return false
|
|
1761
|
-
|
|
1762
|
-
const entries = await readdir(dirPath)
|
|
1763
|
-
// If directory contains .md files but no page.yml, it's a named layout
|
|
1764
|
-
return entries.some(e => e.endsWith('.md') && !e.startsWith('_') && !e.startsWith('.'))
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
1843
|
/**
|
|
1768
1844
|
* Collect layout areas from the layout/ directory, including named layout subdirectories.
|
|
1769
1845
|
*
|
|
1770
1846
|
* Root-level .md files and area directories form the "default" layout's areas.
|
|
1771
|
-
* Subdirectories that
|
|
1772
|
-
* each with its own set of areas.
|
|
1847
|
+
* Subdirectories that match a foundation-declared layout name are named layouts,
|
|
1848
|
+
* each with its own set of areas. All other subdirectories are folder-form areas
|
|
1849
|
+
* for the default layout (multi-section areas).
|
|
1773
1850
|
*
|
|
1774
1851
|
* @param {string} layoutDir - Path to layout directory
|
|
1775
1852
|
* @param {string} siteRoot - Path to site root
|
|
1853
|
+
* @param {Set<string>} layoutNames - Layout names declared in the foundation schema
|
|
1776
1854
|
* @returns {Promise<Object>} { layouts }
|
|
1777
1855
|
*/
|
|
1778
|
-
async function collectLayouts(layoutDir, siteRoot) {
|
|
1856
|
+
async function collectLayouts(layoutDir, siteRoot, layoutNames = new Set()) {
|
|
1779
1857
|
if (!existsSync(layoutDir)) {
|
|
1780
1858
|
return { layouts: null }
|
|
1781
1859
|
}
|
|
1782
1860
|
|
|
1783
1861
|
const entries = await readdir(layoutDir, { withFileTypes: true })
|
|
1784
1862
|
|
|
1785
|
-
//
|
|
1786
|
-
//
|
|
1787
|
-
//
|
|
1788
|
-
const defaultAreaFiles = []
|
|
1789
|
-
const defaultAreaDirs = []
|
|
1863
|
+
// Identify named layout directories: only subdirectories whose name matches
|
|
1864
|
+
// a layout component declared in the foundation. All others are folder-form
|
|
1865
|
+
// areas for the default layout (e.g., layout/header/ with multiple sections).
|
|
1790
1866
|
const namedLayoutDirs = []
|
|
1791
1867
|
|
|
1792
1868
|
for (const entry of entries) {
|
|
1793
1869
|
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue
|
|
1794
1870
|
|
|
1795
|
-
if (entry.
|
|
1796
|
-
|
|
1797
|
-
} else if (entry.isDirectory()) {
|
|
1798
|
-
const dirPath = join(layoutDir, entry.name)
|
|
1799
|
-
if (await isNamedLayoutDir(dirPath)) {
|
|
1800
|
-
namedLayoutDirs.push(entry)
|
|
1801
|
-
} else {
|
|
1802
|
-
defaultAreaDirs.push(entry)
|
|
1803
|
-
}
|
|
1871
|
+
if (entry.isDirectory() && layoutNames.has(entry.name)) {
|
|
1872
|
+
namedLayoutDirs.push(entry)
|
|
1804
1873
|
}
|
|
1805
1874
|
}
|
|
1806
1875
|
|
|
@@ -1861,8 +1930,8 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1861
1930
|
: join(sitePath, 'layout')
|
|
1862
1931
|
const rawThemeConfig = await readYamlFile(join(sitePath, 'theme.yml'))
|
|
1863
1932
|
|
|
1864
|
-
// Load foundation vars and process theme
|
|
1865
|
-
const foundationVars = await
|
|
1933
|
+
// Load foundation info (vars + layout names) and process theme
|
|
1934
|
+
const { vars: foundationVars, layoutNames: layoutNames } = await loadFoundationInfo(foundationPath)
|
|
1866
1935
|
const { config: processedTheme, css: themeCSS, warnings } = buildTheme(rawThemeConfig, { foundationVars })
|
|
1867
1936
|
|
|
1868
1937
|
// Log theme warnings
|
|
@@ -1893,7 +1962,7 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1893
1962
|
const { mode: rootContentMode } = await readFolderConfig(pagesPath, 'sections')
|
|
1894
1963
|
|
|
1895
1964
|
// Collect layout areas from layout/ directory (including named layout subdirectories)
|
|
1896
|
-
const { layouts } = await collectLayouts(layoutPath, sitePath)
|
|
1965
|
+
const { layouts } = await collectLayouts(layoutPath, sitePath, layoutNames)
|
|
1897
1966
|
|
|
1898
1967
|
// Site-level layout name (from site.yml layout: field)
|
|
1899
1968
|
const siteLayoutName = typeof siteConfig.layout === 'string' ? siteConfig.layout
|
package/src/site/plugin.js
CHANGED
|
@@ -39,6 +39,9 @@ import { processAdvancedAssets } from './advanced-processors.js'
|
|
|
39
39
|
import { processCollections, writeCollectionFiles } from './collection-processor.js'
|
|
40
40
|
import { executeFetch, mergeDataIntoContent } from './data-fetcher.js'
|
|
41
41
|
|
|
42
|
+
// BCP 47 locale code pattern: en, zh-CN, zh-Hant, pt-BR, fr-CA, sr-Latn, etc.
|
|
43
|
+
const LOCALE_RE = '[a-z]{2,3}(?:-[A-Za-z]{2,4})?'
|
|
44
|
+
|
|
42
45
|
/**
|
|
43
46
|
* Execute all fetches for site content (used in dev mode)
|
|
44
47
|
* Collects fetchedData for DataStore pre-population at runtime
|
|
@@ -772,7 +775,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
772
775
|
}
|
|
773
776
|
|
|
774
777
|
// Handle locale-prefixed site-content request (e.g., /es/site-content.json)
|
|
775
|
-
const localeContentMatch = req.url.match(
|
|
778
|
+
const localeContentMatch = req.url.match(new RegExp(`^\\/(${LOCALE_RE})\\/site-content\\.json$`))
|
|
776
779
|
if (localeContentMatch) {
|
|
777
780
|
const locale = localeContentMatch[1]
|
|
778
781
|
const translatedContent = await getTranslatedContent(locale)
|
|
@@ -808,7 +811,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
808
811
|
}
|
|
809
812
|
|
|
810
813
|
// Serve search-index.json in dev mode (supports locale prefixes)
|
|
811
|
-
const searchIndexMatch = req.url.match(
|
|
814
|
+
const searchIndexMatch = req.url.match(new RegExp(`^(?:\\/(${LOCALE_RE}))?\\/search-index\\.json$`))
|
|
812
815
|
if (searchIndexMatch && siteContent) {
|
|
813
816
|
const searchEnabled = searchPluginConfig.enabled !== false && isSearchEnabled(siteContent)
|
|
814
817
|
if (searchEnabled) {
|
|
@@ -830,7 +833,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
830
833
|
}
|
|
831
834
|
|
|
832
835
|
// Handle localized collection data (e.g., /fr/data/articles.json)
|
|
833
|
-
const localeDataMatch = req.url.match(
|
|
836
|
+
const localeDataMatch = req.url.match(new RegExp(`^\\/(${LOCALE_RE})\\/data\\/(.+\\.json)$`))
|
|
834
837
|
if (localeDataMatch) {
|
|
835
838
|
const locale = localeDataMatch[1]
|
|
836
839
|
const filename = localeDataMatch[2]
|
|
@@ -883,7 +886,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
883
886
|
let activeLocale = null
|
|
884
887
|
|
|
885
888
|
if (ctx?.originalUrl) {
|
|
886
|
-
const localeMatch = ctx.originalUrl.match(
|
|
889
|
+
const localeMatch = ctx.originalUrl.match(new RegExp(`^\\/(${LOCALE_RE})(\\/|$)`))
|
|
887
890
|
if (localeMatch) {
|
|
888
891
|
activeLocale = localeMatch[1]
|
|
889
892
|
const translatedContent = await getTranslatedContent(activeLocale)
|