@uniweb/build 0.6.20 → 0.7.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 +3 -3
- package/src/generate-entry.js +63 -9
- package/src/prerender.js +15 -15
- package/src/runtime-schema.js +53 -0
- package/src/schema.js +95 -2
- package/src/site/asset-processor.js +7 -6
- package/src/site/content-collector.js +209 -86
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
53
|
"@uniweb/content-reader": "1.1.2",
|
|
54
|
-
"@uniweb/runtime": "0.5.
|
|
54
|
+
"@uniweb/runtime": "0.5.23",
|
|
55
55
|
"@uniweb/schemas": "0.2.1"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
@@ -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.
|
|
64
|
+
"@uniweb/core": "0.5.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"vite": {
|
package/src/generate-entry.js
CHANGED
|
@@ -22,20 +22,22 @@
|
|
|
22
22
|
import { writeFile, mkdir } from 'node:fs/promises'
|
|
23
23
|
import { existsSync } from 'node:fs'
|
|
24
24
|
import { join, dirname } from 'node:path'
|
|
25
|
-
import { discoverComponents } from './schema.js'
|
|
26
|
-
import { extractAllRuntimeSchemas } from './runtime-schema.js'
|
|
25
|
+
import { discoverComponents, discoverLayoutsInPath } from './schema.js'
|
|
26
|
+
import { extractAllRuntimeSchemas, extractAllLayoutRuntimeSchemas } from './runtime-schema.js'
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Detect foundation config/exports file (for
|
|
29
|
+
* Detect foundation config/exports file (for props, vars, etc.)
|
|
30
30
|
*
|
|
31
31
|
* Looks for (in order of preference):
|
|
32
32
|
* 1. foundation.js - New consolidated format
|
|
33
33
|
* 2. exports.js - Legacy format (for backward compatibility)
|
|
34
34
|
*
|
|
35
35
|
* The file should export:
|
|
36
|
-
* - Layout (optional) - Custom page layout component
|
|
37
36
|
* - props (optional) - Foundation-wide props
|
|
38
37
|
* - vars (optional) - CSS custom properties (also read by schema builder)
|
|
38
|
+
* - defaultLayout (optional) - Default layout name
|
|
39
|
+
*
|
|
40
|
+
* Note: Layout components are now discovered from src/layouts/
|
|
39
41
|
*/
|
|
40
42
|
function detectFoundationExports(srcDir) {
|
|
41
43
|
// Prefer foundation.js (new consolidated format)
|
|
@@ -92,9 +94,12 @@ function generateEntrySource(components, options = {}) {
|
|
|
92
94
|
cssPath = null,
|
|
93
95
|
foundationExports = null,
|
|
94
96
|
meta = {},
|
|
97
|
+
layouts = {},
|
|
98
|
+
layoutMeta = {},
|
|
95
99
|
} = options
|
|
96
100
|
|
|
97
101
|
const componentNames = Object.keys(components).sort()
|
|
102
|
+
const layoutNames = Object.keys(layouts).sort()
|
|
98
103
|
|
|
99
104
|
const lines = [
|
|
100
105
|
'// Auto-generated foundation entry point',
|
|
@@ -107,9 +112,10 @@ function generateEntrySource(components, options = {}) {
|
|
|
107
112
|
lines.push(`import '${cssPath}'`)
|
|
108
113
|
}
|
|
109
114
|
|
|
110
|
-
// Foundation capabilities import (for
|
|
115
|
+
// Foundation capabilities import (for props, vars, etc.)
|
|
116
|
+
// Note: Layout/layouts no longer merged from foundation.js — layouts come from src/layouts/ discovery
|
|
111
117
|
if (foundationExports) {
|
|
112
|
-
lines.push(`import
|
|
118
|
+
lines.push(`import * as _foundationModule from '${foundationExports.path}'`)
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
// Component imports
|
|
@@ -118,6 +124,12 @@ function generateEntrySource(components, options = {}) {
|
|
|
118
124
|
lines.push(`import ${name} from './${path}/${entryFile}'`)
|
|
119
125
|
}
|
|
120
126
|
|
|
127
|
+
// Layout imports
|
|
128
|
+
for (const name of layoutNames) {
|
|
129
|
+
const { path, entryFile = `index.js` } = layouts[name]
|
|
130
|
+
lines.push(`import ${name} from './${path}/${entryFile}'`)
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
lines.push('')
|
|
122
134
|
|
|
123
135
|
// Export components object
|
|
@@ -129,9 +141,17 @@ function generateEntrySource(components, options = {}) {
|
|
|
129
141
|
lines.push('export const components = {}')
|
|
130
142
|
}
|
|
131
143
|
|
|
132
|
-
// Foundation capabilities (
|
|
144
|
+
// Foundation capabilities (props, vars, etc. + discovered layouts)
|
|
133
145
|
lines.push('')
|
|
134
|
-
if (foundationExports) {
|
|
146
|
+
if (foundationExports || layoutNames.length > 0) {
|
|
147
|
+
const capParts = []
|
|
148
|
+
if (foundationExports) {
|
|
149
|
+
capParts.push('..._foundationModule.default')
|
|
150
|
+
}
|
|
151
|
+
if (layoutNames.length > 0) {
|
|
152
|
+
capParts.push(`layouts: { ${layoutNames.join(', ')} }`)
|
|
153
|
+
}
|
|
154
|
+
lines.push(`const capabilities = { ${capParts.join(', ')} }`)
|
|
135
155
|
lines.push('export { capabilities }')
|
|
136
156
|
} else {
|
|
137
157
|
lines.push('export const capabilities = null')
|
|
@@ -147,6 +167,16 @@ function generateEntrySource(components, options = {}) {
|
|
|
147
167
|
lines.push('export const meta = {}')
|
|
148
168
|
}
|
|
149
169
|
|
|
170
|
+
// Per-layout runtime metadata (areas, transitions, defaults)
|
|
171
|
+
lines.push('')
|
|
172
|
+
if (Object.keys(layoutMeta).length > 0) {
|
|
173
|
+
const layoutMetaJson = JSON.stringify(layoutMeta, null, 2)
|
|
174
|
+
lines.push(`// Per-layout runtime metadata (from meta.js)`)
|
|
175
|
+
lines.push(`export const layoutMeta = ${layoutMetaJson}`)
|
|
176
|
+
} else {
|
|
177
|
+
lines.push('export const layoutMeta = {}')
|
|
178
|
+
}
|
|
179
|
+
|
|
150
180
|
lines.push('')
|
|
151
181
|
|
|
152
182
|
return lines.join('\n')
|
|
@@ -201,6 +231,10 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
201
231
|
console.warn('Warning: No section types found')
|
|
202
232
|
}
|
|
203
233
|
|
|
234
|
+
// Discover layouts from src/layouts/
|
|
235
|
+
const layouts = await discoverLayoutsInPath(srcDir)
|
|
236
|
+
const layoutNames = Object.keys(layouts).sort()
|
|
237
|
+
|
|
204
238
|
// Detect entry files for each component
|
|
205
239
|
// Bare files discovered in sections/ already have entryFile set — skip detection for those
|
|
206
240
|
for (const name of componentNames) {
|
|
@@ -212,20 +246,35 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
212
246
|
}
|
|
213
247
|
}
|
|
214
248
|
|
|
249
|
+
// Detect entry files for each layout (same logic as components)
|
|
250
|
+
for (const name of layoutNames) {
|
|
251
|
+
const layout = layouts[name]
|
|
252
|
+
if (!layout.entryFile) {
|
|
253
|
+
const entry = detectComponentEntry(srcDir, layout.path, layout.name)
|
|
254
|
+
layout.ext = entry.ext
|
|
255
|
+
layout.entryFile = entry.file
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
215
259
|
// Check for CSS file
|
|
216
260
|
const cssPath = detectCssFile(srcDir)
|
|
217
261
|
|
|
218
|
-
// Check for foundation exports (
|
|
262
|
+
// Check for foundation exports (props, vars, etc.)
|
|
219
263
|
const foundationExports = detectFoundationExports(srcDir)
|
|
220
264
|
|
|
221
265
|
// Extract per-component runtime metadata from meta.js files
|
|
222
266
|
const meta = extractAllRuntimeSchemas(components)
|
|
223
267
|
|
|
268
|
+
// Extract per-layout runtime metadata from meta.js files
|
|
269
|
+
const layoutMeta = extractAllLayoutRuntimeSchemas(layouts)
|
|
270
|
+
|
|
224
271
|
// Generate source
|
|
225
272
|
const source = generateEntrySource(components, {
|
|
226
273
|
cssPath,
|
|
227
274
|
foundationExports,
|
|
228
275
|
meta,
|
|
276
|
+
layouts,
|
|
277
|
+
layoutMeta,
|
|
229
278
|
})
|
|
230
279
|
|
|
231
280
|
// Write to file
|
|
@@ -235,6 +284,9 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
235
284
|
|
|
236
285
|
console.log(`Generated entry point: ${output}`)
|
|
237
286
|
console.log(` - ${componentNames.length} components: ${componentNames.join(', ')}`)
|
|
287
|
+
if (layoutNames.length > 0) {
|
|
288
|
+
console.log(` - ${layoutNames.length} layouts: ${layoutNames.join(', ')}`)
|
|
289
|
+
}
|
|
238
290
|
if (foundationExports) {
|
|
239
291
|
console.log(` - Foundation exports found: ${foundationExports.path}`)
|
|
240
292
|
}
|
|
@@ -242,8 +294,10 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
242
294
|
return {
|
|
243
295
|
outputPath: output,
|
|
244
296
|
componentNames,
|
|
297
|
+
layoutNames,
|
|
245
298
|
foundationExports,
|
|
246
299
|
meta,
|
|
300
|
+
layoutMeta,
|
|
247
301
|
}
|
|
248
302
|
}
|
|
249
303
|
|
package/src/prerender.js
CHANGED
|
@@ -569,33 +569,33 @@ function renderBlocks(blocks) {
|
|
|
569
569
|
* Render page layout for SSR
|
|
570
570
|
*/
|
|
571
571
|
function renderLayout(page, website) {
|
|
572
|
-
const
|
|
572
|
+
const layoutName = page.getLayoutName()
|
|
573
|
+
const RemoteLayout = website.getRemoteLayout(layoutName)
|
|
574
|
+
const layoutMeta = website.getLayoutMeta(layoutName)
|
|
573
575
|
|
|
574
|
-
const headerBlocks = page.getHeaderBlocks()
|
|
575
576
|
const bodyBlocks = page.getBodyBlocks()
|
|
576
|
-
const
|
|
577
|
-
const leftBlocks = page.getLeftBlocks()
|
|
578
|
-
const rightBlocks = page.getRightBlocks()
|
|
577
|
+
const areas = page.getLayoutAreas()
|
|
579
578
|
|
|
580
|
-
const headerElement = headerBlocks ? renderBlocks(headerBlocks) : null
|
|
581
579
|
const bodyElement = bodyBlocks ? renderBlocks(bodyBlocks) : null
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
|
|
580
|
+
const areaElements = {}
|
|
581
|
+
for (const [name, blocks] of Object.entries(areas)) {
|
|
582
|
+
areaElements[name] = renderBlocks(blocks)
|
|
583
|
+
}
|
|
585
584
|
|
|
586
585
|
if (RemoteLayout) {
|
|
586
|
+
const params = { ...(layoutMeta?.defaults || {}), ...(page.getLayoutParams() || {}) }
|
|
587
|
+
|
|
587
588
|
return React.createElement(RemoteLayout, {
|
|
588
|
-
page, website,
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
leftPanel: leftElement, rightPanel: rightElement
|
|
589
|
+
page, website, params,
|
|
590
|
+
body: bodyElement,
|
|
591
|
+
...areaElements,
|
|
592
592
|
})
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
return React.createElement(React.Fragment, null,
|
|
596
|
-
|
|
596
|
+
areaElements.header && React.createElement('header', null, areaElements.header),
|
|
597
597
|
bodyElement && React.createElement('main', null, bodyElement),
|
|
598
|
-
|
|
598
|
+
areaElements.footer && React.createElement('footer', null, areaElements.footer)
|
|
599
599
|
)
|
|
600
600
|
}
|
|
601
601
|
|
package/src/runtime-schema.js
CHANGED
|
@@ -288,3 +288,56 @@ export function extractAllRuntimeSchemas(componentsMeta) {
|
|
|
288
288
|
|
|
289
289
|
return schemas
|
|
290
290
|
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Extract lean runtime schema for a layout from its full meta.js
|
|
294
|
+
*
|
|
295
|
+
* Layout runtime metadata:
|
|
296
|
+
* - areas: Array of area names this layout supports
|
|
297
|
+
* - transitions: View transition name mapping (stored but not acted on yet)
|
|
298
|
+
* - defaults: Param default values
|
|
299
|
+
*
|
|
300
|
+
* @param {Object} fullMeta - The full meta.js default export for a layout
|
|
301
|
+
* @returns {Object|null} - Lean layout runtime schema or null if empty
|
|
302
|
+
*/
|
|
303
|
+
export function extractLayoutRuntimeSchema(fullMeta) {
|
|
304
|
+
if (!fullMeta || typeof fullMeta !== 'object') {
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const runtime = {}
|
|
309
|
+
|
|
310
|
+
if (fullMeta.areas && Array.isArray(fullMeta.areas)) {
|
|
311
|
+
runtime.areas = fullMeta.areas
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (fullMeta.transitions && typeof fullMeta.transitions === 'object') {
|
|
315
|
+
runtime.transitions = fullMeta.transitions
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const defaults = extractParamDefaults(fullMeta.params)
|
|
319
|
+
if (defaults) {
|
|
320
|
+
runtime.defaults = defaults
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return Object.keys(runtime).length > 0 ? runtime : null
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Extract runtime schemas for all layouts
|
|
328
|
+
*
|
|
329
|
+
* @param {Object} layoutsMeta - Map of layoutName -> meta.js content
|
|
330
|
+
* @returns {Object} - Map of layoutName -> layout runtime schema (excludes null entries)
|
|
331
|
+
*/
|
|
332
|
+
export function extractAllLayoutRuntimeSchemas(layoutsMeta) {
|
|
333
|
+
const schemas = {}
|
|
334
|
+
|
|
335
|
+
for (const [name, meta] of Object.entries(layoutsMeta)) {
|
|
336
|
+
const schema = extractLayoutRuntimeSchema(meta)
|
|
337
|
+
if (schema) {
|
|
338
|
+
schemas[name] = schema
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return schemas
|
|
343
|
+
}
|
package/src/schema.js
CHANGED
|
@@ -32,6 +32,9 @@ const COMPONENT_EXTENSIONS = new Set(['.jsx', '.tsx', '.js', '.ts'])
|
|
|
32
32
|
// The primary sections path where relaxed discovery applies
|
|
33
33
|
const SECTIONS_PATH = 'sections'
|
|
34
34
|
|
|
35
|
+
// The layouts path where layout components are discovered
|
|
36
|
+
const LAYOUTS_PATH = 'layouts'
|
|
37
|
+
|
|
35
38
|
/**
|
|
36
39
|
* Load a meta.js file via dynamic import
|
|
37
40
|
*/
|
|
@@ -107,10 +110,11 @@ export async function loadFoundationConfig(srcDir) {
|
|
|
107
110
|
try {
|
|
108
111
|
const module = await import(pathToFileURL(filePath).href)
|
|
109
112
|
// Support both default export and named exports
|
|
113
|
+
// Note: Layout/layouts no longer read from foundation.js — layouts come from src/layouts/ discovery
|
|
110
114
|
return {
|
|
111
115
|
...module.default,
|
|
112
116
|
vars: module.vars || module.default?.vars,
|
|
113
|
-
|
|
117
|
+
defaultLayout: module.default?.defaultLayout,
|
|
114
118
|
}
|
|
115
119
|
} catch (error) {
|
|
116
120
|
console.warn(`Warning: Failed to load foundation config ${filePath}:`, error.message)
|
|
@@ -252,6 +256,87 @@ async function discoverSectionsInPath(srcDir, sectionsRelPath) {
|
|
|
252
256
|
return components
|
|
253
257
|
}
|
|
254
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Discover layout components in src/layouts/ with relaxed rules
|
|
261
|
+
*
|
|
262
|
+
* Same discovery pattern as sections: root-level files and directories
|
|
263
|
+
* are addressable by default. No recursion — layouts are flat.
|
|
264
|
+
*
|
|
265
|
+
* @param {string} srcDir - Source directory (e.g., 'src')
|
|
266
|
+
* @param {string} layoutsRelPath - Relative path to layouts dir (e.g., 'layouts')
|
|
267
|
+
* @returns {Object} Map of layoutName -> { name, path, ...meta }
|
|
268
|
+
*/
|
|
269
|
+
export async function discoverLayoutsInPath(srcDir, layoutsRelPath = LAYOUTS_PATH) {
|
|
270
|
+
const fullPath = join(srcDir, layoutsRelPath)
|
|
271
|
+
|
|
272
|
+
if (!existsSync(fullPath)) {
|
|
273
|
+
return {}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const entries = await readdir(fullPath, { withFileTypes: true })
|
|
277
|
+
const layouts = {}
|
|
278
|
+
|
|
279
|
+
// Collect names from both files and directories to detect collisions
|
|
280
|
+
const fileNames = new Set()
|
|
281
|
+
const dirNames = new Set()
|
|
282
|
+
|
|
283
|
+
for (const entry of entries) {
|
|
284
|
+
const ext = extname(entry.name)
|
|
285
|
+
if (entry.isFile() && COMPONENT_EXTENSIONS.has(ext)) {
|
|
286
|
+
const name = basename(entry.name, ext)
|
|
287
|
+
if (isComponentFileName(name)) {
|
|
288
|
+
fileNames.add(name)
|
|
289
|
+
}
|
|
290
|
+
} else if (entry.isDirectory()) {
|
|
291
|
+
dirNames.add(entry.name)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check for name collisions (e.g., DocsLayout.jsx AND DocsLayout/)
|
|
296
|
+
for (const name of fileNames) {
|
|
297
|
+
if (dirNames.has(name)) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Name collision in ${layoutsRelPath}/: both "${name}.jsx" (or similar) and "${name}/" exist. ` +
|
|
300
|
+
`Use one or the other, not both.`
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Discover bare files at root
|
|
306
|
+
for (const entry of entries) {
|
|
307
|
+
if (!entry.isFile()) continue
|
|
308
|
+
const ext = extname(entry.name)
|
|
309
|
+
if (!COMPONENT_EXTENSIONS.has(ext)) continue
|
|
310
|
+
const name = basename(entry.name, ext)
|
|
311
|
+
if (!isComponentFileName(name)) continue
|
|
312
|
+
|
|
313
|
+
const meta = createImplicitMeta(name)
|
|
314
|
+
layouts[name] = {
|
|
315
|
+
...buildComponentEntry(name, layoutsRelPath, meta),
|
|
316
|
+
entryFile: entry.name,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Discover directories at root (no recursion for layouts)
|
|
321
|
+
for (const entry of entries) {
|
|
322
|
+
if (!entry.isDirectory()) continue
|
|
323
|
+
if (!isComponentFileName(entry.name)) continue
|
|
324
|
+
|
|
325
|
+
const dirPath = join(fullPath, entry.name)
|
|
326
|
+
const relativePath = join(layoutsRelPath, entry.name)
|
|
327
|
+
const result = await loadComponentMeta(dirPath)
|
|
328
|
+
|
|
329
|
+
if (result && result.meta) {
|
|
330
|
+
if (result.meta.exposed === false) continue
|
|
331
|
+
layouts[entry.name] = buildComponentEntry(entry.name, relativePath, result.meta)
|
|
332
|
+
} else if (hasEntryFile(dirPath, entry.name)) {
|
|
333
|
+
layouts[entry.name] = buildComponentEntry(entry.name, relativePath, createImplicitMeta(entry.name))
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return layouts
|
|
338
|
+
}
|
|
339
|
+
|
|
255
340
|
/**
|
|
256
341
|
* Recursively discover nested section types that have meta.js
|
|
257
342
|
*
|
|
@@ -376,12 +461,20 @@ export async function buildSchema(srcDir, componentPaths) {
|
|
|
376
461
|
// Discover components
|
|
377
462
|
const components = await discoverComponents(srcDir, componentPaths)
|
|
378
463
|
|
|
464
|
+
// Discover layouts from src/layouts/
|
|
465
|
+
const layouts = await discoverLayoutsInPath(srcDir)
|
|
466
|
+
|
|
379
467
|
return {
|
|
380
|
-
// Merge identity and config - identity fields take precedence
|
|
381
468
|
_self: {
|
|
382
469
|
...foundationConfig,
|
|
383
470
|
...identity,
|
|
471
|
+
// foundation.js overrides package.json for editor-facing identity
|
|
472
|
+
...(foundationConfig.name && { name: foundationConfig.name }),
|
|
473
|
+
...(foundationConfig.description && { description: foundationConfig.description }),
|
|
474
|
+
...(foundationConfig.defaultLayout && { defaultLayout: foundationConfig.defaultLayout }),
|
|
384
475
|
},
|
|
476
|
+
// Layout metadata (full, for editor)
|
|
477
|
+
...(Object.keys(layouts).length > 0 && { _layouts: layouts }),
|
|
385
478
|
...components,
|
|
386
479
|
}
|
|
387
480
|
}
|
|
@@ -312,12 +312,13 @@ export function rewriteSiteContentPaths(siteContent, pathMapping) {
|
|
|
312
312
|
result.pages.forEach(processPage)
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
// Process
|
|
316
|
-
if (result.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
315
|
+
// Process layout area sets
|
|
316
|
+
if (result.layouts) {
|
|
317
|
+
for (const [name, areas] of Object.entries(result.layouts)) {
|
|
318
|
+
for (const areaPage of Object.values(areas)) {
|
|
319
|
+
if (areaPage) processPage(areaPage)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
321
322
|
}
|
|
322
323
|
|
|
323
324
|
// Remove the assets manifest from output (no longer needed at runtime)
|
|
@@ -535,12 +535,7 @@ async function processFileAsPage(filePath, fileName, siteRoot, parentRoute) {
|
|
|
535
535
|
hidden: false,
|
|
536
536
|
hideInHeader: false,
|
|
537
537
|
hideInFooter: false,
|
|
538
|
-
layout: {
|
|
539
|
-
header: true,
|
|
540
|
-
footer: true,
|
|
541
|
-
leftPanel: true,
|
|
542
|
-
rightPanel: true
|
|
543
|
-
},
|
|
538
|
+
layout: {},
|
|
544
539
|
seo: {
|
|
545
540
|
noindex: false,
|
|
546
541
|
image: null,
|
|
@@ -765,7 +760,7 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
|
|
|
765
760
|
* @param {Object} options.versionContext - Version context from parent { version, versionMeta, scope }
|
|
766
761
|
* @returns {Object} Page data with assets manifest
|
|
767
762
|
*/
|
|
768
|
-
async function processPage(pagePath, pageName, siteRoot, { isIndex = false, parentRoute = '/', parentFetch = null, versionContext = null } = {}) {
|
|
763
|
+
async function processPage(pagePath, pageName, siteRoot, { isIndex = false, parentRoute = '/', parentFetch = null, versionContext = null, layoutName = null } = {}) {
|
|
769
764
|
const pageConfig = await readYamlFile(join(pagePath, 'page.yml'))
|
|
770
765
|
|
|
771
766
|
// Note: We no longer skip hidden pages here - they still exist as valid pages,
|
|
@@ -925,7 +920,15 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
925
920
|
const sourcePath = isIndex ? folderRoute : null
|
|
926
921
|
|
|
927
922
|
// Extract configuration
|
|
928
|
-
const { seo = {}, layout
|
|
923
|
+
const { seo = {}, layout: layoutConfig, ...restConfig } = pageConfig
|
|
924
|
+
|
|
925
|
+
// Resolve layout name: page.yml layout (string or object.name) > inherited from parent > null
|
|
926
|
+
const pageLayoutName = typeof layoutConfig === 'string' ? layoutConfig
|
|
927
|
+
: layoutConfig?.name || null
|
|
928
|
+
const resolvedLayoutName = pageLayoutName || layoutName || null
|
|
929
|
+
|
|
930
|
+
// Layout panel visibility (from object form of layout config)
|
|
931
|
+
const layoutObj = typeof layoutConfig === 'object' && layoutConfig !== null ? layoutConfig : {}
|
|
929
932
|
|
|
930
933
|
// For dynamic routes, determine the parent's data schema
|
|
931
934
|
// This tells prerender which data array to iterate over
|
|
@@ -960,12 +963,11 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
960
963
|
hideInHeader: pageConfig.hideInHeader || false, // Hide from header nav
|
|
961
964
|
hideInFooter: pageConfig.hideInFooter || false, // Hide from footer nav
|
|
962
965
|
|
|
963
|
-
// Layout options (per-page overrides)
|
|
966
|
+
// Layout options (named layout + per-page overrides)
|
|
964
967
|
layout: {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
rightPanel: layout.rightPanel !== false // Show right panel (default true)
|
|
968
|
+
...(resolvedLayoutName ? { name: resolvedLayoutName } : {}),
|
|
969
|
+
...(layoutObj.hide ? { hide: layoutObj.hide } : {}),
|
|
970
|
+
...(layoutObj.params ? { params: layoutObj.params } : {}),
|
|
969
971
|
},
|
|
970
972
|
|
|
971
973
|
seo: {
|
|
@@ -1045,7 +1047,7 @@ function determineIndexPage(orderConfig, availableFolders) {
|
|
|
1045
1047
|
* @param {string} contentMode - 'sections' (default) or 'pages' (md files are child pages)
|
|
1046
1048
|
* @returns {Promise<Object>} { pages, assetCollection, iconCollection, notFound, versionedScopes }
|
|
1047
1049
|
*/
|
|
1048
|
-
async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig = {}, parentFetch = null, versionContext = null, contentMode = 'sections', mounts = null) {
|
|
1050
|
+
async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig = {}, parentFetch = null, versionContext = null, contentMode = 'sections', mounts = null, parentLayoutName = null) {
|
|
1049
1051
|
const entries = await readdir(dirPath)
|
|
1050
1052
|
const pages = []
|
|
1051
1053
|
let assetCollection = {
|
|
@@ -1072,6 +1074,10 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1072
1074
|
const { config: dirConfig, mode: dirMode } = await readFolderConfig(entryPath, contentMode)
|
|
1073
1075
|
const numericOrder = typeof dirConfig.order === 'number' ? dirConfig.order : undefined
|
|
1074
1076
|
|
|
1077
|
+
// Extract layout name from folder config (folder.yml layout: or page.yml layout:)
|
|
1078
|
+
const folderLayout = typeof dirConfig.layout === 'string' ? dirConfig.layout
|
|
1079
|
+
: dirConfig.layout?.name || null
|
|
1080
|
+
|
|
1075
1081
|
pageFolders.push({
|
|
1076
1082
|
name: entry,
|
|
1077
1083
|
path: entryPath,
|
|
@@ -1081,7 +1087,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1081
1087
|
childOrderConfig: {
|
|
1082
1088
|
pages: dirConfig.pages,
|
|
1083
1089
|
index: dirConfig.index
|
|
1084
|
-
}
|
|
1090
|
+
},
|
|
1091
|
+
childLayoutName: folderLayout
|
|
1085
1092
|
})
|
|
1086
1093
|
}
|
|
1087
1094
|
|
|
@@ -1090,6 +1097,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1090
1097
|
for (const [routeSegment, mountPath] of mounts) {
|
|
1091
1098
|
if (!pageFolders.some(f => f.name === routeSegment)) {
|
|
1092
1099
|
const { config: mountConfig } = await readFolderConfig(mountPath, 'pages')
|
|
1100
|
+
const mountLayout = typeof mountConfig.layout === 'string' ? mountConfig.layout
|
|
1101
|
+
: mountConfig.layout?.name || null
|
|
1093
1102
|
pageFolders.push({
|
|
1094
1103
|
name: routeSegment,
|
|
1095
1104
|
path: mountPath,
|
|
@@ -1099,7 +1108,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1099
1108
|
childOrderConfig: {
|
|
1100
1109
|
pages: mountConfig.pages,
|
|
1101
1110
|
index: mountConfig.index
|
|
1102
|
-
}
|
|
1111
|
+
},
|
|
1112
|
+
childLayoutName: mountLayout
|
|
1103
1113
|
})
|
|
1104
1114
|
}
|
|
1105
1115
|
}
|
|
@@ -1140,7 +1150,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1140
1150
|
versionedScopes.set(parentRoute, versionMeta)
|
|
1141
1151
|
|
|
1142
1152
|
for (const folder of orderedFolders) {
|
|
1143
|
-
const { name: entry, path: entryPath, childOrderConfig } = folder
|
|
1153
|
+
const { name: entry, path: entryPath, childOrderConfig, childLayoutName } = folder
|
|
1144
1154
|
|
|
1145
1155
|
if (isVersionFolder(entry)) {
|
|
1146
1156
|
const versionInfo = versionMeta.versions.find(v => v.id === entry)
|
|
@@ -1153,7 +1163,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1153
1163
|
|
|
1154
1164
|
const subResult = await collectPagesRecursive(
|
|
1155
1165
|
entryPath, versionRoute, siteRoot, childOrderConfig, parentFetch,
|
|
1156
|
-
{ version: versionInfo, versionMeta, scope: parentRoute }
|
|
1166
|
+
{ version: versionInfo, versionMeta, scope: parentRoute },
|
|
1167
|
+
'sections', null, childLayoutName || parentLayoutName
|
|
1157
1168
|
)
|
|
1158
1169
|
|
|
1159
1170
|
pages.push(...subResult.pages)
|
|
@@ -1164,7 +1175,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1164
1175
|
}
|
|
1165
1176
|
} else {
|
|
1166
1177
|
const result = await processPage(entryPath, entry, siteRoot, {
|
|
1167
|
-
isIndex: false, parentRoute, parentFetch
|
|
1178
|
+
isIndex: false, parentRoute, parentFetch,
|
|
1179
|
+
layoutName: childLayoutName || parentLayoutName
|
|
1168
1180
|
})
|
|
1169
1181
|
if (result) {
|
|
1170
1182
|
pages.push(result.page)
|
|
@@ -1228,18 +1240,25 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1228
1240
|
page.route = parentRoute
|
|
1229
1241
|
}
|
|
1230
1242
|
|
|
1243
|
+
// Inherit layout name from parent (folder.yml or site.yml cascade)
|
|
1244
|
+
if (parentLayoutName && !page.layout.name) {
|
|
1245
|
+
page.layout.name = parentLayoutName
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1231
1248
|
pages.push(page)
|
|
1232
1249
|
}
|
|
1233
1250
|
|
|
1234
1251
|
// Process subdirectories
|
|
1235
1252
|
for (const folder of orderedFolders) {
|
|
1236
|
-
const { name: entry, path: entryPath, dirConfig, dirMode, childOrderConfig } = folder
|
|
1253
|
+
const { name: entry, path: entryPath, dirConfig, dirMode, childOrderConfig, childLayoutName } = folder
|
|
1237
1254
|
const isIndex = entry === indexName
|
|
1255
|
+
const effectiveLayout = childLayoutName || parentLayoutName
|
|
1238
1256
|
|
|
1239
1257
|
if (dirMode === 'sections') {
|
|
1240
1258
|
// Subdirectory overrides to page mode — process normally
|
|
1241
1259
|
const result = await processPage(entryPath, entry, siteRoot, {
|
|
1242
|
-
isIndex, parentRoute, parentFetch, versionContext
|
|
1260
|
+
isIndex, parentRoute, parentFetch, versionContext,
|
|
1261
|
+
layoutName: effectiveLayout
|
|
1243
1262
|
})
|
|
1244
1263
|
|
|
1245
1264
|
if (result) {
|
|
@@ -1252,7 +1271,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1252
1271
|
const childDirPath = mounts?.get(entry) || entryPath
|
|
1253
1272
|
const childParentRoute = isIndex ? parentRoute : page.route
|
|
1254
1273
|
const childFetch = page.fetch || parentFetch
|
|
1255
|
-
const subResult = await collectPagesRecursive(childDirPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext, 'sections', null)
|
|
1274
|
+
const subResult = await collectPagesRecursive(childDirPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext, 'sections', null, effectiveLayout)
|
|
1256
1275
|
pages.push(...subResult.pages)
|
|
1257
1276
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
1258
1277
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -1266,6 +1285,9 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1266
1285
|
? parentRoute
|
|
1267
1286
|
: parentRoute === '/' ? `/${entry}` : `${parentRoute}/${entry}`
|
|
1268
1287
|
|
|
1288
|
+
// Resolve layout for container page
|
|
1289
|
+
const containerLayoutObj = typeof dirConfig.layout === 'object' && dirConfig.layout !== null ? dirConfig.layout : {}
|
|
1290
|
+
|
|
1269
1291
|
const containerPage = {
|
|
1270
1292
|
route: containerRoute,
|
|
1271
1293
|
sourcePath: isIndex ? (parentRoute === '/' ? `/${entry}` : `${parentRoute}/${entry}`) : null,
|
|
@@ -1285,10 +1307,9 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1285
1307
|
hideInHeader: dirConfig.hideInHeader || false,
|
|
1286
1308
|
hideInFooter: dirConfig.hideInFooter || false,
|
|
1287
1309
|
layout: {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
rightPanel: dirConfig.layout?.rightPanel !== false
|
|
1310
|
+
...(effectiveLayout ? { name: effectiveLayout } : {}),
|
|
1311
|
+
...(containerLayoutObj.hide ? { hide: containerLayoutObj.hide } : {}),
|
|
1312
|
+
...(containerLayoutObj.params ? { params: containerLayoutObj.params } : {}),
|
|
1292
1313
|
},
|
|
1293
1314
|
seo: {
|
|
1294
1315
|
noindex: dirConfig.seo?.noindex || false,
|
|
@@ -1305,7 +1326,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1305
1326
|
|
|
1306
1327
|
// Recurse in folder mode
|
|
1307
1328
|
const childDirPath = mounts?.get(entry) || entryPath
|
|
1308
|
-
const subResult = await collectPagesRecursive(childDirPath, containerRoute, siteRoot, childOrderConfig, parentFetch, versionContext, 'pages', null)
|
|
1329
|
+
const subResult = await collectPagesRecursive(childDirPath, containerRoute, siteRoot, childOrderConfig, parentFetch, versionContext, 'pages', null, effectiveLayout)
|
|
1309
1330
|
pages.push(...subResult.pages)
|
|
1310
1331
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
1311
1332
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -1340,8 +1361,9 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1340
1361
|
|
|
1341
1362
|
// Second pass: process each page folder
|
|
1342
1363
|
for (const folder of orderedFolders) {
|
|
1343
|
-
const { name: entry, path: entryPath, dirConfig, dirMode, childOrderConfig } = folder
|
|
1364
|
+
const { name: entry, path: entryPath, dirConfig, dirMode, childOrderConfig, childLayoutName } = folder
|
|
1344
1365
|
const isIndex = entry === indexPageName
|
|
1366
|
+
const effectiveLayout = childLayoutName || parentLayoutName
|
|
1345
1367
|
|
|
1346
1368
|
if (dirMode === 'pages') {
|
|
1347
1369
|
// Child directory switches to folder mode (has folder.yml) —
|
|
@@ -1350,6 +1372,9 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1350
1372
|
? parentRoute
|
|
1351
1373
|
: parentRoute === '/' ? `/${entry}` : `${parentRoute}/${entry}`
|
|
1352
1374
|
|
|
1375
|
+
// Resolve layout for container page
|
|
1376
|
+
const containerLayoutObj = typeof dirConfig.layout === 'object' && dirConfig.layout !== null ? dirConfig.layout : {}
|
|
1377
|
+
|
|
1353
1378
|
const containerPage = {
|
|
1354
1379
|
route: containerRoute,
|
|
1355
1380
|
sourcePath: isIndex ? (parentRoute === '/' ? `/${entry}` : `${parentRoute}/${entry}`) : null,
|
|
@@ -1369,10 +1394,9 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1369
1394
|
hideInHeader: dirConfig.hideInHeader || false,
|
|
1370
1395
|
hideInFooter: dirConfig.hideInFooter || false,
|
|
1371
1396
|
layout: {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
rightPanel: dirConfig.layout?.rightPanel !== false
|
|
1397
|
+
...(effectiveLayout ? { name: effectiveLayout } : {}),
|
|
1398
|
+
...(containerLayoutObj.hide ? { hide: containerLayoutObj.hide } : {}),
|
|
1399
|
+
...(containerLayoutObj.params ? { params: containerLayoutObj.params } : {}),
|
|
1376
1400
|
},
|
|
1377
1401
|
seo: {
|
|
1378
1402
|
noindex: dirConfig.seo?.noindex || false,
|
|
@@ -1392,7 +1416,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1392
1416
|
}
|
|
1393
1417
|
|
|
1394
1418
|
const childDirPath = mounts?.get(entry) || entryPath
|
|
1395
|
-
const subResult = await collectPagesRecursive(childDirPath, containerRoute, siteRoot, childOrderConfig, parentFetch, versionContext, 'pages', null)
|
|
1419
|
+
const subResult = await collectPagesRecursive(childDirPath, containerRoute, siteRoot, childOrderConfig, parentFetch, versionContext, 'pages', null, effectiveLayout)
|
|
1396
1420
|
pages.push(...subResult.pages)
|
|
1397
1421
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
1398
1422
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -1402,7 +1426,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1402
1426
|
} else {
|
|
1403
1427
|
// Sections mode — process directory as a page (existing behavior)
|
|
1404
1428
|
const result = await processPage(entryPath, entry, siteRoot, {
|
|
1405
|
-
isIndex, parentRoute, parentFetch, versionContext
|
|
1429
|
+
isIndex, parentRoute, parentFetch, versionContext,
|
|
1430
|
+
layoutName: effectiveLayout
|
|
1406
1431
|
})
|
|
1407
1432
|
|
|
1408
1433
|
if (result) {
|
|
@@ -1424,7 +1449,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1424
1449
|
? (hasExplicitOrder ? parentRoute : (page.sourcePath || page.route))
|
|
1425
1450
|
: page.route
|
|
1426
1451
|
const childFetch = page.fetch || parentFetch
|
|
1427
|
-
const subResult = await collectPagesRecursive(childDirPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext, dirMode, null)
|
|
1452
|
+
const subResult = await collectPagesRecursive(childDirPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext, dirMode, null, effectiveLayout)
|
|
1428
1453
|
pages.push(...subResult.pages)
|
|
1429
1454
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
1430
1455
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -1481,63 +1506,159 @@ async function loadFoundationVars(foundationPath) {
|
|
|
1481
1506
|
}
|
|
1482
1507
|
|
|
1483
1508
|
/**
|
|
1484
|
-
* Collect
|
|
1509
|
+
* Collect areas from a single directory (general named areas, not hardcoded).
|
|
1485
1510
|
*
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1489
|
-
* Supports two forms:
|
|
1490
|
-
* - Folder: layout/header/ (directory with .md files, like a page)
|
|
1491
|
-
* - File shorthand: layout/header.md (single markdown file)
|
|
1511
|
+
* Supports two forms per area:
|
|
1512
|
+
* - Folder: dir/header/ (directory with .md files, like a page)
|
|
1513
|
+
* - File shorthand: dir/header.md (single markdown file)
|
|
1492
1514
|
* Folder takes priority when both exist.
|
|
1493
1515
|
*
|
|
1494
|
-
* @param {string}
|
|
1516
|
+
* @param {string} dir - Directory to scan for area files
|
|
1495
1517
|
* @param {string} siteRoot - Path to site root
|
|
1496
|
-
* @
|
|
1518
|
+
* @param {string} routePrefix - Route prefix for area pages (e.g., '/layout' or '/layout/marketing')
|
|
1519
|
+
* @returns {Promise<Object>} Map of areaName -> page data
|
|
1497
1520
|
*/
|
|
1498
|
-
async function
|
|
1499
|
-
const result = {
|
|
1500
|
-
|
|
1501
|
-
if (!existsSync(
|
|
1502
|
-
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
+
async function collectAreasFromDir(dir, siteRoot, routePrefix = '/layout') {
|
|
1522
|
+
const result = {}
|
|
1523
|
+
|
|
1524
|
+
if (!existsSync(dir)) return result
|
|
1525
|
+
|
|
1526
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
1527
|
+
|
|
1528
|
+
// Track which area names we've already processed (folder form takes priority)
|
|
1529
|
+
const processed = new Set()
|
|
1530
|
+
|
|
1531
|
+
// First pass: directories (folder form, higher priority)
|
|
1532
|
+
for (const entry of entries) {
|
|
1533
|
+
if (!entry.isDirectory()) continue
|
|
1534
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue
|
|
1535
|
+
|
|
1536
|
+
const areaName = entry.name
|
|
1537
|
+
const entryPath = join(dir, areaName)
|
|
1538
|
+
const pageResult = await processPage(entryPath, areaName, siteRoot, {
|
|
1539
|
+
isIndex: false,
|
|
1540
|
+
parentRoute: routePrefix
|
|
1541
|
+
})
|
|
1542
|
+
if (pageResult) {
|
|
1543
|
+
result[areaName] = pageResult.page
|
|
1544
|
+
processed.add(areaName)
|
|
1521
1545
|
}
|
|
1546
|
+
}
|
|
1522
1547
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
if (
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1548
|
+
// Second pass: markdown file shorthand (only if not already processed as folder)
|
|
1549
|
+
for (const entry of entries) {
|
|
1550
|
+
if (!entry.isFile()) continue
|
|
1551
|
+
if (!entry.name.endsWith('.md')) continue
|
|
1552
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue
|
|
1553
|
+
|
|
1554
|
+
const areaName = entry.name.replace('.md', '')
|
|
1555
|
+
if (processed.has(areaName)) continue
|
|
1556
|
+
|
|
1557
|
+
const filePath = join(dir, entry.name)
|
|
1558
|
+
const { section } = await processMarkdownFile(filePath, '1', siteRoot, areaName)
|
|
1559
|
+
result[areaName] = {
|
|
1560
|
+
route: `${routePrefix}/${areaName}`,
|
|
1561
|
+
title: areaName.charAt(0).toUpperCase() + areaName.slice(1),
|
|
1562
|
+
description: '',
|
|
1563
|
+
layout: {},
|
|
1564
|
+
sections: [section]
|
|
1535
1565
|
}
|
|
1536
1566
|
}
|
|
1537
1567
|
|
|
1538
1568
|
return result
|
|
1539
1569
|
}
|
|
1540
1570
|
|
|
1571
|
+
/**
|
|
1572
|
+
* Check if a directory looks like a named layout (contains area-like .md files or area subdirs)
|
|
1573
|
+
* vs an area in folder form (contains section content processed by processPage).
|
|
1574
|
+
*
|
|
1575
|
+
* Heuristic: if a directory contains .md files at the top level AND no page.yml,
|
|
1576
|
+
* it's a named layout (those .md files are its area definitions).
|
|
1577
|
+
* If it has page.yml or looks like a page directory, it's an area folder.
|
|
1578
|
+
*
|
|
1579
|
+
* @param {string} dirPath - Path to the directory
|
|
1580
|
+
* @returns {Promise<boolean>} True if this looks like a named layout directory
|
|
1581
|
+
*/
|
|
1582
|
+
async function isNamedLayoutDir(dirPath) {
|
|
1583
|
+
// If it has page.yml, it's an area folder (processPage will handle it)
|
|
1584
|
+
if (existsSync(join(dirPath, 'page.yml'))) return false
|
|
1585
|
+
|
|
1586
|
+
const entries = await readdir(dirPath)
|
|
1587
|
+
// If directory contains .md files but no page.yml, it's a named layout
|
|
1588
|
+
return entries.some(e => e.endsWith('.md') && !e.startsWith('_') && !e.startsWith('.'))
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* Collect layout areas from the layout/ directory, including named layout subdirectories.
|
|
1593
|
+
*
|
|
1594
|
+
* Root-level .md files and area directories form the "default" layout's areas.
|
|
1595
|
+
* Subdirectories that themselves contain .md files (without page.yml) are named layouts,
|
|
1596
|
+
* each with its own set of areas.
|
|
1597
|
+
*
|
|
1598
|
+
* @param {string} layoutDir - Path to layout directory
|
|
1599
|
+
* @param {string} siteRoot - Path to site root
|
|
1600
|
+
* @returns {Promise<Object>} { layouts }
|
|
1601
|
+
*/
|
|
1602
|
+
async function collectLayouts(layoutDir, siteRoot) {
|
|
1603
|
+
if (!existsSync(layoutDir)) {
|
|
1604
|
+
return { layouts: null }
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const entries = await readdir(layoutDir, { withFileTypes: true })
|
|
1608
|
+
|
|
1609
|
+
// Separate root-level entries into:
|
|
1610
|
+
// 1. Area .md files (for default layout)
|
|
1611
|
+
// 2. Area directories (for default layout) vs named layout directories
|
|
1612
|
+
const defaultAreaFiles = []
|
|
1613
|
+
const defaultAreaDirs = []
|
|
1614
|
+
const namedLayoutDirs = []
|
|
1615
|
+
|
|
1616
|
+
for (const entry of entries) {
|
|
1617
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue
|
|
1618
|
+
|
|
1619
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
1620
|
+
defaultAreaFiles.push(entry)
|
|
1621
|
+
} else if (entry.isDirectory()) {
|
|
1622
|
+
const dirPath = join(layoutDir, entry.name)
|
|
1623
|
+
if (await isNamedLayoutDir(dirPath)) {
|
|
1624
|
+
namedLayoutDirs.push(entry)
|
|
1625
|
+
} else {
|
|
1626
|
+
defaultAreaDirs.push(entry)
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Collect default layout areas
|
|
1632
|
+
const defaultAreas = await collectAreasFromDir(layoutDir, siteRoot, '/layout')
|
|
1633
|
+
// Remove any named layout directories that got collected as areas
|
|
1634
|
+
for (const dir of namedLayoutDirs) {
|
|
1635
|
+
delete defaultAreas[dir.name]
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// Collect named layout areas
|
|
1639
|
+
const namedLayouts = {}
|
|
1640
|
+
for (const entry of namedLayoutDirs) {
|
|
1641
|
+
const subdir = join(layoutDir, entry.name)
|
|
1642
|
+
namedLayouts[entry.name] = await collectAreasFromDir(subdir, siteRoot, `/layout/${entry.name}`)
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const hasDefaultAreas = Object.keys(defaultAreas).length > 0
|
|
1646
|
+
const hasNamedLayouts = Object.keys(namedLayouts).length > 0
|
|
1647
|
+
|
|
1648
|
+
if (!hasDefaultAreas && !hasNamedLayouts) {
|
|
1649
|
+
return { layouts: null }
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Always use the layouts object format (general areas)
|
|
1653
|
+
const layouts = {}
|
|
1654
|
+
if (hasDefaultAreas) {
|
|
1655
|
+
layouts.default = defaultAreas
|
|
1656
|
+
}
|
|
1657
|
+
Object.assign(layouts, namedLayouts)
|
|
1658
|
+
|
|
1659
|
+
return { layouts }
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1541
1662
|
/**
|
|
1542
1663
|
* Collect all site content
|
|
1543
1664
|
*
|
|
@@ -1595,12 +1716,16 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1595
1716
|
// Determine root content mode from folder.yml/page.yml presence in pages directory
|
|
1596
1717
|
const { mode: rootContentMode } = await readFolderConfig(pagesPath, 'sections')
|
|
1597
1718
|
|
|
1598
|
-
// Collect layout
|
|
1599
|
-
const {
|
|
1719
|
+
// Collect layout areas from layout/ directory (including named layout subdirectories)
|
|
1720
|
+
const { layouts } = await collectLayouts(layoutPath, sitePath)
|
|
1721
|
+
|
|
1722
|
+
// Site-level layout name (from site.yml layout: field)
|
|
1723
|
+
const siteLayoutName = typeof siteConfig.layout === 'string' ? siteConfig.layout
|
|
1724
|
+
: siteConfig.layout?.name || null
|
|
1600
1725
|
|
|
1601
1726
|
// Recursively collect all pages
|
|
1602
1727
|
const { pages, assetCollection, iconCollection, notFound, versionedScopes } =
|
|
1603
|
-
await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig, null, null, rootContentMode, mounts)
|
|
1728
|
+
await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig, null, null, rootContentMode, mounts, siteLayoutName)
|
|
1604
1729
|
|
|
1605
1730
|
// Deduplicate: remove content-less container pages whose route duplicates
|
|
1606
1731
|
// a content-bearing page (e.g., a promoted index page)
|
|
@@ -1679,10 +1804,8 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1679
1804
|
css: themeCSS
|
|
1680
1805
|
},
|
|
1681
1806
|
pages,
|
|
1682
|
-
header,
|
|
1683
|
-
|
|
1684
|
-
left,
|
|
1685
|
-
right,
|
|
1807
|
+
// Layout area sets: { default: { header: page, footer: page, ... }, marketing: { ... } }
|
|
1808
|
+
layouts,
|
|
1686
1809
|
notFound,
|
|
1687
1810
|
// Versioned scopes: route → { versions, latestId }
|
|
1688
1811
|
versionedScopes: versionedScopesObj,
|