@uniweb/build 0.47.2 → 0.48.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 +6 -6
- package/src/prerender.js +124 -47
- package/src/site/content-collector.js +121 -190
- package/src/site/data-fetcher.js +294 -188
- package/src/site/fetch-shapes.js +9 -8
- package/src/site/layout-folder.js +221 -0
- package/src/site/plugin.js +23 -19
- package/src/site/queries-config.js +80 -5
- package/src/site/query-processor.js +23 -25
- package/src/utils/content-files.js +38 -0
- package/src/uwx/records-project.js +7 -2
- package/src/uwx/site-diff.js +7 -2
- package/src/uwx/site-project.js +73 -41
- package/src/uwx/site.js +54 -40
- package/src/validate-data.js +7 -4
|
@@ -24,16 +24,18 @@
|
|
|
24
24
|
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
25
25
|
import { resolveQueriesConfig, toConfigQueries } from './queries-config.js'
|
|
26
26
|
import { parseNumericPrefix, compareByNumericPrefix } from '../utils/numeric-prefix.js'
|
|
27
|
+
import { isMarkdownFile, isIgnoredFolder } from '../utils/content-files.js'
|
|
28
|
+
import { readLayoutFolder, DEFAULT_LAYOUT } from './layout-folder.js'
|
|
27
29
|
import { join, parse, relative, resolve, sep } from 'node:path'
|
|
28
30
|
import { existsSync, statSync, realpathSync, readdirSync } from 'node:fs'
|
|
29
31
|
import yaml from 'js-yaml'
|
|
30
32
|
import { collectSectionAssets, mergeAssetCollections, collectConfigAssets } from './assets.js'
|
|
31
33
|
import { collectSectionIcons, mergeIconCollections, buildIconManifest } from './icons.js'
|
|
32
34
|
import { normalizeHideIn, dropUnpublishedPages } from './nav-visibility.js'
|
|
33
|
-
import { parseFetchConfig } from './data-fetcher.js'
|
|
35
|
+
import { parseFetchConfig, toFetchList } from './data-fetcher.js'
|
|
34
36
|
import { resolveExtensionUrls } from './extension-urls.js'
|
|
35
37
|
import { buildTheme, extractFoundationVars } from '../theme/index.js'
|
|
36
|
-
import { resolveDefaultLocale, resolvePublishableLocales, validateLanguageConfig } from '@uniweb/core'
|
|
38
|
+
import { resolveDefaultLocale, resolvePublishableLocales, validateLanguageConfig, pageRouteQuery } from '@uniweb/core'
|
|
37
39
|
import { parseFrontmatter } from '../utils/frontmatter.js'
|
|
38
40
|
|
|
39
41
|
// Try to import content-reader, fall back to simplified parser
|
|
@@ -245,6 +247,9 @@ export function declaredFetch(config, where) {
|
|
|
245
247
|
return config?.fetch ?? fetchFromQueryShorthand(config?.query)
|
|
246
248
|
}
|
|
247
249
|
|
|
250
|
+
/** `parseFetchConfig`'s arguments for a level: its declaration, the file it sits in, and the level. */
|
|
251
|
+
const fetchIn = (config, where, level) => [declaredFetch(config, where), where, { level }]
|
|
252
|
+
|
|
248
253
|
/**
|
|
249
254
|
* The refusals `declaredFetch` makes, alone — for a reader that must keep the
|
|
250
255
|
* desugaring inline (the sync push's `settings.fetch`, whose sources
|
|
@@ -300,6 +305,51 @@ const isQueryNames = (value) =>
|
|
|
300
305
|
const formatQueryNames = (value) =>
|
|
301
306
|
isQueryNames(value) ? (Array.isArray(value) ? `[${value.join(', ')}]` : value) : '<name>'
|
|
302
307
|
|
|
308
|
+
/**
|
|
309
|
+
* ⚠️ A `current:` NOTHING WILL READ — said once per binding. `current:` applies on a
|
|
310
|
+
* section's binding under the key its page's URL narrows (the route query,
|
|
311
|
+
* `pageRouteQuery`); anywhere else the runtime ignores it, and the author would see
|
|
312
|
+
* the section deliver its list as though the line were not there.
|
|
313
|
+
*
|
|
314
|
+
* @param {Array<Object>} pages - collected pages, parents linked
|
|
315
|
+
* @param {Object} layouts - collected layout areas (never on a parametric route)
|
|
316
|
+
* @param {Object|Array|null} siteFetch - the parsed site fetch
|
|
317
|
+
*/
|
|
318
|
+
function warnUnreadCurrent(pages, layouts, siteFetch) {
|
|
319
|
+
const byRoute = new Map(pages.map((p) => [p.route, p]))
|
|
320
|
+
const access = {
|
|
321
|
+
routeOf: (p) => p.route,
|
|
322
|
+
parentOf: (p) => (p.parent ? byRoute.get(p.parent) ?? null : null),
|
|
323
|
+
fetchOf: (p) => p.fetch,
|
|
324
|
+
sectionsOf: (p) => p.sections,
|
|
325
|
+
site: siteFetch,
|
|
326
|
+
}
|
|
327
|
+
const check = (sections, where, routeKeyOf) => {
|
|
328
|
+
for (const section of sections || []) {
|
|
329
|
+
for (const one of toFetchList(section.fetch)) {
|
|
330
|
+
if (one?.current === undefined) continue
|
|
331
|
+
const routeKey = routeKeyOf()
|
|
332
|
+
if (one.as === routeKey) continue
|
|
333
|
+
console.warn(
|
|
334
|
+
`[uniweb] ${where}: \`current: ${one.current}\` on content.data.${one.as} is ignored — it applies on a ` +
|
|
335
|
+
`section of a parametric page, under the key its URL narrows` +
|
|
336
|
+
(routeKey ? ` (here \`${routeKey}\`).` : '; this page has none.')
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
check(section.subsections, where, routeKeyOf)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
for (const page of pages) {
|
|
343
|
+
let key
|
|
344
|
+
check(page.sections, page.route, () => (key === undefined ? (key = pageRouteQuery(page, access)?.key ?? null) : key))
|
|
345
|
+
}
|
|
346
|
+
for (const [name, areas] of Object.entries(layouts || {})) {
|
|
347
|
+
for (const [area, areaPage] of Object.entries(areas || {})) {
|
|
348
|
+
check(areaPage?.sections, `layout ${name}/${area}`, () => null)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
303
353
|
/**
|
|
304
354
|
* Build version metadata from detected versions and page.yml config
|
|
305
355
|
* @param {Array<Object>} detectedVersions - Detected version infos
|
|
@@ -442,20 +492,6 @@ function extractInsets(doc) {
|
|
|
442
492
|
return insets
|
|
443
493
|
}
|
|
444
494
|
|
|
445
|
-
/**
|
|
446
|
-
* Check if a file is a markdown file that should be processed.
|
|
447
|
-
* Excludes:
|
|
448
|
-
* - Files not ending in .md
|
|
449
|
-
* - Files starting with _ (drafts/private)
|
|
450
|
-
* - README.md (repo documentation, not site content)
|
|
451
|
-
*/
|
|
452
|
-
function isMarkdownFile(filename) {
|
|
453
|
-
if (!filename.endsWith('.md')) return false
|
|
454
|
-
if (filename.startsWith('_')) return false
|
|
455
|
-
if (filename.toLowerCase() === 'readme.md') return false
|
|
456
|
-
return true
|
|
457
|
-
}
|
|
458
|
-
|
|
459
495
|
/**
|
|
460
496
|
* Check if a filename uses the @ prefix (child section convention).
|
|
461
497
|
* @-prefixed files are excluded from auto-discovered top-level sections —
|
|
@@ -473,22 +509,6 @@ function stripAtPrefix(filename) {
|
|
|
473
509
|
return filename.replace(/^@+/, '')
|
|
474
510
|
}
|
|
475
511
|
|
|
476
|
-
/**
|
|
477
|
-
* Check if a folder should be ignored.
|
|
478
|
-
*
|
|
479
|
-
* Excludes folders starting with `_` (drafts/private) and with `.` (hidden).
|
|
480
|
-
*
|
|
481
|
-
* Hidden folders matter more than they look. A site's own `pages/` never holds
|
|
482
|
-
* one, but a mount target routinely does: point `paths:` at a directory that is
|
|
483
|
-
* also a git working tree — a sibling clone, which the docs suggest — and `.git`
|
|
484
|
-
* is a directory sitting right next to the content. Walked as content it
|
|
485
|
-
* contributed hundreds of routes. (A submodule hides this: there, `.git` is a
|
|
486
|
-
* file, so only the plain-clone case ever showed it.)
|
|
487
|
-
*/
|
|
488
|
-
function isIgnoredFolder(name) {
|
|
489
|
-
return name.startsWith('_') || name.startsWith('.')
|
|
490
|
-
}
|
|
491
|
-
|
|
492
512
|
/**
|
|
493
513
|
* Workspace-root content profiles, dispatched on the top-level config filename.
|
|
494
514
|
*
|
|
@@ -1054,7 +1074,7 @@ async function processMarkdownFile(filePath, id, siteRoot, defaultStableId = nul
|
|
|
1054
1074
|
input,
|
|
1055
1075
|
params: { ...params, ...props },
|
|
1056
1076
|
content: proseMirrorContent,
|
|
1057
|
-
fetch: parseFetchConfig(resolvedFetch),
|
|
1077
|
+
fetch: parseFetchConfig(resolvedFetch, relative(siteRoot, filePath), { level: 'section' }),
|
|
1058
1078
|
...(insets.length > 0 ? { insets } : {}),
|
|
1059
1079
|
subsections: []
|
|
1060
1080
|
}
|
|
@@ -1666,7 +1686,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
1666
1686
|
},
|
|
1667
1687
|
|
|
1668
1688
|
// Data fetching — `fetch:`, or the `query:` shorthand (`declaredFetch`)
|
|
1669
|
-
fetch: parseFetchConfig(
|
|
1689
|
+
fetch: parseFetchConfig(...fetchIn(pageConfig, relative(siteRoot, join(pagePath, 'page.yml')), 'page')),
|
|
1670
1690
|
|
|
1671
1691
|
hasContent: hierarchicalSections.length > 0,
|
|
1672
1692
|
sections: hierarchicalSections
|
|
@@ -2038,7 +2058,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
2038
2058
|
// ⭐ `declaredFetch`, as every other level: this read `dirConfig.fetch`
|
|
2039
2059
|
// alone until 2026-09-11, so a container's `folder.yml` shorthand reached
|
|
2040
2060
|
// a backend on `push` and was dropped from a static build.
|
|
2041
|
-
fetch: parseFetchConfig(
|
|
2061
|
+
fetch: parseFetchConfig(...fetchIn(dirConfig, relative(siteRoot, join(entryPath, 'folder.yml')), 'page')) || null,
|
|
2042
2062
|
hasContent: false,
|
|
2043
2063
|
sections: [],
|
|
2044
2064
|
order: typeof dirConfig.order === 'number' ? dirConfig.order : undefined
|
|
@@ -2135,7 +2155,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
2135
2155
|
// a `folder.yml` here lost its `fetch:` (and its shorthand) on a static
|
|
2136
2156
|
// build while `push` carried it, so its pages had the folder's data on a
|
|
2137
2157
|
// hosted site and none on an exported one.
|
|
2138
|
-
fetch: parseFetchConfig(
|
|
2158
|
+
fetch: parseFetchConfig(...fetchIn(dirConfig, relative(siteRoot, join(entryPath, 'folder.yml')), 'page')) || null,
|
|
2139
2159
|
hasContent: false,
|
|
2140
2160
|
sections: [],
|
|
2141
2161
|
order: typeof dirConfig.order === 'number' ? dirConfig.order : undefined
|
|
@@ -2222,12 +2242,6 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
2222
2242
|
return { pages, assetCollection, iconCollection, notFound, versionedScopes }
|
|
2223
2243
|
}
|
|
2224
2244
|
|
|
2225
|
-
/**
|
|
2226
|
-
* Load foundation variables from schema.json
|
|
2227
|
-
*
|
|
2228
|
-
* @param {string} foundationPath - Path to foundation directory
|
|
2229
|
-
* @returns {Promise<Object>} Foundation variables or empty object
|
|
2230
|
-
*/
|
|
2231
2245
|
/**
|
|
2232
2246
|
* Load foundation schema data needed by the content collector.
|
|
2233
2247
|
*
|
|
@@ -2239,11 +2253,16 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
2239
2253
|
* the build never calls it), so it answers "something could resolve this",
|
|
2240
2254
|
* never "Loom will".
|
|
2241
2255
|
*
|
|
2256
|
+
* ⛔ **No layout names.** This returned the foundation's layouts until 2026-09-13,
|
|
2257
|
+
* for `collectLayouts` to decide whether a folder under `layout/` was a named
|
|
2258
|
+
* layout. A site's meaning does not depend on a foundation the build may not have
|
|
2259
|
+
* — see `./layout-folder.js`, which reads that folder from the site alone.
|
|
2260
|
+
*
|
|
2242
2261
|
* @param {string} foundationPath - Path to foundation directory
|
|
2243
|
-
* @returns {Promise<{ vars: Object,
|
|
2262
|
+
* @returns {Promise<{ vars: Object, hasContentHandler: boolean }>}
|
|
2244
2263
|
*/
|
|
2245
2264
|
export async function loadFoundationInfo(foundationPath) {
|
|
2246
|
-
if (!foundationPath) return { vars: {},
|
|
2265
|
+
if (!foundationPath) return { vars: {}, hasContentHandler: false }
|
|
2247
2266
|
|
|
2248
2267
|
// ⛔ **NOT `dist/meta/schema.json`.** That file is the EDITOR's artifact — the
|
|
2249
2268
|
// rich per-section declaration a visual editor needs to render parameter forms
|
|
@@ -2256,26 +2275,11 @@ export async function loadFoundationInfo(foundationPath) {
|
|
|
2256
2275
|
// Reading the editor's copy here was wrong twice over. It made a site build
|
|
2257
2276
|
// depend on the foundation having been BUILT, and it read a derived artifact
|
|
2258
2277
|
// when the source it derives from is right there — `generate-entry.js` calls
|
|
2259
|
-
//
|
|
2260
|
-
// very `schema.json` this used to read back.
|
|
2261
|
-
//
|
|
2262
|
-
// ⭐ **And it made dev and build classify layouts differently.** The old
|
|
2263
|
-
// fallback returned an EMPTY layout set whenever no built schema existed —
|
|
2264
|
-
// "the normal state for `uniweb dev`" — and `collectLayouts` treats a
|
|
2265
|
-
// `layout/<Name>/` directory as a named layout only when `<Name>` is in that
|
|
2266
|
-
// set, otherwise as a folder-form area of the DEFAULT layout. So the same site
|
|
2267
|
-
// rendered `layout/Wide/` as the Wide layout after a build and as a default
|
|
2268
|
-
// area called "Wide" in dev. Measured 2026-09-02 before this change:
|
|
2269
|
-
// built layouts ["default","Wide"], default areas ["footer","header"]
|
|
2270
|
-
// dev layouts ["default"], default areas ["Wide","footer","header"]
|
|
2271
|
-
// Reading from source removes the asymmetry: there is one answer, and it does
|
|
2272
|
-
// not depend on whether `dist/` happens to exist.
|
|
2278
|
+
// the same function to produce the very `schema.json` this used to read back.
|
|
2273
2279
|
const { resolveFoundationSrcPath } = await import('../utils/foundation-source-root.js')
|
|
2274
|
-
const { loadFoundationConfig
|
|
2280
|
+
const { loadFoundationConfig } = await import('../schema.js')
|
|
2275
2281
|
const srcDir = resolveFoundationSrcPath(foundationPath)
|
|
2276
2282
|
|
|
2277
|
-
// Two independent reads, so a failure in one does not cost the other. The
|
|
2278
|
-
// previous single try/catch lost the layouts when only the config was broken.
|
|
2279
2283
|
let vars = {}
|
|
2280
2284
|
let hasContentHandler = false
|
|
2281
2285
|
try {
|
|
@@ -2292,144 +2296,64 @@ export async function loadFoundationInfo(foundationPath) {
|
|
|
2292
2296
|
)
|
|
2293
2297
|
}
|
|
2294
2298
|
|
|
2295
|
-
|
|
2296
|
-
try {
|
|
2297
|
-
layoutNames = new Set(Object.keys(await discoverLayoutsInPath(srcDir)))
|
|
2298
|
-
} catch (err) {
|
|
2299
|
-
console.warn(
|
|
2300
|
-
`[content-collector] Could not discover the foundation's layouts in ${srcDir}: ${err.message}\n` +
|
|
2301
|
-
`[content-collector] Continuing WITHOUT them — a site \`layout/<Name>/\` directory will be collected as an area of the default layout rather than as that named layout.`
|
|
2302
|
-
)
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
return { vars, layoutNames, hasContentHandler }
|
|
2299
|
+
return { vars, hasContentHandler }
|
|
2306
2300
|
}
|
|
2307
2301
|
|
|
2308
2302
|
/**
|
|
2309
|
-
* Collect areas
|
|
2303
|
+
* Collect the site's layout areas: `{ <layout>: { <area>: { route, title, sections } } }`.
|
|
2304
|
+
*
|
|
2305
|
+
* ⭐ Read through `readLayoutFolder` (`./layout-folder.js`) — the ONE reader the
|
|
2306
|
+
* sync producer uses too. Depth decides what a folder under `layout/` is, never the
|
|
2307
|
+
* foundation. ⛔ Until 2026-09-13 a folder was a named layout here only when the
|
|
2308
|
+
* foundation declared a layout by that name, and an area of the default layout
|
|
2309
|
+
* otherwise, while sync called every folder a named layout: `layout/header/` was a
|
|
2310
|
+
* default header on a static build and a layout named `header` once synced
|
|
2311
|
+
* (measured).
|
|
2310
2312
|
*
|
|
2311
|
-
*
|
|
2312
|
-
*
|
|
2313
|
-
*
|
|
2314
|
-
*
|
|
2313
|
+
* An area is a container of sections, not a page: no `page.yml` is read, so an area
|
|
2314
|
+
* carries no `fetch`, `seo` or section ordering of its own. ⭐ Its sections' assets
|
|
2315
|
+
* and icons join the site's manifests — ⛔ they were dropped until 2026-09-13, so a
|
|
2316
|
+
* logo referenced from a header section never entered the asset manifest (measured).
|
|
2315
2317
|
*
|
|
2316
|
-
* @param {string}
|
|
2318
|
+
* @param {string} layoutDir - Path to layout directory
|
|
2317
2319
|
* @param {string} siteRoot - Path to site root
|
|
2318
|
-
* @
|
|
2319
|
-
* @returns {Promise<Object>} Map of areaName -> page data
|
|
2320
|
+
* @returns {Promise<{ layouts: Object|null, assetCollection: Object, iconCollection: Object }>}
|
|
2320
2321
|
*/
|
|
2321
|
-
async function
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
if (!existsSync(dir)) return result
|
|
2322
|
+
async function collectLayouts(layoutDir, siteRoot) {
|
|
2323
|
+
let assetCollection = { assets: {}, hasExplicitPoster: new Set(), hasExplicitPreview: new Set() }
|
|
2324
|
+
let iconCollection = { icons: new Set(), bySource: new Map() }
|
|
2325
2325
|
|
|
2326
|
-
const
|
|
2326
|
+
const areas = await readLayoutFolder(layoutDir, { siteRoot })
|
|
2327
|
+
if (areas.length === 0) return { layouts: null, assetCollection, iconCollection }
|
|
2327
2328
|
|
|
2328
|
-
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
result[areaName] = pageResult.page
|
|
2344
|
-
processed.add(areaName)
|
|
2329
|
+
const layouts = {}
|
|
2330
|
+
for (const area of areas) {
|
|
2331
|
+
const sections = []
|
|
2332
|
+
for (const file of area.files) {
|
|
2333
|
+
const { name } = parse(file)
|
|
2334
|
+
const { prefix, name: stableName } = parseNumericPrefix(name)
|
|
2335
|
+
// A one-section area keeps the handles it has always had: positional id `1`,
|
|
2336
|
+
// and the area's name as its stable id — what sync sends as its `$id`. A
|
|
2337
|
+
// section of a multi-section area is named the way a page's section is.
|
|
2338
|
+
const id = area.form === 'file' ? '1' : (prefix || name)
|
|
2339
|
+
const stableId = area.form === 'file' ? area.area : (stableName || name)
|
|
2340
|
+
const result = await processMarkdownFile(join(area.dir, file), id, siteRoot, stableId)
|
|
2341
|
+
sections.push(result.section)
|
|
2342
|
+
assetCollection = mergeAssetCollections(assetCollection, result.assetCollection)
|
|
2343
|
+
iconCollection = mergeIconCollections(iconCollection, result.iconCollection)
|
|
2345
2344
|
}
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
if (!entry.name.endsWith('.md')) continue
|
|
2352
|
-
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue
|
|
2353
|
-
|
|
2354
|
-
const areaName = entry.name.replace('.md', '')
|
|
2355
|
-
if (processed.has(areaName)) continue
|
|
2356
|
-
|
|
2357
|
-
const filePath = join(dir, entry.name)
|
|
2358
|
-
const { section } = await processMarkdownFile(filePath, '1', siteRoot, areaName)
|
|
2359
|
-
result[areaName] = {
|
|
2360
|
-
route: `${routePrefix}/${areaName}`,
|
|
2361
|
-
title: areaName.charAt(0).toUpperCase() + areaName.slice(1),
|
|
2345
|
+
const route = area.layout === DEFAULT_LAYOUT ? `/layout/${area.area}` : `/layout/${area.layout}/${area.area}`
|
|
2346
|
+
layouts[area.layout] ??= {}
|
|
2347
|
+
layouts[area.layout][area.area] = {
|
|
2348
|
+
route,
|
|
2349
|
+
title: area.area.charAt(0).toUpperCase() + area.area.slice(1),
|
|
2362
2350
|
description: '',
|
|
2363
2351
|
layout: {},
|
|
2364
|
-
sections
|
|
2352
|
+
sections,
|
|
2365
2353
|
}
|
|
2366
2354
|
}
|
|
2367
2355
|
|
|
2368
|
-
return
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
/**
|
|
2372
|
-
* Collect layout areas from the layout/ directory, including named layout subdirectories.
|
|
2373
|
-
*
|
|
2374
|
-
* Root-level .md files and area directories form the "default" layout's areas.
|
|
2375
|
-
* Subdirectories that match a foundation-declared layout name are named layouts,
|
|
2376
|
-
* each with its own set of areas. All other subdirectories are folder-form areas
|
|
2377
|
-
* for the default layout (multi-section areas).
|
|
2378
|
-
*
|
|
2379
|
-
* @param {string} layoutDir - Path to layout directory
|
|
2380
|
-
* @param {string} siteRoot - Path to site root
|
|
2381
|
-
* @param {Set<string>} layoutNames - Layout names declared in the foundation schema
|
|
2382
|
-
* @returns {Promise<Object>} { layouts }
|
|
2383
|
-
*/
|
|
2384
|
-
async function collectLayouts(layoutDir, siteRoot, layoutNames = new Set()) {
|
|
2385
|
-
if (!existsSync(layoutDir)) {
|
|
2386
|
-
return { layouts: null }
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
const entries = await readdir(layoutDir, { withFileTypes: true })
|
|
2390
|
-
|
|
2391
|
-
// Identify named layout directories: only subdirectories whose name matches
|
|
2392
|
-
// a layout component declared in the foundation. All others are folder-form
|
|
2393
|
-
// areas for the default layout (e.g., layout/header/ with multiple sections).
|
|
2394
|
-
const namedLayoutDirs = []
|
|
2395
|
-
|
|
2396
|
-
for (const entry of entries) {
|
|
2397
|
-
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue
|
|
2398
|
-
|
|
2399
|
-
if (entry.isDirectory() && layoutNames.has(entry.name)) {
|
|
2400
|
-
namedLayoutDirs.push(entry)
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
// Collect default layout areas
|
|
2405
|
-
const defaultAreas = await collectAreasFromDir(layoutDir, siteRoot, '/layout')
|
|
2406
|
-
// Remove any named layout directories that got collected as areas
|
|
2407
|
-
for (const dir of namedLayoutDirs) {
|
|
2408
|
-
delete defaultAreas[dir.name]
|
|
2409
|
-
}
|
|
2410
|
-
|
|
2411
|
-
// Collect named layout areas
|
|
2412
|
-
const namedLayouts = {}
|
|
2413
|
-
for (const entry of namedLayoutDirs) {
|
|
2414
|
-
const subdir = join(layoutDir, entry.name)
|
|
2415
|
-
namedLayouts[entry.name] = await collectAreasFromDir(subdir, siteRoot, `/layout/${entry.name}`)
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
const hasDefaultAreas = Object.keys(defaultAreas).length > 0
|
|
2419
|
-
const hasNamedLayouts = Object.keys(namedLayouts).length > 0
|
|
2420
|
-
|
|
2421
|
-
if (!hasDefaultAreas && !hasNamedLayouts) {
|
|
2422
|
-
return { layouts: null }
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
// Always use the layouts object format (general areas)
|
|
2426
|
-
const layouts = {}
|
|
2427
|
-
if (hasDefaultAreas) {
|
|
2428
|
-
layouts.default = defaultAreas
|
|
2429
|
-
}
|
|
2430
|
-
Object.assign(layouts, namedLayouts)
|
|
2431
|
-
|
|
2432
|
-
return { layouts }
|
|
2356
|
+
return { layouts, assetCollection, iconCollection }
|
|
2433
2357
|
}
|
|
2434
2358
|
|
|
2435
2359
|
/**
|
|
@@ -2519,9 +2443,8 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2519
2443
|
: join(sitePath, 'layout')
|
|
2520
2444
|
const rawThemeConfig = await readYamlFile(join(sitePath, 'theme.yml'))
|
|
2521
2445
|
|
|
2522
|
-
// Load foundation info (vars
|
|
2523
|
-
const { vars: foundationVars,
|
|
2524
|
-
await loadFoundationInfo(foundationPath)
|
|
2446
|
+
// Load foundation info (vars) and process theme
|
|
2447
|
+
const { vars: foundationVars, hasContentHandler } = await loadFoundationInfo(foundationPath)
|
|
2525
2448
|
|
|
2526
2449
|
// ⭐ `placeholders:` IS DECLARED FOR A READER THAT MAY NOT EXIST, and that is
|
|
2527
2450
|
// the one way this feature fails. Resolving `{…}` in page content is a
|
|
@@ -2581,8 +2504,9 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2581
2504
|
// overrides the profile default per readFolderConfig.
|
|
2582
2505
|
const { mode: rootContentMode } = await readFolderConfig(pagesPath, profile.defaultMode)
|
|
2583
2506
|
|
|
2584
|
-
// Collect layout areas from layout/ directory
|
|
2585
|
-
const { layouts
|
|
2507
|
+
// Collect layout areas from the layout/ directory, read from the site alone
|
|
2508
|
+
const { layouts, assetCollection: layoutAssets, iconCollection: layoutIcons } =
|
|
2509
|
+
await collectLayouts(layoutPath, sitePath)
|
|
2586
2510
|
|
|
2587
2511
|
// Site-level layout (from `site.yml::layout`) — the ROOT of the cascade, and
|
|
2588
2512
|
// the whole object, not just its name.
|
|
@@ -2592,6 +2516,10 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2592
2516
|
let { pages, assetCollection, iconCollection, notFound, versionedScopes } =
|
|
2593
2517
|
await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig, null, null, rootContentMode, mounts, siteLayout)
|
|
2594
2518
|
|
|
2519
|
+
// The layout sections' assets and icons, beside the pages'.
|
|
2520
|
+
assetCollection = mergeAssetCollections(assetCollection, layoutAssets)
|
|
2521
|
+
iconCollection = mergeIconCollections(iconCollection, layoutIcons)
|
|
2522
|
+
|
|
2595
2523
|
// Merge top-level config assets (e.g. document.yml's book.covers.front,
|
|
2596
2524
|
// banner images, logos) into the manifest. The compile pipeline reads
|
|
2597
2525
|
// website.assets[<original src>] to resolve these to filesystem paths
|
|
@@ -2650,6 +2578,9 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2650
2578
|
page.parent = parentPage ? parentPage.route : null
|
|
2651
2579
|
}
|
|
2652
2580
|
|
|
2581
|
+
const siteFetch = parseFetchConfig(...fetchIn(siteConfig, 'site.yml', 'site'))
|
|
2582
|
+
warnUnreadCurrent(pages, layouts, siteFetch)
|
|
2583
|
+
|
|
2653
2584
|
// Page order is determined by per-level sorting during collection:
|
|
2654
2585
|
// 1. Numeric 'order' property in page.yml (lower first, within each level)
|
|
2655
2586
|
// 2. pages: array in parent config (wildcard-aware, overrides numeric order)
|
|
@@ -2786,7 +2717,7 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2786
2717
|
// shorthand reached a backend on the sync lane and was silently ignored on
|
|
2787
2718
|
// a static build — the works-on-one-lane shape. Every level reads it
|
|
2788
2719
|
// through `declaredFetch` now.
|
|
2789
|
-
fetch:
|
|
2720
|
+
fetch: siteFetch,
|
|
2790
2721
|
fetcher: warnRetiredFetcherKeys(siteConfig.fetcher),
|
|
2791
2722
|
// NOTE: `intelligence.yml` was read here and emitted as `config.intelligence`.
|
|
2792
2723
|
// Removed 2026-08-12 — the assistant surface is `site.yml::assistant`, which
|