@uniweb/build 0.8.41 → 0.9.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 +68 -4
- package/src/site/config.js +22 -0
- package/src/site/content-collector.js +13 -0
- package/src/site/plugin.js +77 -0
- package/src/site/split-content.js +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"@uniweb/theming": "0.1.3"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"@uniweb/
|
|
58
|
-
"@uniweb/
|
|
59
|
-
"@uniweb/
|
|
57
|
+
"@uniweb/runtime": "0.7.0",
|
|
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.
|
|
68
|
+
"@uniweb/core": "0.6.0"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
71
71
|
"vite": {
|
package/src/prerender.js
CHANGED
|
@@ -12,6 +12,7 @@ import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
|
12
12
|
import { join, dirname, resolve } from 'node:path'
|
|
13
13
|
import { pathToFileURL } from 'node:url'
|
|
14
14
|
import { executeFetch, mergeDataIntoContent, singularize } from './site/data-fetcher.js'
|
|
15
|
+
import { shouldSplitContent } from './site/split-content.js'
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Resolve an extension URL to a filesystem path for prerender.
|
|
@@ -302,9 +303,12 @@ async function discoverLocaleContents(distDir, defaultContent) {
|
|
|
302
303
|
*
|
|
303
304
|
* @param {string} html - HTML with prerendered content already injected
|
|
304
305
|
* @param {Object} siteContent - Site content JSON
|
|
306
|
+
* @param {Object} [options]
|
|
307
|
+
* @param {boolean} [options.splitContent=false] - Whether split content mode is active
|
|
308
|
+
* @param {string|null} [options.currentRoute=null] - Route of the page this HTML is for
|
|
305
309
|
* @returns {string} HTML with build-specific data injected
|
|
306
310
|
*/
|
|
307
|
-
function injectBuildData(html, siteContent) {
|
|
311
|
+
function injectBuildData(html, siteContent, { splitContent = false, currentRoute = null } = {}) {
|
|
308
312
|
let result = html
|
|
309
313
|
|
|
310
314
|
// Inject theme CSS if not already present
|
|
@@ -317,11 +321,26 @@ function injectBuildData(html, siteContent) {
|
|
|
317
321
|
|
|
318
322
|
// Inject site content as JSON for hydration
|
|
319
323
|
// Strip CSS from theme (it's already in a <style> tag)
|
|
320
|
-
|
|
324
|
+
let contentForJson = { ...siteContent }
|
|
321
325
|
if (contentForJson.theme?.css) {
|
|
322
326
|
contentForJson.theme = { ...contentForJson.theme }
|
|
323
327
|
delete contentForJson.theme.css
|
|
324
328
|
}
|
|
329
|
+
|
|
330
|
+
// Split mode: strip sections from all pages except the current one.
|
|
331
|
+
// Dynamic templates (isDynamic) keep their sections — needed by _createDynamicPage().
|
|
332
|
+
if (splitContent) {
|
|
333
|
+
contentForJson = {
|
|
334
|
+
...contentForJson,
|
|
335
|
+
pages: contentForJson.pages.map(page => {
|
|
336
|
+
if (page.route === currentRoute) return page
|
|
337
|
+
if (page.isDynamic) return page
|
|
338
|
+
const { sections, ...metadata } = page
|
|
339
|
+
return metadata
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
325
344
|
const contentScript = `<script id="__SITE_CONTENT__" type="application/json">${JSON.stringify(contentForJson).replace(/</g, '\\u003c')}</script>`
|
|
326
345
|
if (result.includes('__SITE_CONTENT__')) {
|
|
327
346
|
// Replace existing site content with updated version (includes expanded dynamic routes)
|
|
@@ -462,6 +481,30 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
462
481
|
siteContent.pages = expandDynamicPages(siteContent.pages, pageFetchedData, onProgress)
|
|
463
482
|
}
|
|
464
483
|
|
|
484
|
+
// Determine whether to split content (after dynamic expansion, after data fetches)
|
|
485
|
+
const splitContent = shouldSplitContent(
|
|
486
|
+
siteContent.config?.build?.splitContent,
|
|
487
|
+
siteContent.pages
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
// Emit per-page content files (after dynamic expansion so expanded pages get their own files)
|
|
491
|
+
if (splitContent) {
|
|
492
|
+
onProgress('Writing per-page content files...')
|
|
493
|
+
const pagesBaseDir = routePrefix
|
|
494
|
+
? join(distDir, routePrefix.replace(/^\//, ''), '_pages')
|
|
495
|
+
: join(distDir, '_pages')
|
|
496
|
+
|
|
497
|
+
for (const page of siteContent.pages) {
|
|
498
|
+
if (!page.sections?.length) continue // Skip content-less pages
|
|
499
|
+
if (page.isDynamic) continue // Templates stay inline
|
|
500
|
+
const routePath = page.route === '/' ? '/index' : page.route
|
|
501
|
+
const outputPath = join(pagesBaseDir, `${routePath.replace(/^\//, '')}.json`)
|
|
502
|
+
await mkdir(dirname(outputPath), { recursive: true })
|
|
503
|
+
await writeFile(outputPath, JSON.stringify({ sections: page.sections }))
|
|
504
|
+
onProgress(` → _pages${routePath}.json`)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
465
508
|
// Load the HTML shell for this locale
|
|
466
509
|
const shellPath = existsSync(htmlPath) ? htmlPath : join(distDir, 'index.html')
|
|
467
510
|
const htmlShell = await readFile(shellPath, 'utf8')
|
|
@@ -571,7 +614,10 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
571
614
|
})
|
|
572
615
|
|
|
573
616
|
// Build-specific: theme CSS, __SITE_CONTENT__, icon cache
|
|
574
|
-
html = injectBuildData(html, siteContent
|
|
617
|
+
html = injectBuildData(html, siteContent, {
|
|
618
|
+
splitContent,
|
|
619
|
+
currentRoute: page.route,
|
|
620
|
+
})
|
|
575
621
|
|
|
576
622
|
// Output to the locale-prefixed route
|
|
577
623
|
const outputPath = getOutputPath(distDir, outputRoute)
|
|
@@ -583,7 +629,10 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
583
629
|
}
|
|
584
630
|
|
|
585
631
|
// Write 404.html — shared logic from @uniweb/runtime/ssr
|
|
586
|
-
const fallbackBaseHtml = injectBuildData(htmlShell, siteContent
|
|
632
|
+
const fallbackBaseHtml = injectBuildData(htmlShell, siteContent, {
|
|
633
|
+
splitContent,
|
|
634
|
+
currentRoute: null, // 404 has no current page — manifest only
|
|
635
|
+
})
|
|
587
636
|
const { html: notFoundHtml, hasNotFoundPage } = generate404Html({
|
|
588
637
|
baseHtml: fallbackBaseHtml,
|
|
589
638
|
website,
|
|
@@ -595,6 +644,21 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
595
644
|
await writeFile(join(fallbackDir, '404.html'), notFoundHtml)
|
|
596
645
|
const fallbackNote = hasNotFoundPage ? '404 page + SPA fallback' : 'SPA fallback'
|
|
597
646
|
onProgress(` → ${routePrefix || ''}404.html (${fallbackNote})`)
|
|
647
|
+
|
|
648
|
+
// Rewrite site-content.json as lightweight manifest (for shell/CF mode)
|
|
649
|
+
// Must happen after all HTML files are written since some code re-reads it.
|
|
650
|
+
if (splitContent) {
|
|
651
|
+
const manifest = {
|
|
652
|
+
...siteContent,
|
|
653
|
+
pages: siteContent.pages.map(page => {
|
|
654
|
+
if (page.isDynamic) return page
|
|
655
|
+
const { sections, ...metadata } = page
|
|
656
|
+
return metadata
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
await writeFile(localeContentPath, JSON.stringify(manifest))
|
|
660
|
+
onProgress('Rewrote site-content.json as lightweight manifest')
|
|
661
|
+
}
|
|
598
662
|
}
|
|
599
663
|
|
|
600
664
|
// Generate _redirects file for Cloudflare Pages / Netlify
|
package/src/site/config.js
CHANGED
|
@@ -153,6 +153,28 @@ export function readSiteConfig(siteRoot) {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Read and parse intelligence.yml configuration (AI knowledge page settings).
|
|
158
|
+
*
|
|
159
|
+
* Returns the non-secret fields. The `apiKey` field (if present) uses the
|
|
160
|
+
* `env:VAR_NAME` syntax for CLI/local dev — the actual key is never stored
|
|
161
|
+
* in artifacts or published content.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} siteRoot - Path to site directory
|
|
164
|
+
* @returns {Object|null} Parsed intelligence config, or null if no file exists
|
|
165
|
+
*/
|
|
166
|
+
export function readIntelligenceConfig(siteRoot) {
|
|
167
|
+
const configPath = resolve(siteRoot, 'intelligence.yml')
|
|
168
|
+
if (!existsSync(configPath)) return null
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
return yaml.load(readFileSync(configPath, 'utf8')) || {}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.warn('[site-config] Failed to read intelligence.yml:', err.message)
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
156
178
|
/**
|
|
157
179
|
* Create a complete Vite configuration for a Uniweb site
|
|
158
180
|
*
|
|
@@ -1210,6 +1210,9 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
1210
1210
|
hideInHeader: pageConfig.hideInHeader || false, // Hide from header nav
|
|
1211
1211
|
hideInFooter: pageConfig.hideInFooter || false, // Hide from footer nav
|
|
1212
1212
|
|
|
1213
|
+
// Knowledge page — content feeds AI pipeline instead of (or in addition to) rendering
|
|
1214
|
+
...(pageConfig.knowledge != null ? { knowledge: pageConfig.knowledge } : {}),
|
|
1215
|
+
|
|
1213
1216
|
// Layout options (named layout + per-page overrides)
|
|
1214
1217
|
layout: {
|
|
1215
1218
|
...(resolvedLayoutName ? { name: resolvedLayoutName } : {}),
|
|
@@ -1233,6 +1236,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
1233
1236
|
: undefined)
|
|
1234
1237
|
),
|
|
1235
1238
|
|
|
1239
|
+
hasContent: hierarchicalSections.length > 0,
|
|
1236
1240
|
sections: hierarchicalSections
|
|
1237
1241
|
},
|
|
1238
1242
|
assetCollection: pageAssetCollection,
|
|
@@ -1565,6 +1569,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1565
1569
|
hidden: dirConfig.hidden || false,
|
|
1566
1570
|
hideInHeader: dirConfig.hideInHeader || false,
|
|
1567
1571
|
hideInFooter: dirConfig.hideInFooter || false,
|
|
1572
|
+
...(dirConfig.knowledge != null ? { knowledge: dirConfig.knowledge } : {}),
|
|
1568
1573
|
layout: {
|
|
1569
1574
|
...(effectiveLayout ? { name: effectiveLayout } : {}),
|
|
1570
1575
|
...(containerLayoutObj.hide ? { hide: containerLayoutObj.hide } : {}),
|
|
@@ -1577,6 +1582,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1577
1582
|
priority: dirConfig.seo?.priority || null
|
|
1578
1583
|
},
|
|
1579
1584
|
fetch: parseFetchConfig(dirConfig.fetch) || null,
|
|
1585
|
+
hasContent: false,
|
|
1580
1586
|
sections: [],
|
|
1581
1587
|
order: typeof dirConfig.order === 'number' ? dirConfig.order : undefined
|
|
1582
1588
|
}
|
|
@@ -1657,6 +1663,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1657
1663
|
hidden: dirConfig.hidden || false,
|
|
1658
1664
|
hideInHeader: dirConfig.hideInHeader || false,
|
|
1659
1665
|
hideInFooter: dirConfig.hideInFooter || false,
|
|
1666
|
+
...(dirConfig.knowledge != null ? { knowledge: dirConfig.knowledge } : {}),
|
|
1660
1667
|
layout: {
|
|
1661
1668
|
...(effectiveLayout ? { name: effectiveLayout } : {}),
|
|
1662
1669
|
...(containerLayoutObj.hide ? { hide: containerLayoutObj.hide } : {}),
|
|
@@ -1669,6 +1676,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1669
1676
|
priority: dirConfig.seo?.priority || null
|
|
1670
1677
|
},
|
|
1671
1678
|
fetch: null,
|
|
1679
|
+
hasContent: false,
|
|
1672
1680
|
sections: [],
|
|
1673
1681
|
order: typeof dirConfig.order === 'number' ? dirConfig.order : undefined
|
|
1674
1682
|
}
|
|
@@ -2051,10 +2059,15 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2051
2059
|
// Convert versionedScopes Map to plain object for JSON serialization
|
|
2052
2060
|
const versionedScopesObj = Object.fromEntries(versionedScopes)
|
|
2053
2061
|
|
|
2062
|
+
// Read intelligence.yml if it exists (AI knowledge page settings)
|
|
2063
|
+
const intelligenceConfig = await readYamlFile(join(sitePath, 'intelligence.yml'))
|
|
2064
|
+
const hasIntelligence = intelligenceConfig && Object.keys(intelligenceConfig).length > 0
|
|
2065
|
+
|
|
2054
2066
|
return {
|
|
2055
2067
|
config: {
|
|
2056
2068
|
...siteConfig,
|
|
2057
2069
|
fetch: parseFetchConfig(siteConfig.fetch),
|
|
2070
|
+
...(hasIntelligence && { intelligence: intelligenceConfig }),
|
|
2058
2071
|
},
|
|
2059
2072
|
theme: {
|
|
2060
2073
|
...processedTheme,
|
package/src/site/plugin.js
CHANGED
|
@@ -38,10 +38,41 @@ import { processAssets, rewriteSiteContentPaths } from './asset-processor.js'
|
|
|
38
38
|
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
|
+
import { shouldSplitContent } from './split-content.js'
|
|
41
42
|
|
|
42
43
|
// BCP 47 locale code pattern: en, zh-CN, zh-Hant, pt-BR, fr-CA, sr-Latn, etc.
|
|
43
44
|
const LOCALE_RE = '[a-z]{2,3}(?:-[A-Za-z]{2,4})?'
|
|
44
45
|
|
|
46
|
+
const FAVICON_AUTODETECT = ['favicon.svg', 'favicon.ico', 'favicon.png']
|
|
47
|
+
|
|
48
|
+
function faviconTypeFor(href) {
|
|
49
|
+
const ext = href.split('?')[0].split('.').pop()?.toLowerCase()
|
|
50
|
+
if (ext === 'svg') return 'image/svg+xml'
|
|
51
|
+
if (ext === 'ico') return 'image/x-icon'
|
|
52
|
+
if (ext === 'png') return 'image/png'
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a favicon href for index.html injection.
|
|
58
|
+
* - If config.favicon is a string, use it as-is.
|
|
59
|
+
* - Otherwise, scan the public dir for favicon.{svg,ico,png} and link the first match.
|
|
60
|
+
* - Returns null if nothing found.
|
|
61
|
+
*/
|
|
62
|
+
function resolveFaviconHref(configFavicon, publicDir, basePath) {
|
|
63
|
+
if (typeof configFavicon === 'string' && configFavicon.trim()) {
|
|
64
|
+
return configFavicon.trim()
|
|
65
|
+
}
|
|
66
|
+
if (!publicDir) return null
|
|
67
|
+
for (const name of FAVICON_AUTODETECT) {
|
|
68
|
+
if (existsSync(resolve(publicDir, name))) {
|
|
69
|
+
const base = (basePath || '/').replace(/\/$/, '')
|
|
70
|
+
return `${base}/${name}`
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
45
76
|
/**
|
|
46
77
|
* Execute all fetches for site content (used in dev mode)
|
|
47
78
|
* Collects fetchedData for DataStore pre-population at runtime
|
|
@@ -372,6 +403,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
372
403
|
|
|
373
404
|
let siteContent = null
|
|
374
405
|
let resolvedSitePath = null
|
|
406
|
+
let resolvedPublicDir = null
|
|
375
407
|
let resolvedOutDir = null
|
|
376
408
|
let isProduction = false
|
|
377
409
|
let watcher = null
|
|
@@ -496,6 +528,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
496
528
|
|
|
497
529
|
async configResolved(config) {
|
|
498
530
|
resolvedSitePath = resolve(config.root, sitePath)
|
|
531
|
+
resolvedPublicDir = config.publicDir || resolve(config.root, 'public')
|
|
499
532
|
resolvedOutDir = resolve(config.root, config.build.outDir)
|
|
500
533
|
isProduction = config.command === 'build'
|
|
501
534
|
basePath = config.base || '/'
|
|
@@ -871,6 +904,18 @@ export function siteContentPlugin(options = {}) {
|
|
|
871
904
|
}
|
|
872
905
|
}
|
|
873
906
|
|
|
907
|
+
// Serve per-page content on demand (dev mode, split content)
|
|
908
|
+
if (siteContent && req.url.startsWith('/_pages/')) {
|
|
909
|
+
const routePath = req.url.replace(/^\/_pages/, '').replace(/\.json$/, '')
|
|
910
|
+
const route = routePath === '/index' ? '/' : routePath
|
|
911
|
+
const page = siteContent.pages.find(p => p.route === route)
|
|
912
|
+
if (page?.sections) {
|
|
913
|
+
res.setHeader('Content-Type', 'application/json')
|
|
914
|
+
res.end(JSON.stringify({ sections: page.sections }))
|
|
915
|
+
return
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
874
919
|
next()
|
|
875
920
|
})
|
|
876
921
|
},
|
|
@@ -940,6 +985,16 @@ export function siteContentPlugin(options = {}) {
|
|
|
940
985
|
}
|
|
941
986
|
}
|
|
942
987
|
|
|
988
|
+
// Inject favicon link
|
|
989
|
+
// Source 1: contentToInject.config.favicon (matches Cloudflare publish schema)
|
|
990
|
+
// Source 2: auto-detect public/favicon.{ico,svg,png}
|
|
991
|
+
const faviconHref = resolveFaviconHref(contentToInject?.config?.favicon, resolvedPublicDir, basePath)
|
|
992
|
+
if (faviconHref) {
|
|
993
|
+
const type = faviconTypeFor(faviconHref)
|
|
994
|
+
const typeAttr = type ? ` type="${type}"` : ''
|
|
995
|
+
headInjection += ` <link rel="icon"${typeAttr} href="${faviconHref}">\n`
|
|
996
|
+
}
|
|
997
|
+
|
|
943
998
|
// Inject content as JSON script tag
|
|
944
999
|
if (inject) {
|
|
945
1000
|
headInjection += ` <script type="application/json" id="${variableName}">${JSON.stringify(contentToInject).replace(/</g, '\\u003c')}</script>\n`
|
|
@@ -1048,7 +1103,29 @@ export function siteContentPlugin(options = {}) {
|
|
|
1048
1103
|
// Note: theme.css is kept here so prerender can inject it into HTML
|
|
1049
1104
|
// Prerender will strip it from the JSON it injects into each page
|
|
1050
1105
|
|
|
1106
|
+
// Check if split content mode is active
|
|
1107
|
+
const splitContent = shouldSplitContent(
|
|
1108
|
+
finalContent.config?.build?.splitContent,
|
|
1109
|
+
finalContent.pages
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
if (splitContent) {
|
|
1113
|
+
// Emit per-page content files as baseline (prerender overwrites with post-fetch versions)
|
|
1114
|
+
for (const page of finalContent.pages) {
|
|
1115
|
+
if (!page.sections?.length) continue
|
|
1116
|
+
if (page.isDynamic) continue
|
|
1117
|
+
const routePath = page.route === '/' ? 'index' : page.route.replace(/^\//, '')
|
|
1118
|
+
this.emitFile({
|
|
1119
|
+
type: 'asset',
|
|
1120
|
+
fileName: `_pages/${routePath}.json`,
|
|
1121
|
+
source: JSON.stringify({ sections: page.sections })
|
|
1122
|
+
})
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1051
1126
|
// Emit content as JSON file in production build
|
|
1127
|
+
// Always emit full content — prerender needs sections to render pages.
|
|
1128
|
+
// Prerender will rewrite this as a lightweight manifest when split mode is active.
|
|
1052
1129
|
this.emitFile({
|
|
1053
1130
|
type: 'asset',
|
|
1054
1131
|
fileName: filename,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split Content Helper
|
|
3
|
+
*
|
|
4
|
+
* Shared utility to determine whether a site should use split page content.
|
|
5
|
+
* Used by the site plugin, prerender, and unicloud.
|
|
6
|
+
*
|
|
7
|
+
* @module @uniweb/build/site
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const THRESHOLD = 100 * 1024 // 100KB uncompressed JSON
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Determine whether site content should be split into per-page files.
|
|
14
|
+
*
|
|
15
|
+
* @param {boolean|string|undefined} splitConfig - Value from site.yml build.splitContent
|
|
16
|
+
* - true: always split
|
|
17
|
+
* - false: never split
|
|
18
|
+
* - 'auto' or undefined: split when total sections payload > 100KB
|
|
19
|
+
* @param {Array} pages - Array of page objects with sections arrays
|
|
20
|
+
* @returns {boolean} Whether to split content
|
|
21
|
+
*/
|
|
22
|
+
export function shouldSplitContent(splitConfig, pages) {
|
|
23
|
+
if (splitConfig === true) return true
|
|
24
|
+
if (splitConfig === false) return false
|
|
25
|
+
|
|
26
|
+
// auto (default): measure total sections payload
|
|
27
|
+
if (!pages?.length) return false
|
|
28
|
+
|
|
29
|
+
let totalSize = 0
|
|
30
|
+
for (const page of pages) {
|
|
31
|
+
if (!page.sections?.length) continue
|
|
32
|
+
totalSize += JSON.stringify(page.sections).length
|
|
33
|
+
if (totalSize > THRESHOLD) return true // Early exit
|
|
34
|
+
}
|
|
35
|
+
return false
|
|
36
|
+
}
|