@uniweb/build 0.1.32 → 0.2.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/README.md +30 -1
- package/package.json +3 -3
- package/src/docs.js +2 -2
- package/src/generate-entry.js +26 -5
- package/src/index.js +2 -1
- package/src/prerender.js +18 -2
- package/src/schema.js +78 -11
- package/src/site/config.js +2 -1
- package/src/site/content-collector.js +265 -20
- package/src/site/plugin.js +14 -4
- package/src/theme/css-generator.js +341 -0
- package/src/theme/index.js +65 -0
- package/src/theme/processor.js +422 -0
- package/src/theme/shade-generator.js +666 -0
|
@@ -28,6 +28,7 @@ import { existsSync } from 'node:fs'
|
|
|
28
28
|
import yaml from 'js-yaml'
|
|
29
29
|
import { collectSectionAssets, mergeAssetCollections } from './assets.js'
|
|
30
30
|
import { parseFetchConfig, singularize } from './data-fetcher.js'
|
|
31
|
+
import { buildTheme, extractFoundationVars } from '../theme/index.js'
|
|
31
32
|
|
|
32
33
|
// Try to import content-reader, fall back to simplified parser
|
|
33
34
|
let markdownToProseMirror
|
|
@@ -66,6 +67,90 @@ function extractRouteParam(folderName) {
|
|
|
66
67
|
return match ? match[1] : null
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────
|
|
71
|
+
// Version Detection
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if a folder name represents a version (e.g., v1, v2, v1.0, v2.1)
|
|
76
|
+
* @param {string} folderName - The folder name to check
|
|
77
|
+
* @returns {boolean}
|
|
78
|
+
*/
|
|
79
|
+
function isVersionFolder(folderName) {
|
|
80
|
+
return /^v\d+(\.\d+)?$/.test(folderName)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse version info from folder name
|
|
85
|
+
* @param {string} folderName - The folder name (e.g., "v1", "v2.1")
|
|
86
|
+
* @returns {Object} Version info { id, major, minor, sortKey }
|
|
87
|
+
*/
|
|
88
|
+
function parseVersionInfo(folderName) {
|
|
89
|
+
const match = folderName.match(/^v(\d+)(?:\.(\d+))?$/)
|
|
90
|
+
if (!match) return null
|
|
91
|
+
|
|
92
|
+
const major = parseInt(match[1], 10)
|
|
93
|
+
const minor = match[2] ? parseInt(match[2], 10) : 0
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
id: folderName,
|
|
97
|
+
major,
|
|
98
|
+
minor,
|
|
99
|
+
sortKey: major * 1000 + minor // For sorting: v2.1 > v2.0 > v1.9
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Detect if a set of folders contains version folders
|
|
105
|
+
* @param {Array<string>} folderNames - List of folder names
|
|
106
|
+
* @returns {Array<Object>|null} Sorted version infos (highest first) or null if not versioned
|
|
107
|
+
*/
|
|
108
|
+
function detectVersions(folderNames) {
|
|
109
|
+
const versions = folderNames
|
|
110
|
+
.filter(isVersionFolder)
|
|
111
|
+
.map(parseVersionInfo)
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
|
|
114
|
+
if (versions.length === 0) return null
|
|
115
|
+
|
|
116
|
+
// Sort by version (highest first)
|
|
117
|
+
versions.sort((a, b) => b.sortKey - a.sortKey)
|
|
118
|
+
|
|
119
|
+
return versions
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build version metadata from detected versions and page.yml config
|
|
124
|
+
* @param {Array<Object>} detectedVersions - Detected version infos
|
|
125
|
+
* @param {Object} pageConfig - page.yml configuration
|
|
126
|
+
* @returns {Object} Version metadata { versions, latestId, scope }
|
|
127
|
+
*/
|
|
128
|
+
function buildVersionMetadata(detectedVersions, pageConfig = {}) {
|
|
129
|
+
const configVersions = pageConfig.versions || {}
|
|
130
|
+
|
|
131
|
+
// Build version list with metadata
|
|
132
|
+
const versions = detectedVersions.map((v, index) => {
|
|
133
|
+
const config = configVersions[v.id] || {}
|
|
134
|
+
const isLatest = config.latest === true || (index === 0 && !Object.values(configVersions).some(c => c.latest))
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
id: v.id,
|
|
138
|
+
label: config.label || v.id,
|
|
139
|
+
latest: isLatest,
|
|
140
|
+
deprecated: config.deprecated || false,
|
|
141
|
+
sortKey: v.sortKey
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Find the latest version
|
|
146
|
+
const latestVersion = versions.find(v => v.latest) || versions[0]
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
versions,
|
|
150
|
+
latestId: latestVersion?.id || null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
69
154
|
/**
|
|
70
155
|
* Parse YAML string using js-yaml
|
|
71
156
|
*/
|
|
@@ -143,11 +228,12 @@ function compareFilenames(a, b) {
|
|
|
143
228
|
* Process a markdown file into a section
|
|
144
229
|
*
|
|
145
230
|
* @param {string} filePath - Path to markdown file
|
|
146
|
-
* @param {string} id - Section ID
|
|
231
|
+
* @param {string} id - Section ID (numeric/positional)
|
|
147
232
|
* @param {string} siteRoot - Site root directory for asset resolution
|
|
233
|
+
* @param {string} defaultStableId - Default stable ID from filename (can be overridden in frontmatter)
|
|
148
234
|
* @returns {Object} Section data with assets manifest
|
|
149
235
|
*/
|
|
150
|
-
async function processMarkdownFile(filePath, id, siteRoot) {
|
|
236
|
+
async function processMarkdownFile(filePath, id, siteRoot, defaultStableId = null) {
|
|
151
237
|
const content = await readFile(filePath, 'utf8')
|
|
152
238
|
let frontMatter = {}
|
|
153
239
|
let markdown = content
|
|
@@ -161,19 +247,33 @@ async function processMarkdownFile(filePath, id, siteRoot) {
|
|
|
161
247
|
}
|
|
162
248
|
}
|
|
163
249
|
|
|
164
|
-
const { type, component, preset, input, props, fetch, ...params } = frontMatter
|
|
250
|
+
const { type, component, preset, input, props, fetch, data, id: frontmatterId, ...params } = frontMatter
|
|
165
251
|
|
|
166
252
|
// Convert markdown to ProseMirror
|
|
167
253
|
const proseMirrorContent = markdownToProseMirror(markdown)
|
|
168
254
|
|
|
255
|
+
// Support 'data:' shorthand for collection fetch
|
|
256
|
+
// data: team → fetch: { collection: team }
|
|
257
|
+
// data: [team, articles] → fetch: { collection: team } (first item, others via inheritData)
|
|
258
|
+
let resolvedFetch = fetch
|
|
259
|
+
if (!fetch && data) {
|
|
260
|
+
const collectionName = Array.isArray(data) ? data[0] : data
|
|
261
|
+
resolvedFetch = { collection: collectionName }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Stable ID for scroll targeting: frontmatter id > filename-derived > null
|
|
265
|
+
// This ID is stable across reordering (unlike the positional id)
|
|
266
|
+
const stableId = frontmatterId || defaultStableId || null
|
|
267
|
+
|
|
169
268
|
const section = {
|
|
170
269
|
id,
|
|
270
|
+
stableId,
|
|
171
271
|
component: type || component || 'Section',
|
|
172
272
|
preset,
|
|
173
273
|
input,
|
|
174
274
|
params: { ...params, ...props },
|
|
175
275
|
content: proseMirrorContent,
|
|
176
|
-
fetch: parseFetchConfig(
|
|
276
|
+
fetch: parseFetchConfig(resolvedFetch),
|
|
177
277
|
subsections: []
|
|
178
278
|
}
|
|
179
279
|
|
|
@@ -280,7 +380,8 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
|
|
|
280
380
|
}
|
|
281
381
|
|
|
282
382
|
// Process the section
|
|
283
|
-
|
|
383
|
+
// Use sectionName as stable ID for scroll targeting (e.g., "hero", "features")
|
|
384
|
+
const { section, assetCollection: sectionAssets } = await processMarkdownFile(filePath, id, siteRoot, sectionName)
|
|
284
385
|
assetCollection = mergeAssetCollections(assetCollection, sectionAssets)
|
|
285
386
|
|
|
286
387
|
// Track last modified
|
|
@@ -316,9 +417,10 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
|
|
|
316
417
|
* @param {boolean} options.isIndex - Whether this page is the index for its parent route
|
|
317
418
|
* @param {string} options.parentRoute - The parent route (e.g., '/' or '/docs')
|
|
318
419
|
* @param {Object} options.parentFetch - Parent page's fetch config (for dynamic routes)
|
|
420
|
+
* @param {Object} options.versionContext - Version context from parent { version, versionMeta, scope }
|
|
319
421
|
* @returns {Object} Page data with assets manifest
|
|
320
422
|
*/
|
|
321
|
-
async function processPage(pagePath, pageName, siteRoot, { isIndex = false, parentRoute = '/', parentFetch = null } = {}) {
|
|
423
|
+
async function processPage(pagePath, pageName, siteRoot, { isIndex = false, parentRoute = '/', parentFetch = null, versionContext = null } = {}) {
|
|
322
424
|
const pageConfig = await readYamlFile(join(pagePath, 'page.yml'))
|
|
323
425
|
|
|
324
426
|
// Note: We no longer skip hidden pages here - they still exist as valid pages,
|
|
@@ -344,10 +446,13 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
344
446
|
const sections = []
|
|
345
447
|
for (const file of mdFiles) {
|
|
346
448
|
const { name } = parse(file)
|
|
347
|
-
const { prefix } = parseNumericPrefix(name)
|
|
449
|
+
const { prefix, name: stableName } = parseNumericPrefix(name)
|
|
348
450
|
const id = prefix || name
|
|
451
|
+
// Use the name part (after prefix) as stable ID for scroll targeting
|
|
452
|
+
// e.g., "1-intro.md" → stableId: "intro", "2-features.md" → stableId: "features"
|
|
453
|
+
const stableId = stableName || name
|
|
349
454
|
|
|
350
|
-
const { section, assetCollection } = await processMarkdownFile(join(pagePath, file), id, siteRoot)
|
|
455
|
+
const { section, assetCollection } = await processMarkdownFile(join(pagePath, file), id, siteRoot, stableId)
|
|
351
456
|
sections.push(section)
|
|
352
457
|
pageAssetCollection = mergeAssetCollections(pageAssetCollection, assetCollection)
|
|
353
458
|
|
|
@@ -405,6 +510,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
405
510
|
return {
|
|
406
511
|
page: {
|
|
407
512
|
route,
|
|
513
|
+
id: pageConfig.id || null, // Stable page ID for page: links (survives reorganization)
|
|
408
514
|
isIndex, // Marks this page as the index for its parent route (accessible at parentRoute)
|
|
409
515
|
title: pageConfig.title || pageName,
|
|
410
516
|
description: pageConfig.description || '',
|
|
@@ -417,6 +523,11 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
417
523
|
paramName, // e.g., "slug" from [slug]
|
|
418
524
|
parentSchema, // e.g., "articles" - the data array to iterate over
|
|
419
525
|
|
|
526
|
+
// Version metadata (if within a versioned section)
|
|
527
|
+
version: versionContext?.version || null,
|
|
528
|
+
versionMeta: versionContext?.versionMeta || null,
|
|
529
|
+
versionScope: versionContext?.scope || null,
|
|
530
|
+
|
|
420
531
|
// Navigation options
|
|
421
532
|
hidden: pageConfig.hidden || false, // Hide from all navigation
|
|
422
533
|
hideInHeader: pageConfig.hideInHeader || false, // Hide from header nav
|
|
@@ -438,7 +549,13 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
438
549
|
},
|
|
439
550
|
|
|
440
551
|
// Data fetching
|
|
441
|
-
|
|
552
|
+
// Support 'data:' shorthand at page level
|
|
553
|
+
// data: team → fetch: { collection: team }
|
|
554
|
+
fetch: parseFetchConfig(
|
|
555
|
+
pageConfig.fetch || (pageConfig.data
|
|
556
|
+
? { collection: Array.isArray(pageConfig.data) ? pageConfig.data[0] : pageConfig.data }
|
|
557
|
+
: undefined)
|
|
558
|
+
),
|
|
442
559
|
|
|
443
560
|
sections: hierarchicalSections
|
|
444
561
|
},
|
|
@@ -488,9 +605,10 @@ function determineIndexPage(orderConfig, availableFolders) {
|
|
|
488
605
|
* @param {string} siteRoot - Site root directory for asset resolution
|
|
489
606
|
* @param {Object} orderConfig - { pages: [...], index: 'name' } from parent's config
|
|
490
607
|
* @param {Object} parentFetch - Parent page's fetch config (for dynamic child routes)
|
|
491
|
-
* @
|
|
608
|
+
* @param {Object} versionContext - Version context from parent { version, versionMeta }
|
|
609
|
+
* @returns {Promise<Object>} { pages, assetCollection, header, footer, left, right, notFound, versionedScopes }
|
|
492
610
|
*/
|
|
493
|
-
async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig = {}, parentFetch = null) {
|
|
611
|
+
async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig = {}, parentFetch = null, versionContext = null) {
|
|
494
612
|
const entries = await readdir(dirPath)
|
|
495
613
|
const pages = []
|
|
496
614
|
let assetCollection = {
|
|
@@ -503,6 +621,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
503
621
|
let left = null
|
|
504
622
|
let right = null
|
|
505
623
|
let notFound = null
|
|
624
|
+
const versionedScopes = new Map() // scope route → versionMeta
|
|
506
625
|
|
|
507
626
|
// First pass: discover all page folders and read their order values
|
|
508
627
|
const pageFolders = []
|
|
@@ -517,6 +636,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
517
636
|
name: entry,
|
|
518
637
|
path: entryPath,
|
|
519
638
|
order: pageConfig.order,
|
|
639
|
+
pageConfig,
|
|
520
640
|
childOrderConfig: {
|
|
521
641
|
pages: pageConfig.pages,
|
|
522
642
|
index: pageConfig.index
|
|
@@ -524,6 +644,72 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
524
644
|
})
|
|
525
645
|
}
|
|
526
646
|
|
|
647
|
+
// Check if this directory contains version folders (versioned section)
|
|
648
|
+
const folderNames = pageFolders.map(f => f.name)
|
|
649
|
+
const detectedVersions = detectVersions(folderNames)
|
|
650
|
+
|
|
651
|
+
// If versioned section, handle version folders specially
|
|
652
|
+
if (detectedVersions && !versionContext) {
|
|
653
|
+
// Read parent page.yml for version metadata
|
|
654
|
+
const parentConfig = await readYamlFile(join(dirPath, 'page.yml'))
|
|
655
|
+
const versionMeta = buildVersionMetadata(detectedVersions, parentConfig)
|
|
656
|
+
|
|
657
|
+
// Record this versioned scope
|
|
658
|
+
versionedScopes.set(parentRoute, versionMeta)
|
|
659
|
+
|
|
660
|
+
// Process version folders
|
|
661
|
+
for (const folder of pageFolders) {
|
|
662
|
+
const { name: entry, path: entryPath, childOrderConfig, pageConfig } = folder
|
|
663
|
+
|
|
664
|
+
if (isVersionFolder(entry)) {
|
|
665
|
+
// This is a version folder
|
|
666
|
+
const versionInfo = versionMeta.versions.find(v => v.id === entry)
|
|
667
|
+
const isLatest = versionInfo?.latest || false
|
|
668
|
+
|
|
669
|
+
// For latest version, use parent route directly
|
|
670
|
+
// For other versions, add version prefix to route
|
|
671
|
+
const versionRoute = isLatest ? parentRoute : `${parentRoute}/${entry}`
|
|
672
|
+
|
|
673
|
+
// Recurse into version folder with version context
|
|
674
|
+
const subResult = await collectPagesRecursive(
|
|
675
|
+
entryPath,
|
|
676
|
+
versionRoute,
|
|
677
|
+
siteRoot,
|
|
678
|
+
childOrderConfig,
|
|
679
|
+
parentFetch,
|
|
680
|
+
{
|
|
681
|
+
version: versionInfo,
|
|
682
|
+
versionMeta,
|
|
683
|
+
scope: parentRoute // The route where versioning is scoped
|
|
684
|
+
}
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
pages.push(...subResult.pages)
|
|
688
|
+
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
689
|
+
// Merge any nested versioned scopes (shouldn't happen often, but possible)
|
|
690
|
+
for (const [scope, meta] of subResult.versionedScopes) {
|
|
691
|
+
versionedScopes.set(scope, meta)
|
|
692
|
+
}
|
|
693
|
+
} else if (!entry.startsWith('@')) {
|
|
694
|
+
// Non-version, non-special folders in a versioned section
|
|
695
|
+
// These could be shared across versions - process normally
|
|
696
|
+
const result = await processPage(entryPath, entry, siteRoot, {
|
|
697
|
+
isIndex: false,
|
|
698
|
+
parentRoute,
|
|
699
|
+
parentFetch
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
if (result) {
|
|
703
|
+
pages.push(result.page)
|
|
704
|
+
assetCollection = mergeAssetCollections(assetCollection, result.assetCollection)
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Return early - we've handled all children
|
|
710
|
+
return { pages, assetCollection, header, footer, left, right, notFound, versionedScopes }
|
|
711
|
+
}
|
|
712
|
+
|
|
527
713
|
// Determine which page is the index for this level
|
|
528
714
|
const regularFolders = pageFolders.filter(f => !f.name.startsWith('@'))
|
|
529
715
|
const indexPageName = determineIndexPage(orderConfig, regularFolders)
|
|
@@ -539,7 +725,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
539
725
|
const result = await processPage(entryPath, entry, siteRoot, {
|
|
540
726
|
isIndex: isIndex && !isSpecial,
|
|
541
727
|
parentRoute,
|
|
542
|
-
parentFetch
|
|
728
|
+
parentFetch,
|
|
729
|
+
versionContext
|
|
543
730
|
})
|
|
544
731
|
|
|
545
732
|
if (result) {
|
|
@@ -571,34 +758,84 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
571
758
|
const childParentRoute = isIndex ? parentRoute : page.route
|
|
572
759
|
// Pass this page's fetch config to children (for dynamic routes that inherit parent data)
|
|
573
760
|
const childFetch = page.fetch || parentFetch
|
|
574
|
-
|
|
761
|
+
// Pass version context to children (maintains version scope)
|
|
762
|
+
const subResult = await collectPagesRecursive(entryPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext)
|
|
575
763
|
pages.push(...subResult.pages)
|
|
576
764
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
765
|
+
// Merge any versioned scopes from children
|
|
766
|
+
for (const [scope, meta] of subResult.versionedScopes) {
|
|
767
|
+
versionedScopes.set(scope, meta)
|
|
768
|
+
}
|
|
577
769
|
}
|
|
578
770
|
}
|
|
579
771
|
}
|
|
580
772
|
|
|
581
|
-
return { pages, assetCollection, header, footer, left, right, notFound }
|
|
773
|
+
return { pages, assetCollection, header, footer, left, right, notFound, versionedScopes }
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Load foundation variables from schema.json
|
|
778
|
+
*
|
|
779
|
+
* @param {string} foundationPath - Path to foundation directory
|
|
780
|
+
* @returns {Promise<Object>} Foundation variables or empty object
|
|
781
|
+
*/
|
|
782
|
+
async function loadFoundationVars(foundationPath) {
|
|
783
|
+
if (!foundationPath) return {}
|
|
784
|
+
|
|
785
|
+
// Try dist/meta/schema.json first (built foundation), then root schema.json
|
|
786
|
+
const distSchemaPath = join(foundationPath, 'dist', 'meta', 'schema.json')
|
|
787
|
+
const rootSchemaPath = join(foundationPath, 'schema.json')
|
|
788
|
+
|
|
789
|
+
const schemaPath = existsSync(distSchemaPath) ? distSchemaPath : rootSchemaPath
|
|
790
|
+
|
|
791
|
+
if (!existsSync(schemaPath)) {
|
|
792
|
+
return {}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
try {
|
|
796
|
+
const schemaContent = await readFile(schemaPath, 'utf8')
|
|
797
|
+
const schema = JSON.parse(schemaContent)
|
|
798
|
+
// Foundation config is in _self, support both 'vars' (new) and 'themeVars' (legacy)
|
|
799
|
+
return schema._self?.vars || schema._self?.themeVars || schema.themeVars || {}
|
|
800
|
+
} catch (err) {
|
|
801
|
+
console.warn('[content-collector] Failed to load foundation schema:', err.message)
|
|
802
|
+
return {}
|
|
803
|
+
}
|
|
582
804
|
}
|
|
583
805
|
|
|
584
806
|
/**
|
|
585
807
|
* Collect all site content
|
|
586
808
|
*
|
|
587
809
|
* @param {string} sitePath - Path to site directory
|
|
810
|
+
* @param {Object} options - Collection options
|
|
811
|
+
* @param {string} options.foundationPath - Path to foundation directory (for theme vars)
|
|
588
812
|
* @returns {Promise<Object>} Site content object with assets manifest
|
|
589
813
|
*/
|
|
590
|
-
export async function collectSiteContent(sitePath) {
|
|
814
|
+
export async function collectSiteContent(sitePath, options = {}) {
|
|
815
|
+
const { foundationPath } = options
|
|
591
816
|
const pagesPath = join(sitePath, 'pages')
|
|
592
817
|
|
|
593
|
-
// Read site config
|
|
818
|
+
// Read site config and raw theme config
|
|
594
819
|
const siteConfig = await readYamlFile(join(sitePath, 'site.yml'))
|
|
595
|
-
const
|
|
820
|
+
const rawThemeConfig = await readYamlFile(join(sitePath, 'theme.yml'))
|
|
821
|
+
|
|
822
|
+
// Load foundation vars and process theme
|
|
823
|
+
const foundationVars = await loadFoundationVars(foundationPath)
|
|
824
|
+
const { config: processedTheme, css: themeCSS, warnings } = buildTheme(rawThemeConfig, { foundationVars })
|
|
825
|
+
|
|
826
|
+
// Log theme warnings
|
|
827
|
+
if (warnings?.length > 0) {
|
|
828
|
+
warnings.forEach(w => console.warn(`[theme] ${w}`))
|
|
829
|
+
}
|
|
596
830
|
|
|
597
831
|
// Check if pages directory exists
|
|
598
832
|
if (!existsSync(pagesPath)) {
|
|
599
833
|
return {
|
|
600
834
|
config: siteConfig,
|
|
601
|
-
theme:
|
|
835
|
+
theme: {
|
|
836
|
+
...processedTheme,
|
|
837
|
+
css: themeCSS
|
|
838
|
+
},
|
|
602
839
|
pages: [],
|
|
603
840
|
assets: {}
|
|
604
841
|
}
|
|
@@ -611,7 +848,7 @@ export async function collectSiteContent(sitePath) {
|
|
|
611
848
|
}
|
|
612
849
|
|
|
613
850
|
// Recursively collect all pages
|
|
614
|
-
const { pages, assetCollection, header, footer, left, right, notFound } =
|
|
851
|
+
const { pages, assetCollection, header, footer, left, right, notFound, versionedScopes } =
|
|
615
852
|
await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig)
|
|
616
853
|
|
|
617
854
|
// Sort pages by order
|
|
@@ -624,18 +861,26 @@ export async function collectSiteContent(sitePath) {
|
|
|
624
861
|
console.log(`[content-collector] Found ${assetCount} asset references${explicitCount > 0 ? ` (${explicitCount} with explicit poster/preview)` : ''}`)
|
|
625
862
|
}
|
|
626
863
|
|
|
864
|
+
// Convert versionedScopes Map to plain object for JSON serialization
|
|
865
|
+
const versionedScopesObj = Object.fromEntries(versionedScopes)
|
|
866
|
+
|
|
627
867
|
return {
|
|
628
868
|
config: {
|
|
629
869
|
...siteConfig,
|
|
630
870
|
fetch: parseFetchConfig(siteConfig.fetch),
|
|
631
871
|
},
|
|
632
|
-
theme:
|
|
872
|
+
theme: {
|
|
873
|
+
...processedTheme,
|
|
874
|
+
css: themeCSS
|
|
875
|
+
},
|
|
633
876
|
pages,
|
|
634
877
|
header,
|
|
635
878
|
footer,
|
|
636
879
|
left,
|
|
637
880
|
right,
|
|
638
881
|
notFound,
|
|
882
|
+
// Versioned scopes: route → { versions, latestId }
|
|
883
|
+
versionedScopes: versionedScopesObj,
|
|
639
884
|
assets: assetCollection.assets,
|
|
640
885
|
hasExplicitPoster: assetCollection.hasExplicitPoster,
|
|
641
886
|
hasExplicitPreview: assetCollection.hasExplicitPreview
|
package/src/site/plugin.js
CHANGED
|
@@ -305,6 +305,7 @@ function escapeHtml(str) {
|
|
|
305
305
|
* @param {Object} [options.search] - Search index configuration
|
|
306
306
|
* @param {boolean} [options.search.enabled=true] - Generate search index (uses site.yml config by default)
|
|
307
307
|
* @param {string} [options.search.filename='search-index.json'] - Search index filename
|
|
308
|
+
* @param {string} [options.foundationPath] - Path to foundation directory (for loading theme vars)
|
|
308
309
|
*/
|
|
309
310
|
export function siteContentPlugin(options = {}) {
|
|
310
311
|
const {
|
|
@@ -316,7 +317,8 @@ export function siteContentPlugin(options = {}) {
|
|
|
316
317
|
watch: shouldWatch = true,
|
|
317
318
|
seo = {},
|
|
318
319
|
assets: assetsConfig = {},
|
|
319
|
-
search: searchPluginConfig = {}
|
|
320
|
+
search: searchPluginConfig = {},
|
|
321
|
+
foundationPath
|
|
320
322
|
} = options
|
|
321
323
|
|
|
322
324
|
// Extract asset processing options
|
|
@@ -416,7 +418,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
416
418
|
if (!isProduction) {
|
|
417
419
|
try {
|
|
418
420
|
// Do an early content collection to get the collections config
|
|
419
|
-
const earlyContent = await collectSiteContent(resolvedSitePath)
|
|
421
|
+
const earlyContent = await collectSiteContent(resolvedSitePath, { foundationPath })
|
|
420
422
|
collectionsConfig = earlyContent.config?.collections
|
|
421
423
|
|
|
422
424
|
if (collectionsConfig) {
|
|
@@ -433,7 +435,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
433
435
|
async buildStart() {
|
|
434
436
|
// Collect content at build start
|
|
435
437
|
try {
|
|
436
|
-
siteContent = await collectSiteContent(resolvedSitePath)
|
|
438
|
+
siteContent = await collectSiteContent(resolvedSitePath, { foundationPath })
|
|
437
439
|
console.log(`[site-content] Collected ${siteContent.pages?.length || 0} pages`)
|
|
438
440
|
|
|
439
441
|
// Process content collections if defined in site.yml
|
|
@@ -480,7 +482,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
480
482
|
rebuildTimeout = setTimeout(async () => {
|
|
481
483
|
console.log('[site-content] Content changed, rebuilding...')
|
|
482
484
|
try {
|
|
483
|
-
siteContent = await collectSiteContent(resolvedSitePath)
|
|
485
|
+
siteContent = await collectSiteContent(resolvedSitePath, { foundationPath })
|
|
484
486
|
// Execute fetches for the updated content
|
|
485
487
|
await executeDevFetches(siteContent, resolvedSitePath)
|
|
486
488
|
console.log(`[site-content] Rebuilt ${siteContent.pages?.length || 0} pages`)
|
|
@@ -685,6 +687,11 @@ export function siteContentPlugin(options = {}) {
|
|
|
685
687
|
|
|
686
688
|
let headInjection = ''
|
|
687
689
|
|
|
690
|
+
// Inject theme CSS
|
|
691
|
+
if (contentToInject.theme?.css) {
|
|
692
|
+
headInjection += ` <style id="uniweb-theme">\n${contentToInject.theme.css}\n </style>\n`
|
|
693
|
+
}
|
|
694
|
+
|
|
688
695
|
// Inject SEO meta tags
|
|
689
696
|
if (seoEnabled) {
|
|
690
697
|
const metaTags = generateMetaTags(contentToInject, seoOptions)
|
|
@@ -791,6 +798,9 @@ export function siteContentPlugin(options = {}) {
|
|
|
791
798
|
delete finalContent.hasExplicitPoster
|
|
792
799
|
delete finalContent.hasExplicitPreview
|
|
793
800
|
|
|
801
|
+
// Note: theme.css is kept here so prerender can inject it into HTML
|
|
802
|
+
// Prerender will strip it from the JSON it injects into each page
|
|
803
|
+
|
|
794
804
|
// Emit content as JSON file in production build
|
|
795
805
|
this.emitFile({
|
|
796
806
|
type: 'asset',
|