@uniweb/build 0.6.3 → 0.6.5
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/i18n/extract.js +1 -1
- package/src/i18n/merge.js +3 -3
- package/src/prerender.js +37 -8
- package/src/search/extract.js +0 -5
- package/src/site/collection-processor.js +17 -16
- package/src/site/config.js +15 -1
- package/src/site/content-collector.js +65 -36
- package/src/site/plugin.js +54 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"sharp": "^0.33.2"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"@uniweb/
|
|
54
|
-
"@uniweb/runtime": "0.5.
|
|
55
|
-
"@uniweb/
|
|
53
|
+
"@uniweb/schemas": "0.2.1",
|
|
54
|
+
"@uniweb/runtime": "0.5.14",
|
|
55
|
+
"@uniweb/content-reader": "1.1.2"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@tailwindcss/vite": "^4.0.0",
|
|
62
62
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
63
63
|
"vite-plugin-svgr": "^4.0.0",
|
|
64
|
-
"@uniweb/core": "0.4.
|
|
64
|
+
"@uniweb/core": "0.4.3"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"vite": {
|
package/src/i18n/extract.js
CHANGED
|
@@ -41,7 +41,7 @@ export function extractTranslatableContent(siteContent) {
|
|
|
41
41
|
for (const layoutKey of ['header', 'footer', 'left', 'right']) {
|
|
42
42
|
const layoutPage = siteContent[layoutKey]
|
|
43
43
|
if (layoutPage?.sections) {
|
|
44
|
-
const pageRoute = layoutPage.route ||
|
|
44
|
+
const pageRoute = layoutPage.route || `/layout/${layoutKey}`
|
|
45
45
|
for (const section of layoutPage.sections) {
|
|
46
46
|
extractFromSection(section, pageRoute, units)
|
|
47
47
|
}
|
package/src/i18n/merge.js
CHANGED
|
@@ -80,7 +80,7 @@ function mergeTranslationsSync(siteContent, translations, fallbackToSource) {
|
|
|
80
80
|
for (const layoutKey of ['header', 'footer', 'left', 'right']) {
|
|
81
81
|
const layoutPage = translated[layoutKey]
|
|
82
82
|
if (layoutPage?.sections) {
|
|
83
|
-
const pageRoute = layoutPage.route ||
|
|
83
|
+
const pageRoute = layoutPage.route || `/layout/${layoutKey}`
|
|
84
84
|
for (const section of layoutPage.sections) {
|
|
85
85
|
translateSectionSync(section, pageRoute, translations, fallbackToSource)
|
|
86
86
|
}
|
|
@@ -132,8 +132,8 @@ async function mergeTranslationsAsync(siteContent, translations, options) {
|
|
|
132
132
|
for (const layoutKey of ['header', 'footer', 'left', 'right']) {
|
|
133
133
|
const layoutPage = translated[layoutKey]
|
|
134
134
|
if (layoutPage?.sections) {
|
|
135
|
-
// Ensure route is set for context matching
|
|
136
|
-
if (!layoutPage.route) layoutPage.route =
|
|
135
|
+
// Ensure route is set for context matching
|
|
136
|
+
if (!layoutPage.route) layoutPage.route = `/layout/${layoutKey}`
|
|
137
137
|
for (const section of layoutPage.sections) {
|
|
138
138
|
await translateSectionAsync(section, layoutPage, translations, {
|
|
139
139
|
fallbackToSource,
|
package/src/prerender.js
CHANGED
|
@@ -26,19 +26,41 @@ let preparePropsSSR, getComponentMetaSSR
|
|
|
26
26
|
* @param {Object} siteContent - The site content from site-content.json
|
|
27
27
|
* @param {string} siteDir - Path to the site directory
|
|
28
28
|
* @param {function} onProgress - Progress callback
|
|
29
|
+
* @param {Object} [localeInfo] - Locale info for collection data localization
|
|
30
|
+
* @param {string} [localeInfo.locale] - Active locale code
|
|
31
|
+
* @param {string} [localeInfo.defaultLocale] - Default locale code
|
|
32
|
+
* @param {string} [localeInfo.distDir] - Path to dist directory (where locale-specific data lives)
|
|
29
33
|
* @returns {Object} { pageFetchedData, fetchedData } - Fetched data for dynamic route expansion and DataStore pre-population
|
|
30
34
|
*/
|
|
31
|
-
async function executeAllFetches(siteContent, siteDir, onProgress) {
|
|
35
|
+
async function executeAllFetches(siteContent, siteDir, onProgress, localeInfo) {
|
|
32
36
|
const fetchOptions = { siteRoot: siteDir, publicDir: 'public' }
|
|
33
37
|
const fetchedData = [] // Collected for DataStore pre-population
|
|
34
38
|
|
|
39
|
+
// For non-default locales, translated collection data lives in dist/{locale}/data/
|
|
40
|
+
// instead of public/data/. Create a localized fetch helper.
|
|
41
|
+
const isNonDefaultLocale = localeInfo &&
|
|
42
|
+
localeInfo.locale !== localeInfo.defaultLocale &&
|
|
43
|
+
localeInfo.distDir
|
|
44
|
+
|
|
45
|
+
function localizeFetch(config) {
|
|
46
|
+
if (!isNonDefaultLocale || !config.path?.startsWith('/data/')) return config
|
|
47
|
+
return { ...config, path: `/${localeInfo.locale}${config.path}` }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fetch options pointing to dist/ for localized data
|
|
51
|
+
const localizedFetchOptions = isNonDefaultLocale
|
|
52
|
+
? { siteRoot: localeInfo.distDir, publicDir: '.' }
|
|
53
|
+
: fetchOptions
|
|
54
|
+
|
|
35
55
|
// 1. Site-level fetch
|
|
36
56
|
const siteFetch = siteContent.config?.fetch
|
|
37
57
|
if (siteFetch && siteFetch.prerender !== false) {
|
|
38
|
-
|
|
39
|
-
const
|
|
58
|
+
const cfg = localizeFetch(siteFetch)
|
|
59
|
+
const opts = cfg !== siteFetch ? localizedFetchOptions : fetchOptions
|
|
60
|
+
onProgress(` Fetching site data: ${cfg.path || cfg.url}`)
|
|
61
|
+
const result = await executeFetch(cfg, opts)
|
|
40
62
|
if (result.data && !result.error) {
|
|
41
|
-
fetchedData.push({ config:
|
|
63
|
+
fetchedData.push({ config: cfg, data: result.data })
|
|
42
64
|
}
|
|
43
65
|
}
|
|
44
66
|
|
|
@@ -49,10 +71,12 @@ async function executeAllFetches(siteContent, siteDir, onProgress) {
|
|
|
49
71
|
// Page-level fetch
|
|
50
72
|
const pageFetch = page.fetch
|
|
51
73
|
if (pageFetch && pageFetch.prerender !== false) {
|
|
52
|
-
|
|
53
|
-
const
|
|
74
|
+
const cfg = localizeFetch(pageFetch)
|
|
75
|
+
const opts = cfg !== pageFetch ? localizedFetchOptions : fetchOptions
|
|
76
|
+
onProgress(` Fetching page data for ${page.route}: ${cfg.path || cfg.url}`)
|
|
77
|
+
const result = await executeFetch(cfg, opts)
|
|
54
78
|
if (result.data && !result.error) {
|
|
55
|
-
fetchedData.push({ config:
|
|
79
|
+
fetchedData.push({ config: cfg, data: result.data })
|
|
56
80
|
// Store for dynamic route expansion
|
|
57
81
|
pageFetchedData.set(page.route, {
|
|
58
82
|
schema: pageFetch.schema,
|
|
@@ -656,8 +680,13 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
656
680
|
siteContent.config.activeLocale = locale
|
|
657
681
|
|
|
658
682
|
// Execute data fetches (site, page, section levels)
|
|
683
|
+
// For non-default locales, collection data is read from dist/{locale}/data/
|
|
659
684
|
onProgress('Executing data fetches...')
|
|
660
|
-
const
|
|
685
|
+
const defaultLocale = defaultSiteContent.config?.defaultLanguage || 'en'
|
|
686
|
+
const { pageFetchedData, fetchedData } = await executeAllFetches(
|
|
687
|
+
siteContent, siteDir, onProgress,
|
|
688
|
+
{ locale, defaultLocale, distDir }
|
|
689
|
+
)
|
|
661
690
|
|
|
662
691
|
// Store fetchedData on siteContent for runtime DataStore pre-population
|
|
663
692
|
siteContent.fetchedData = fetchedData
|
package/src/search/extract.js
CHANGED
|
@@ -41,11 +41,6 @@ export function extractSearchContent(siteContent, options = {}) {
|
|
|
41
41
|
continue
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// Skip special pages (header, footer, etc.)
|
|
45
|
-
if (pageRoute.startsWith('/@')) {
|
|
46
|
-
continue
|
|
47
|
-
}
|
|
48
|
-
|
|
49
44
|
// Skip pages marked as noindex
|
|
50
45
|
if (page.seo?.noindex) {
|
|
51
46
|
continue
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* // site.yml
|
|
18
18
|
* collections:
|
|
19
19
|
* articles:
|
|
20
|
-
* path:
|
|
20
|
+
* path: collections/articles
|
|
21
21
|
* sort: date desc
|
|
22
22
|
*
|
|
23
23
|
* // Usage
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { readFile, readdir, stat, writeFile, mkdir, copyFile } from 'node:fs/promises'
|
|
29
|
-
import { join, basename, extname, dirname, relative } from 'node:path'
|
|
29
|
+
import { join, basename, extname, dirname, relative, resolve } from 'node:path'
|
|
30
30
|
import { existsSync } from 'node:fs'
|
|
31
31
|
import yaml from 'js-yaml'
|
|
32
32
|
import { applyFilter, applySort } from './data-fetcher.js'
|
|
@@ -59,11 +59,11 @@ try {
|
|
|
59
59
|
*
|
|
60
60
|
* @example
|
|
61
61
|
* // Simple form
|
|
62
|
-
* parseCollectionConfig('articles', '
|
|
62
|
+
* parseCollectionConfig('articles', 'collections/articles')
|
|
63
63
|
*
|
|
64
64
|
* // Extended form
|
|
65
65
|
* parseCollectionConfig('articles', {
|
|
66
|
-
* path: '
|
|
66
|
+
* path: 'collections/articles',
|
|
67
67
|
* route: '/blog',
|
|
68
68
|
* sort: 'date desc',
|
|
69
69
|
* filter: 'published != false',
|
|
@@ -213,7 +213,7 @@ function isExternalUrl(src) {
|
|
|
213
213
|
/**
|
|
214
214
|
* Process assets in collection content
|
|
215
215
|
* - Resolves relative paths to site-root-relative paths
|
|
216
|
-
* - Copies co-located assets to public/
|
|
216
|
+
* - Copies co-located assets to public/collections/<collection>/
|
|
217
217
|
* - Updates paths in the content in place
|
|
218
218
|
*
|
|
219
219
|
* @param {Object} content - ProseMirror document
|
|
@@ -226,7 +226,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
|
|
|
226
226
|
const assets = {}
|
|
227
227
|
const itemDir = dirname(itemPath)
|
|
228
228
|
const publicDir = join(siteRoot, 'public')
|
|
229
|
-
const targetDir = join(publicDir, '
|
|
229
|
+
const targetDir = join(publicDir, 'collections', collectionName)
|
|
230
230
|
|
|
231
231
|
// Walk content and collect asset paths
|
|
232
232
|
const assetNodes = []
|
|
@@ -248,7 +248,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
|
|
|
248
248
|
if (src.startsWith('./') || src.startsWith('../')) {
|
|
249
249
|
// Check if file exists at resolved location
|
|
250
250
|
if (existsSync(result.resolved)) {
|
|
251
|
-
// Copy to public/
|
|
251
|
+
// Copy to public/collections/<collection>/
|
|
252
252
|
const assetFilename = basename(result.resolved)
|
|
253
253
|
const targetPath = join(targetDir, assetFilename)
|
|
254
254
|
|
|
@@ -259,7 +259,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
|
|
|
259
259
|
await copyFile(result.resolved, targetPath)
|
|
260
260
|
|
|
261
261
|
// Update path to site-root-relative
|
|
262
|
-
finalPath = `/
|
|
262
|
+
finalPath = `/collections/${collectionName}/${assetFilename}`
|
|
263
263
|
|
|
264
264
|
assets[src] = {
|
|
265
265
|
original: src,
|
|
@@ -294,7 +294,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
|
|
|
294
294
|
const posterTarget = join(targetDir, posterFilename)
|
|
295
295
|
await mkdir(targetDir, { recursive: true })
|
|
296
296
|
await copyFile(posterResult.resolved, posterTarget)
|
|
297
|
-
node.attrs.poster = `/
|
|
297
|
+
node.attrs.poster = `/collections/${collectionName}/${posterFilename}`
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
|
|
@@ -305,7 +305,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
|
|
|
305
305
|
const previewTarget = join(targetDir, previewFilename)
|
|
306
306
|
await mkdir(targetDir, { recursive: true })
|
|
307
307
|
await copyFile(previewResult.resolved, previewTarget)
|
|
308
|
-
node.attrs.preview = `/
|
|
308
|
+
node.attrs.preview = `/collections/${collectionName}/${previewFilename}`
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
}
|
|
@@ -421,8 +421,9 @@ async function processContentItem(dir, filename, config, siteRoot) {
|
|
|
421
421
|
* @param {Object} config - Parsed collection config
|
|
422
422
|
* @returns {Promise<Array>} Array of processed items
|
|
423
423
|
*/
|
|
424
|
-
async function collectItems(siteDir, config) {
|
|
425
|
-
const
|
|
424
|
+
async function collectItems(siteDir, config, collectionsBase) {
|
|
425
|
+
const base = collectionsBase || siteDir
|
|
426
|
+
const collectionDir = resolve(base, config.path)
|
|
426
427
|
|
|
427
428
|
// Check if collection directory exists
|
|
428
429
|
if (!existsSync(collectionDir)) {
|
|
@@ -491,12 +492,12 @@ async function collectItems(siteDir, config) {
|
|
|
491
492
|
*
|
|
492
493
|
* @example
|
|
493
494
|
* const collections = await processCollections('/path/to/site', {
|
|
494
|
-
* articles: { path: '
|
|
495
|
-
* products: '
|
|
495
|
+
* articles: { path: 'collections/articles', sort: 'date desc' },
|
|
496
|
+
* products: 'collections/products'
|
|
496
497
|
* })
|
|
497
498
|
* // { articles: [...], products: [...] }
|
|
498
499
|
*/
|
|
499
|
-
export async function processCollections(siteDir, collectionsConfig) {
|
|
500
|
+
export async function processCollections(siteDir, collectionsConfig, collectionsBase) {
|
|
500
501
|
if (!collectionsConfig || typeof collectionsConfig !== 'object') {
|
|
501
502
|
return {}
|
|
502
503
|
}
|
|
@@ -505,7 +506,7 @@ export async function processCollections(siteDir, collectionsConfig) {
|
|
|
505
506
|
|
|
506
507
|
for (const [name, config] of Object.entries(collectionsConfig)) {
|
|
507
508
|
const parsed = parseCollectionConfig(name, config)
|
|
508
|
-
const items = await collectItems(siteDir, parsed)
|
|
509
|
+
const items = await collectItems(siteDir, parsed, collectionsBase)
|
|
509
510
|
results[name] = items
|
|
510
511
|
console.log(`[collection-processor] Processed ${name}: ${items.length} items`)
|
|
511
512
|
}
|
package/src/site/config.js
CHANGED
|
@@ -328,7 +328,21 @@ export async function defineSiteConfig(options = {}) {
|
|
|
328
328
|
server: {
|
|
329
329
|
fs: {
|
|
330
330
|
// Allow parent directory for foundation sibling access
|
|
331
|
-
|
|
331
|
+
// Plus any external content paths from site.yml paths: group
|
|
332
|
+
allow: (() => {
|
|
333
|
+
const allowed = ['..']
|
|
334
|
+
const parentDir = resolve(siteRoot, '..')
|
|
335
|
+
const paths = siteConfig.paths || {}
|
|
336
|
+
for (const key of ['pages', 'layout', 'collections']) {
|
|
337
|
+
if (paths[key]) {
|
|
338
|
+
const resolved = resolve(siteRoot, paths[key])
|
|
339
|
+
if (!resolved.startsWith(parentDir)) {
|
|
340
|
+
allowed.push(resolved)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return allowed
|
|
345
|
+
})()
|
|
332
346
|
},
|
|
333
347
|
...(siteConfig.build?.port && { port: siteConfig.build.port }),
|
|
334
348
|
...serverOverrides
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Content Collector
|
|
3
3
|
*
|
|
4
|
-
* Collects site content from a
|
|
4
|
+
* Collects site content from a site directory structure:
|
|
5
5
|
* - site.yml: Site configuration
|
|
6
6
|
* - pages/: Directory of page folders
|
|
7
7
|
* - page.yml: Page metadata
|
|
8
8
|
* - *.md: Section content with YAML frontmatter
|
|
9
|
+
* - layout/: Layout panel folders (header, footer, left, right)
|
|
9
10
|
*
|
|
10
11
|
* Section frontmatter reserved properties:
|
|
11
12
|
* - type: Component type (e.g., "Hero", "Features")
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
26
|
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
26
|
-
import { join, parse } from 'node:path'
|
|
27
|
+
import { join, parse, resolve } from 'node:path'
|
|
27
28
|
import { existsSync } from 'node:fs'
|
|
28
29
|
import yaml from 'js-yaml'
|
|
29
30
|
import { collectSectionAssets, mergeAssetCollections } from './assets.js'
|
|
@@ -502,10 +503,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
502
503
|
|
|
503
504
|
// First, calculate the folder-based route (what the route would be without index handling)
|
|
504
505
|
let folderRoute
|
|
505
|
-
if (
|
|
506
|
-
// Special pages (layout areas) keep their @ prefix
|
|
507
|
-
folderRoute = parentRoute === '/' ? `/@${pageName.slice(1)}` : `${parentRoute}/@${pageName.slice(1)}`
|
|
508
|
-
} else if (isDynamic) {
|
|
506
|
+
if (isDynamic) {
|
|
509
507
|
// Dynamic routes: /blog/[slug] → /blog/:slug (for route matching)
|
|
510
508
|
folderRoute = parentRoute === '/' ? `/:${paramName}` : `${parentRoute}/:${paramName}`
|
|
511
509
|
} else {
|
|
@@ -648,10 +646,6 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
648
646
|
icons: new Set(),
|
|
649
647
|
bySource: new Map()
|
|
650
648
|
}
|
|
651
|
-
let header = null
|
|
652
|
-
let footer = null
|
|
653
|
-
let left = null
|
|
654
|
-
let right = null
|
|
655
649
|
let notFound = null
|
|
656
650
|
const versionedScopes = new Map() // scope route → versionMeta
|
|
657
651
|
|
|
@@ -737,8 +731,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
737
731
|
for (const [scope, meta] of subResult.versionedScopes) {
|
|
738
732
|
versionedScopes.set(scope, meta)
|
|
739
733
|
}
|
|
740
|
-
} else
|
|
741
|
-
// Non-version
|
|
734
|
+
} else {
|
|
735
|
+
// Non-version folders in a versioned section
|
|
742
736
|
// These could be shared across versions - process normally
|
|
743
737
|
const result = await processPage(entryPath, entry, siteRoot, {
|
|
744
738
|
isIndex: false,
|
|
@@ -755,14 +749,14 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
755
749
|
}
|
|
756
750
|
|
|
757
751
|
// Return early - we've handled all children
|
|
758
|
-
return { pages, assetCollection, iconCollection,
|
|
752
|
+
return { pages, assetCollection, iconCollection, notFound, versionedScopes }
|
|
759
753
|
}
|
|
760
754
|
|
|
761
755
|
// Determine which page is the index for this level
|
|
762
756
|
// A directory with its own .md content is a real page, not a container —
|
|
763
757
|
// never promote a child as index, even if explicit config says so
|
|
764
758
|
// (that config is likely a leftover from before the directory had content)
|
|
765
|
-
const regularFolders = pageFolders
|
|
759
|
+
const regularFolders = pageFolders
|
|
766
760
|
const hasExplicitOrder = orderConfig?.index || (Array.isArray(orderConfig?.pages) && orderConfig.pages.length > 0)
|
|
767
761
|
const hasMdContent = entries.some(e => isMarkdownFile(e))
|
|
768
762
|
const indexPageName = hasMdContent ? null : determineIndexPage(orderConfig, regularFolders)
|
|
@@ -771,12 +765,11 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
771
765
|
for (const folder of pageFolders) {
|
|
772
766
|
const { name: entry, path: entryPath, childOrderConfig } = folder
|
|
773
767
|
const isIndex = entry === indexPageName
|
|
774
|
-
const isSpecial = entry.startsWith('@')
|
|
775
768
|
|
|
776
769
|
// Process this directory as a page
|
|
777
770
|
// Pass parentFetch so dynamic routes can inherit parent's data schema
|
|
778
771
|
const result = await processPage(entryPath, entry, siteRoot, {
|
|
779
|
-
isIndex
|
|
772
|
+
isIndex,
|
|
780
773
|
parentRoute,
|
|
781
774
|
parentFetch,
|
|
782
775
|
versionContext
|
|
@@ -787,27 +780,15 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
787
780
|
assetCollection = mergeAssetCollections(assetCollection, pageAssets)
|
|
788
781
|
iconCollection = mergeIconCollections(iconCollection, pageIcons)
|
|
789
782
|
|
|
790
|
-
// Handle
|
|
791
|
-
if (parentRoute === '/') {
|
|
792
|
-
|
|
793
|
-
header = page
|
|
794
|
-
} else if (entry === '@footer') {
|
|
795
|
-
footer = page
|
|
796
|
-
} else if (entry === '@left') {
|
|
797
|
-
left = page
|
|
798
|
-
} else if (entry === '@right') {
|
|
799
|
-
right = page
|
|
800
|
-
} else if (entry === '404') {
|
|
801
|
-
notFound = page
|
|
802
|
-
} else {
|
|
803
|
-
pages.push(page)
|
|
804
|
-
}
|
|
783
|
+
// Handle 404 page - only at root level
|
|
784
|
+
if (parentRoute === '/' && entry === '404') {
|
|
785
|
+
notFound = page
|
|
805
786
|
} else {
|
|
806
787
|
pages.push(page)
|
|
807
788
|
}
|
|
808
789
|
|
|
809
|
-
// Recursively process subdirectories
|
|
810
|
-
|
|
790
|
+
// Recursively process subdirectories
|
|
791
|
+
{
|
|
811
792
|
// The child route depends on whether this page is the index
|
|
812
793
|
// For explicit index (from site.yml `index:` or `pages:`), children use parentRoute
|
|
813
794
|
// since that's a true structural promotion. For auto-detected index, children use
|
|
@@ -830,7 +811,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
830
811
|
}
|
|
831
812
|
}
|
|
832
813
|
|
|
833
|
-
return { pages, assetCollection, iconCollection,
|
|
814
|
+
return { pages, assetCollection, iconCollection, notFound, versionedScopes }
|
|
834
815
|
}
|
|
835
816
|
|
|
836
817
|
/**
|
|
@@ -863,6 +844,43 @@ async function loadFoundationVars(foundationPath) {
|
|
|
863
844
|
}
|
|
864
845
|
}
|
|
865
846
|
|
|
847
|
+
/**
|
|
848
|
+
* Collect layout panels from the layout/ directory
|
|
849
|
+
*
|
|
850
|
+
* Layout panels (header, footer, left, right) are persistent regions
|
|
851
|
+
* that appear on every page. They live in layout/ parallel to pages/.
|
|
852
|
+
*
|
|
853
|
+
* @param {string} layoutDir - Path to layout directory
|
|
854
|
+
* @param {string} siteRoot - Path to site root
|
|
855
|
+
* @returns {Promise<Object>} { header, footer, left, right }
|
|
856
|
+
*/
|
|
857
|
+
async function collectLayoutPanels(layoutDir, siteRoot) {
|
|
858
|
+
const result = { header: null, footer: null, left: null, right: null }
|
|
859
|
+
|
|
860
|
+
if (!existsSync(layoutDir)) return result
|
|
861
|
+
|
|
862
|
+
const knownPanels = ['header', 'footer', 'left', 'right']
|
|
863
|
+
const entries = await readdir(layoutDir)
|
|
864
|
+
|
|
865
|
+
for (const entry of entries) {
|
|
866
|
+
if (!knownPanels.includes(entry)) continue
|
|
867
|
+
const entryPath = join(layoutDir, entry)
|
|
868
|
+
const stats = await stat(entryPath)
|
|
869
|
+
if (!stats.isDirectory()) continue
|
|
870
|
+
|
|
871
|
+
const pageResult = await processPage(entryPath, entry, siteRoot, {
|
|
872
|
+
isIndex: false,
|
|
873
|
+
parentRoute: '/layout'
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
if (pageResult) {
|
|
877
|
+
result[entry] = pageResult.page
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return result
|
|
882
|
+
}
|
|
883
|
+
|
|
866
884
|
/**
|
|
867
885
|
* Collect all site content
|
|
868
886
|
*
|
|
@@ -873,10 +891,18 @@ async function loadFoundationVars(foundationPath) {
|
|
|
873
891
|
*/
|
|
874
892
|
export async function collectSiteContent(sitePath, options = {}) {
|
|
875
893
|
const { foundationPath } = options
|
|
876
|
-
const pagesPath = join(sitePath, 'pages')
|
|
877
894
|
|
|
878
895
|
// Read site config and raw theme config
|
|
879
896
|
const siteConfig = await readYamlFile(join(sitePath, 'site.yml'))
|
|
897
|
+
|
|
898
|
+
// Resolve content paths from site.yml paths: group, defaulting to standard locations
|
|
899
|
+
const pagesPath = siteConfig.paths?.pages
|
|
900
|
+
? resolve(sitePath, siteConfig.paths.pages)
|
|
901
|
+
: join(sitePath, 'pages')
|
|
902
|
+
|
|
903
|
+
const layoutPath = siteConfig.paths?.layout
|
|
904
|
+
? resolve(sitePath, siteConfig.paths.layout)
|
|
905
|
+
: join(sitePath, 'layout')
|
|
880
906
|
const rawThemeConfig = await readYamlFile(join(sitePath, 'theme.yml'))
|
|
881
907
|
|
|
882
908
|
// Load foundation vars and process theme
|
|
@@ -907,8 +933,11 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
907
933
|
index: siteConfig.index
|
|
908
934
|
}
|
|
909
935
|
|
|
936
|
+
// Collect layout panels from layout/ directory
|
|
937
|
+
const { header, footer, left, right } = await collectLayoutPanels(layoutPath, sitePath)
|
|
938
|
+
|
|
910
939
|
// Recursively collect all pages
|
|
911
|
-
const { pages, assetCollection, iconCollection,
|
|
940
|
+
const { pages, assetCollection, iconCollection, notFound, versionedScopes } =
|
|
912
941
|
await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig)
|
|
913
942
|
|
|
914
943
|
// Deduplicate: remove content-less container pages whose route duplicates
|
package/src/site/plugin.js
CHANGED
|
@@ -376,6 +376,9 @@ export function siteContentPlugin(options = {}) {
|
|
|
376
376
|
let collectionTranslations = {} // Cache: { locale: collection translations }
|
|
377
377
|
let localesDir = 'locales' // Default, updated from site config
|
|
378
378
|
let collectionsConfig = null // Cached for watcher setup
|
|
379
|
+
let resolvedPagesPath = null // Resolved from site.yml pagesDir or default
|
|
380
|
+
let resolvedLayoutPath = null // Resolved from site.yml layoutDir or default
|
|
381
|
+
let resolvedCollectionsBase = null // Resolved from site.yml collectionsDir
|
|
379
382
|
|
|
380
383
|
/**
|
|
381
384
|
* Load translations for a specific locale
|
|
@@ -486,15 +489,43 @@ export function siteContentPlugin(options = {}) {
|
|
|
486
489
|
const earlyContent = await collectSiteContent(resolvedSitePath, { foundationPath })
|
|
487
490
|
collectionsConfig = earlyContent.config?.collections
|
|
488
491
|
|
|
492
|
+
// Resolve content directory paths from site.yml paths: group
|
|
493
|
+
const paths = earlyContent?.config?.paths || {}
|
|
494
|
+
resolvedPagesPath = paths.pages
|
|
495
|
+
? resolve(resolvedSitePath, paths.pages)
|
|
496
|
+
: resolve(resolvedSitePath, pagesDir)
|
|
497
|
+
resolvedLayoutPath = paths.layout
|
|
498
|
+
? resolve(resolvedSitePath, paths.layout)
|
|
499
|
+
: resolve(resolvedSitePath, 'layout')
|
|
500
|
+
resolvedCollectionsBase = paths.collections
|
|
501
|
+
? resolve(resolvedSitePath, paths.collections)
|
|
502
|
+
: null
|
|
503
|
+
|
|
489
504
|
if (collectionsConfig) {
|
|
490
505
|
console.log('[site-content] Processing content collections...')
|
|
491
|
-
const collections = await processCollections(resolvedSitePath, collectionsConfig)
|
|
506
|
+
const collections = await processCollections(resolvedSitePath, collectionsConfig, resolvedCollectionsBase)
|
|
492
507
|
await writeCollectionFiles(resolvedSitePath, collections)
|
|
493
508
|
}
|
|
494
509
|
} catch (err) {
|
|
495
510
|
console.warn('[site-content] Early collection processing failed:', err.message)
|
|
496
511
|
}
|
|
497
512
|
}
|
|
513
|
+
|
|
514
|
+
// In production, resolve content paths from site.yml directly
|
|
515
|
+
if (isProduction || !resolvedPagesPath) {
|
|
516
|
+
const { readSiteConfig } = await import('./config.js')
|
|
517
|
+
const cfg = readSiteConfig(resolvedSitePath)
|
|
518
|
+
const paths = cfg.paths || {}
|
|
519
|
+
resolvedPagesPath = paths.pages
|
|
520
|
+
? resolve(resolvedSitePath, paths.pages)
|
|
521
|
+
: resolve(resolvedSitePath, pagesDir)
|
|
522
|
+
resolvedLayoutPath = paths.layout
|
|
523
|
+
? resolve(resolvedSitePath, paths.layout)
|
|
524
|
+
: resolve(resolvedSitePath, 'layout')
|
|
525
|
+
resolvedCollectionsBase = paths.collections
|
|
526
|
+
? resolve(resolvedSitePath, paths.collections)
|
|
527
|
+
: null
|
|
528
|
+
}
|
|
498
529
|
},
|
|
499
530
|
|
|
500
531
|
async buildStart() {
|
|
@@ -508,7 +539,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
508
539
|
// In production, do it here
|
|
509
540
|
if (isProduction && siteContent.config?.collections) {
|
|
510
541
|
console.log('[site-content] Processing content collections...')
|
|
511
|
-
const collections = await processCollections(resolvedSitePath, siteContent.config.collections)
|
|
542
|
+
const collections = await processCollections(resolvedSitePath, siteContent.config.collections, resolvedCollectionsBase)
|
|
512
543
|
await writeCollectionFiles(resolvedSitePath, collections)
|
|
513
544
|
}
|
|
514
545
|
|
|
@@ -537,7 +568,6 @@ export function siteContentPlugin(options = {}) {
|
|
|
537
568
|
|
|
538
569
|
// Watch for content changes in dev mode
|
|
539
570
|
if (shouldWatch) {
|
|
540
|
-
const pagesPath = resolve(resolvedSitePath, pagesDir)
|
|
541
571
|
const siteYmlPath = resolve(resolvedSitePath, 'site.yml')
|
|
542
572
|
const themeYmlPath = resolve(resolvedSitePath, 'theme.yml')
|
|
543
573
|
|
|
@@ -571,7 +601,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
571
601
|
// Use collectionsConfig (cached from configResolved) or siteContent
|
|
572
602
|
const collections = collectionsConfig || siteContent?.config?.collections
|
|
573
603
|
if (collections) {
|
|
574
|
-
const processed = await processCollections(resolvedSitePath, collections)
|
|
604
|
+
const processed = await processCollections(resolvedSitePath, collections, resolvedCollectionsBase)
|
|
575
605
|
await writeCollectionFiles(resolvedSitePath, processed)
|
|
576
606
|
}
|
|
577
607
|
// Send full reload to client
|
|
@@ -585,12 +615,24 @@ export function siteContentPlugin(options = {}) {
|
|
|
585
615
|
// Track all watchers for cleanup
|
|
586
616
|
const watchers = []
|
|
587
617
|
|
|
588
|
-
// Watch pages directory
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
618
|
+
// Watch pages directory (resolved from site.yml pagesDir or default)
|
|
619
|
+
if (existsSync(resolvedPagesPath)) {
|
|
620
|
+
try {
|
|
621
|
+
watchers.push(watch(resolvedPagesPath, { recursive: true }, scheduleRebuild))
|
|
622
|
+
console.log(`[site-content] Watching ${resolvedPagesPath}`)
|
|
623
|
+
} catch (err) {
|
|
624
|
+
console.warn('[site-content] Could not watch pages directory:', err.message)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Watch layout directory (resolved from site.yml layoutDir or default)
|
|
629
|
+
if (existsSync(resolvedLayoutPath)) {
|
|
630
|
+
try {
|
|
631
|
+
watchers.push(watch(resolvedLayoutPath, { recursive: true }, scheduleRebuild))
|
|
632
|
+
console.log(`[site-content] Watching ${resolvedLayoutPath}`)
|
|
633
|
+
} catch (err) {
|
|
634
|
+
console.warn('[site-content] Could not watch layout directory:', err.message)
|
|
635
|
+
}
|
|
594
636
|
}
|
|
595
637
|
|
|
596
638
|
// Watch site.yml
|
|
@@ -611,10 +653,11 @@ export function siteContentPlugin(options = {}) {
|
|
|
611
653
|
// Use collectionsConfig cached from configResolved (siteContent may be null here)
|
|
612
654
|
if (collectionsConfig) {
|
|
613
655
|
const contentPaths = new Set()
|
|
656
|
+
const collectionBase = resolvedCollectionsBase || resolvedSitePath
|
|
614
657
|
for (const config of Object.values(collectionsConfig)) {
|
|
615
658
|
const collectionPath = typeof config === 'string' ? config : config.path
|
|
616
659
|
if (collectionPath) {
|
|
617
|
-
contentPaths.add(resolve(
|
|
660
|
+
contentPaths.add(resolve(collectionBase, collectionPath))
|
|
618
661
|
}
|
|
619
662
|
}
|
|
620
663
|
|