@uniweb/build 1.8.1 → 1.8.2
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 +8 -8
- package/src/i18n/records.js +6 -5
- package/src/prerender.js +6 -13
- package/src/site/index.js +4 -3
- package/src/site/plugin.js +5 -10
- package/src/site/queries-config.js +26 -1
- package/src/uwx/index.js +3 -0
- package/src/uwx/site-project.js +16 -7
- package/src/uwx/site.js +27 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -56,15 +56,15 @@
|
|
|
56
56
|
"js-yaml": "^4.1.0",
|
|
57
57
|
"sharp": "^0.35.3",
|
|
58
58
|
"yaml": "^2.5.0",
|
|
59
|
+
"@uniweb/content-reader": "~1.5.4",
|
|
60
|
+
"@uniweb/projections": "~1.1.7",
|
|
59
61
|
"@uniweb/schemas": "~1.6.5",
|
|
60
62
|
"@uniweb/semantic-parser": "~1.12.2",
|
|
61
|
-
"@uniweb/
|
|
62
|
-
"@uniweb/content-writer": "~1.3.0"
|
|
63
|
-
"@uniweb/content-reader": "~1.5.4",
|
|
64
|
-
"@uniweb/theming": "~1.2.1"
|
|
63
|
+
"@uniweb/theming": "~1.2.1",
|
|
64
|
+
"@uniweb/content-writer": "~1.3.0"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
|
-
"@uniweb/runtime": "~1.1.
|
|
67
|
+
"@uniweb/runtime": "~1.1.16"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"@tailwindcss/vite": "^4.0.0",
|
|
74
74
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
75
75
|
"vite-plugin-svgr": "^4.0.0",
|
|
76
|
-
"@uniweb/core": "~1.4.
|
|
77
|
-
"@uniweb/runtime": "~1.1.
|
|
76
|
+
"@uniweb/core": "~1.4.1",
|
|
77
|
+
"@uniweb/runtime": "~1.1.16"
|
|
78
78
|
},
|
|
79
79
|
"peerDependenciesMeta": {
|
|
80
80
|
"vite": {
|
package/src/i18n/records.js
CHANGED
|
@@ -1011,11 +1011,12 @@ export async function buildLocalizedRecords(siteRoot, options = {}) {
|
|
|
1011
1011
|
}
|
|
1012
1012
|
|
|
1013
1013
|
// ⭐ AND THE QUERY'S PER-RECORD FILES, beside the translated list and with the
|
|
1014
|
-
// same translations — `/<locale>/data/<query
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1014
|
+
// same translations — `/<locale>/data/<query>/<$name>.json`. Every query writes each
|
|
1015
|
+
// record whole there (2026-09-27), and a parametric page reads its record from that
|
|
1016
|
+
// file. ⛔ Until 2026-09-14 they were not written for any locale, so a localized
|
|
1017
|
+
// site's records showed their whole fields — an article's body — in the source
|
|
1018
|
+
// language under a translated list. ⛔ *This said only a `deferred:` query had them
|
|
1019
|
+
// until 2026-10-24; `deferred:` was removed on 2026-09-27.*
|
|
1019
1020
|
// They are translated first: a lean list's derived excerpt is derived from its record's
|
|
1020
1021
|
// translated body (`EXCERPT`).
|
|
1021
1022
|
const localeRecordsDir = join(localeDataDir, queryName)
|
package/src/prerender.js
CHANGED
|
@@ -28,6 +28,7 @@ import DataStore from '@uniweb/core/datastore'
|
|
|
28
28
|
import { executeFetch, mergeDataIntoContent, toFetchList } from './site/data-fetcher.js'
|
|
29
29
|
import { createFileTransport } from './site/file-transport.js'
|
|
30
30
|
import { shouldSplitContent } from './site/split-content.js'
|
|
31
|
+
import { jsonForScript, splitPageSections } from '@uniweb/core/data-script'
|
|
31
32
|
import { FONT_LINKS_MARKER } from './site/head-markers.js'
|
|
32
33
|
import { getAdapter } from './hosts/index.js'
|
|
33
34
|
import { detectCiContext } from './hosts/detect-ci-context.js'
|
|
@@ -724,18 +725,10 @@ export function injectBuildData(html, siteContent, { splitContent = false, curre
|
|
|
724
725
|
delete contentForJson.theme.links
|
|
725
726
|
}
|
|
726
727
|
|
|
727
|
-
// Split mode:
|
|
728
|
-
//
|
|
728
|
+
// Split mode: sections for the current page and every dynamic template only — the rule a host
|
|
729
|
+
// writes its `__DATA__` by too (`@uniweb/core/data-script`). Every other page's are in `_pages/`.
|
|
729
730
|
if (splitContent) {
|
|
730
|
-
contentForJson = {
|
|
731
|
-
...contentForJson,
|
|
732
|
-
pages: contentForJson.pages.map(page => {
|
|
733
|
-
if (page.route === currentRoute) return page
|
|
734
|
-
if (page.isDynamic) return page
|
|
735
|
-
const { sections, ...metadata } = page
|
|
736
|
-
return metadata
|
|
737
|
-
})
|
|
738
|
-
}
|
|
731
|
+
contentForJson = { ...contentForJson, pages: splitPageSections(contentForJson.pages, currentRoute) }
|
|
739
732
|
}
|
|
740
733
|
|
|
741
734
|
// Scope fetched (collection / API) data to this page's cascade in split mode,
|
|
@@ -748,7 +741,7 @@ export function injectBuildData(html, siteContent, { splitContent = false, curre
|
|
|
748
741
|
}
|
|
749
742
|
}
|
|
750
743
|
|
|
751
|
-
const contentScript = `<script id="__SITE_CONTENT__" type="application/json">${
|
|
744
|
+
const contentScript = `<script id="__SITE_CONTENT__" type="application/json">${jsonForScript(contentForJson)}</script>`
|
|
752
745
|
if (result.includes('__SITE_CONTENT__')) {
|
|
753
746
|
// Replace existing site content with updated version (includes expanded dynamic routes)
|
|
754
747
|
result = result.replace(
|
|
@@ -764,7 +757,7 @@ export function injectBuildData(html, siteContent, { splitContent = false, curre
|
|
|
764
757
|
|
|
765
758
|
// Inject icon cache so client can render icons immediately without CDN fetches
|
|
766
759
|
if (siteContent._iconCache) {
|
|
767
|
-
const iconScript = `<script id="__ICON_CACHE__" type="application/json">${
|
|
760
|
+
const iconScript = `<script id="__ICON_CACHE__" type="application/json">${jsonForScript(siteContent._iconCache)}</script>`
|
|
768
761
|
if (result.includes('__ICON_CACHE__')) {
|
|
769
762
|
result = result.replace(
|
|
770
763
|
/<script[^>]*id="__ICON_CACHE__"[^>]*>[\s\S]*?<\/script>/,
|
package/src/site/index.js
CHANGED
|
@@ -12,9 +12,10 @@ export { defineSiteConfig, readSiteConfig, default } from './config.js'
|
|
|
12
12
|
export { PREVIEW_ENV, encodePreview, readPreview } from './preview.js'
|
|
13
13
|
export { collectSiteContent } from './content-collector.js'
|
|
14
14
|
// The `agents:` vocabulary lives in @uniweb/projections (which owns the block);
|
|
15
|
-
// re-exported so the CLI can validate an author's spelling
|
|
16
|
-
//
|
|
17
|
-
|
|
15
|
+
// re-exported so the CLI can validate an author's spelling, and read an
|
|
16
|
+
// `expectedOrigins` entry by its rule, without taking a direct dependency on it.
|
|
17
|
+
// Same shape as the search re-exports.
|
|
18
|
+
export { AGENTS_KEYS, originOf } from '@uniweb/projections'
|
|
18
19
|
export { buildSiteData } from './build-site-data.js'
|
|
19
20
|
export { shouldSplitContent } from './split-content.js'
|
|
20
21
|
export {
|
package/src/site/plugin.js
CHANGED
|
@@ -53,6 +53,9 @@ import { processQueries, writeQueryFiles } from './query-processor.js'
|
|
|
53
53
|
import { RECORDS_DIR, resolveRecordsDir } from './entity-pool.js'
|
|
54
54
|
import { executeFetch, mergeDataIntoContent, toFetchList } from './data-fetcher.js'
|
|
55
55
|
import { shouldSplitContent } from './split-content.js'
|
|
56
|
+
import { jsonForScript } from '@uniweb/core/data-script'
|
|
57
|
+
// A favicon's MIME type by its extension — the rule a host's page shell types it by too.
|
|
58
|
+
import { faviconType } from '@uniweb/core/page-shell'
|
|
56
59
|
import { FONT_LINKS_MARKER } from './head-markers.js'
|
|
57
60
|
import { collectExtensionAssets, isBrowserAsset } from './emit-extensions.js'
|
|
58
61
|
import { extensionNameOf, findSiteExtension } from './workspace-extension.js'
|
|
@@ -86,14 +89,6 @@ function extensionContentType(file) {
|
|
|
86
89
|
return EXTENSION_CONTENT_TYPES[file.split('.').pop()?.toLowerCase()] || 'application/octet-stream'
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
function faviconTypeFor(href) {
|
|
90
|
-
const ext = href.split('?')[0].split('.').pop()?.toLowerCase()
|
|
91
|
-
if (ext === 'svg') return 'image/svg+xml'
|
|
92
|
-
if (ext === 'ico') return 'image/x-icon'
|
|
93
|
-
if (ext === 'png') return 'image/png'
|
|
94
|
-
return null
|
|
95
|
-
}
|
|
96
|
-
|
|
97
92
|
/**
|
|
98
93
|
* Resolve a favicon href for index.html injection.
|
|
99
94
|
* - If config.favicon is a string, use it as-is.
|
|
@@ -1572,14 +1567,14 @@ export function siteContentPlugin(options = {}) {
|
|
|
1572
1567
|
// Source 2: auto-detect public/favicon.{ico,svg,png}
|
|
1573
1568
|
const faviconHref = resolveFaviconHref(contentToInject?.config?.favicon, resolvedPublicDir, basePath)
|
|
1574
1569
|
if (faviconHref) {
|
|
1575
|
-
const type =
|
|
1570
|
+
const type = faviconType(faviconHref)
|
|
1576
1571
|
const typeAttr = type ? ` type="${type}"` : ''
|
|
1577
1572
|
headInjection += ` <link rel="icon"${typeAttr} href="${faviconHref}">\n`
|
|
1578
1573
|
}
|
|
1579
1574
|
|
|
1580
1575
|
// Inject content as JSON script tag
|
|
1581
1576
|
if (inject) {
|
|
1582
|
-
headInjection += ` <script type="application/json" id="${variableName}">${
|
|
1577
|
+
headInjection += ` <script type="application/json" id="${variableName}">${jsonForScript(contentToInject)}</script>\n`
|
|
1583
1578
|
}
|
|
1584
1579
|
|
|
1585
1580
|
let result = html
|
|
@@ -36,7 +36,8 @@ import { detectFoundationType, parseCatalogRef } from './foundation-ref.js'
|
|
|
36
36
|
import { readRegisteredFoundation } from './registered-foundation.js'
|
|
37
37
|
import { buildDataSchemaMap, SCHEMA_NOT_FOUND } from '../resolve-data-schema.js'
|
|
38
38
|
import { resolveFoundationSrcPath } from '../utils/foundation-source-root.js'
|
|
39
|
-
import { buildSchema } from '../schema.js'
|
|
39
|
+
import { buildSchema, loadFoundationConfig } from '../schema.js'
|
|
40
|
+
import { defaultSectionType } from '@uniweb/core/section-type'
|
|
40
41
|
import { refuseQueryRoute, refuseLimit, refuseFilter } from './data-fetcher.js'
|
|
41
42
|
import { readFile } from 'node:fs/promises'
|
|
42
43
|
import yaml from 'js-yaml'
|
|
@@ -466,6 +467,30 @@ export async function foundationSections(siteRoot, siteYml = null) {
|
|
|
466
467
|
return foundationSchemaJson(siteRoot, yml)
|
|
467
468
|
}
|
|
468
469
|
|
|
470
|
+
/**
|
|
471
|
+
* The type a section that names none renders with on this site — its foundation's
|
|
472
|
+
* `defaultSection`, else `Section` (`@uniweb/core/section-type`, the rule the site's render
|
|
473
|
+
* asks). A push fills it into such a section, since a store requires a type, and a pull leaves it
|
|
474
|
+
* out of the section's file again. Read like `foundationSections`: from the foundation's source
|
|
475
|
+
* when it is local, else from its built or registered `schema.json`, else `Section`.
|
|
476
|
+
*
|
|
477
|
+
* @param {string} siteRoot
|
|
478
|
+
* @param {object} [siteYml] - an already-read site.yml
|
|
479
|
+
* @returns {Promise<string>}
|
|
480
|
+
*/
|
|
481
|
+
export async function siteDefaultSectionType(siteRoot, siteYml = null) {
|
|
482
|
+
const yml = siteYml || (await readYamlFile(join(siteRoot, 'site.yml')))
|
|
483
|
+
const srcDir = localFoundationSrcDir(siteRoot, yml)
|
|
484
|
+
if (srcDir) {
|
|
485
|
+
try {
|
|
486
|
+
return defaultSectionType(await loadFoundationConfig(srcDir))
|
|
487
|
+
} catch {
|
|
488
|
+
// a foundation whose source does not read is reported by the build that builds it
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return defaultSectionType(foundationSchemaJson(siteRoot, yml)?._self)
|
|
492
|
+
}
|
|
493
|
+
|
|
469
494
|
/** The data schemas a site's foundation declares, or null when unresolvable. */
|
|
470
495
|
export function foundationDataSchemas(siteRoot, siteYml) {
|
|
471
496
|
return foundationSchemaJson(siteRoot, siteYml)?.dataSchemas || null
|
package/src/uwx/index.js
CHANGED
|
@@ -83,6 +83,9 @@ export { matchStoredRecords } from './record-identity.js'
|
|
|
83
83
|
export { siteSelfScope } from './self-scope.js'
|
|
84
84
|
// What each section type declares its data blocks are — a pull reads a translated block back by it.
|
|
85
85
|
export { dataBlockModels } from '../i18n/data-models.js'
|
|
86
|
+
// The type a section that names none renders with — a push fills it in, and a pull, handed it up
|
|
87
|
+
// front like the scope above, leaves it out of the section's file.
|
|
88
|
+
export { siteDefaultSectionType } from '../site/queries-config.js'
|
|
86
89
|
export {
|
|
87
90
|
siteInfoToConfig,
|
|
88
91
|
sectionRecordToFile,
|
package/src/uwx/site-project.js
CHANGED
|
@@ -44,7 +44,8 @@ import yaml from 'js-yaml'
|
|
|
44
44
|
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
45
45
|
import { writeSiteConfig, writeThemeFile, writeIfChanged, writeSectionFile, writeMergedYaml } from './project-writer.js'
|
|
46
46
|
import { declarationsToQueriesYml } from './records-project.js'
|
|
47
|
-
import {
|
|
47
|
+
import { buildRouteOf } from './site.js'
|
|
48
|
+
import { DEFAULT_SECTION_TYPE } from '@uniweb/core/section-type'
|
|
48
49
|
import { translationContext, SITE_META_CONTEXT } from '../i18n/extract.js'
|
|
49
50
|
import { authorableDeclaration, DECLARATION_KEYS } from '../site/fetch-shapes.js'
|
|
50
51
|
import { createTranslationCollector, writeLocaleTranslations, writeFreeformTranslations, unwrapLocalizedContent, isProseMirrorDoc } from './locale-sync.js'
|
|
@@ -381,9 +382,11 @@ export function siteInfoToConfig({ document, siteRoot, backend = null, sourceLoc
|
|
|
381
382
|
* field to (its source doc → the `.md` body)
|
|
382
383
|
* @param {object} [params.collector] - translation collector; target-locale
|
|
383
384
|
* structural maps on a localized `content` field are captured into it
|
|
385
|
+
* @param {string} [params.impliedType] - the type a section that names none renders with on this
|
|
386
|
+
* site (`siteDefaultSectionType`), which a file leaves out
|
|
384
387
|
* @returns {'updated'|'unchanged'}
|
|
385
388
|
*/
|
|
386
|
-
export function sectionRecordToFile({ filePath, record, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.defaultSourceLocale, collector, freeformRelPath, freeformCandidates = null, writeId = true, context = null, dataModel = null }) {
|
|
389
|
+
export function sectionRecordToFile({ filePath, record, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.defaultSourceLocale, collector, freeformRelPath, freeformCandidates = null, writeId = true, context = null, dataModel = null, impliedType = DEFAULT_SECTION_TYPE }) {
|
|
387
390
|
// ⛔ A stored `preset` or `input` is not written back: neither has an effect, and the build
|
|
388
391
|
// would only warn about it (2026-09-27).
|
|
389
392
|
// ⛔ No `background` / `theme_override` / `insets` / `fetch`: the site-content Model dropped all
|
|
@@ -416,14 +419,15 @@ export function sectionRecordToFile({ filePath, record, sourceLocale = LOCALIZED
|
|
|
416
419
|
// for names alone (`authorableDeclaration`, the inverse of the build's resolution), and
|
|
417
420
|
// removed from the file of a section that declares none. ⛔ Until 2026-10-16 a file's own
|
|
418
421
|
// declaration was kept over the backend's, and the next edit to the section pushed it back.
|
|
419
|
-
// ⛔ Not
|
|
420
|
-
// absent, the
|
|
422
|
+
// ⛔ Not the implied type into a file that names no type: it is what the push sent for it, and
|
|
423
|
+
// absent, the same type renders it (`siteDefaultSectionType`). Until 2026-09-26 a pull wrote it
|
|
424
|
+
// back; until 2026-10-23 the type was `Content`, which the push sent and no foundation rendered.
|
|
421
425
|
return writeSectionFile({
|
|
422
426
|
filePath,
|
|
423
427
|
content: sourceContent,
|
|
424
428
|
params: frontmatter,
|
|
425
429
|
declaration: paramFetch ?? null,
|
|
426
|
-
implied: { type:
|
|
430
|
+
implied: { type: impliedType },
|
|
427
431
|
})
|
|
428
432
|
}
|
|
429
433
|
|
|
@@ -542,6 +546,7 @@ export function pageSectionsToFiles({ pageDir, pageSections, ctx, pageContext })
|
|
|
542
546
|
sourceLocale: ctx?.sourceLocale,
|
|
543
547
|
collector: ctx?.collector,
|
|
544
548
|
dataModel: ctx?.dataModel,
|
|
549
|
+
impliedType: ctx?.sectionType,
|
|
545
550
|
freeformRelPath,
|
|
546
551
|
freeformCandidates,
|
|
547
552
|
writeId: fileSectionName(filePath) !== stableId,
|
|
@@ -1242,6 +1247,7 @@ function projectLayout(layoutSections, layoutBaseDir, report, prune, ctx) {
|
|
|
1242
1247
|
sourceLocale: ctx?.sourceLocale,
|
|
1243
1248
|
collector: ctx?.collector,
|
|
1244
1249
|
dataModel: ctx?.dataModel,
|
|
1250
|
+
impliedType: ctx?.sectionType,
|
|
1245
1251
|
writeId: fileSectionName(filePath) !== stableId,
|
|
1246
1252
|
freeformRelPath: stableId ? buildFreeformPath({ stableId }, page) : null,
|
|
1247
1253
|
freeformCandidates: stableId ? freeformPathsFor({ stableId }, page) : null,
|
|
@@ -1329,9 +1335,12 @@ function refuseContentFilesCannotHold(document, sourceLocale) {
|
|
|
1329
1335
|
* @param {string|null} [params.scope] - the scope the push qualified a query's
|
|
1330
1336
|
* `@/x` schema with, so it is written back as `@/x` — see
|
|
1331
1337
|
* `declarationsToQueriesYml`, whose default it overrides.
|
|
1338
|
+
* @param {string} [params.sectionType] - the type a section that names none renders with on this
|
|
1339
|
+
* site, which the push filled in and a section's file leaves out — `siteDefaultSectionType`,
|
|
1340
|
+
* read by the caller since it reads the foundation's source
|
|
1332
1341
|
* @returns {{ config: object, collections: object, locales: object, pages: string[], sections: string[], layout: string[], deleted: string[], renamed: object[] }}
|
|
1333
1342
|
*/
|
|
1334
|
-
export function siteContentDocumentToProject({ document, siteRoot, backend = null, sourceLocale = documentSourceLocale(document), prune = false, keepAuthoredFoundation = false, scope, dataModel = null }) {
|
|
1343
|
+
export function siteContentDocumentToProject({ document, siteRoot, backend = null, sourceLocale = documentSourceLocale(document), prune = false, keepAuthoredFoundation = false, scope, dataModel = null, sectionType = DEFAULT_SECTION_TYPE }) {
|
|
1335
1344
|
// Before anything is written: a pull that would leave content out of the files stops here.
|
|
1336
1345
|
refuseContentFilesCannotHold(document, sourceLocale)
|
|
1337
1346
|
|
|
@@ -1363,7 +1372,7 @@ export function siteContentDocumentToProject({ document, siteRoot, backend = nul
|
|
|
1363
1372
|
// rename detection, build a fresh one as we project, then persist it. Items not
|
|
1364
1373
|
// re-projected (deleted) drop out naturally. `collector` rides along to capture
|
|
1365
1374
|
// localized scalars during the page walk.
|
|
1366
|
-
const ctx = { siteRoot, oldIndex: readPullIndex(siteRoot), newIndex: {}, report, collector, sourceLocale, sectionFiles: new Map(), dataModel }
|
|
1375
|
+
const ctx = { siteRoot, oldIndex: readPullIndex(siteRoot), newIndex: {}, report, collector, sourceLocale, sectionFiles: new Map(), dataModel, sectionType }
|
|
1367
1376
|
// Where the site keeps its localized URLs — `site.yml`'s map, when it has one (`pageSlugToYml`).
|
|
1368
1377
|
const siteMap = readAuthoredYaml(join(siteRoot, 'site.yml'))?.i18n?.routeTranslations
|
|
1369
1378
|
ctx.routes = { siteMap: isPlainRecord(siteMap) && Object.keys(siteMap).length ? siteMap : null, slugs: new Map() }
|
package/src/uwx/site.js
CHANGED
|
@@ -75,6 +75,7 @@ import {
|
|
|
75
75
|
import { refuseBinding, refuseUnder, refuseOutsideLanguage, warnDuplicateBindings } from '../site/data-fetcher.js'
|
|
76
76
|
import { readLayoutFolder, layoutAreaRoute } from '../site/layout-folder.js'
|
|
77
77
|
import { normalizeHideIn } from '../site/nav-visibility.js'
|
|
78
|
+
import { siteDefaultSectionType } from '../site/queries-config.js'
|
|
78
79
|
import { resolveDefaultLocale, validateLanguageConfig, queryDataUrl } from '@uniweb/core'
|
|
79
80
|
import { emitEntitySyncPackage } from './entity-document.js'
|
|
80
81
|
import { loadLocaleTranslations, localizeScalar, localizeScalarList, localizeContentDoc, localesDir, isLocalizedContent, plainTextTargets } from './locale-sync.js'
|
|
@@ -116,10 +117,6 @@ function scalarString(value) {
|
|
|
116
117
|
// processMarkdownFile takes only type/hidden/props/fetch/query/data/id out of the
|
|
117
118
|
// frontmatter (and drops preset/input), so `background:` and `theme:` stay
|
|
118
119
|
// inside section.params — and ride there on the wire too (`mapSectionData`).
|
|
119
|
-
// The `type` a push sends for a section whose file names none — the Model requires one. A pull does
|
|
120
|
-
// not write it back into a file that names none (`sectionRecordToFile`): absent, the foundation's
|
|
121
|
-
// `defaultSection` renders it, which is not always `Content`.
|
|
122
|
-
export const FILLED_SECTION_TYPE = 'Content'
|
|
123
120
|
|
|
124
121
|
const AUTHORED_SLUG = Symbol('authoredSlug')
|
|
125
122
|
const isPlainRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
@@ -291,7 +288,8 @@ function mapSectionData(section) {
|
|
|
291
288
|
// the `fetch` field beside the params, equal; a pull still reads a stored field where `params`
|
|
292
289
|
// has no `fetch` (`sectionRecordToFile`). uwx-format.md § A page section's fields.
|
|
293
290
|
if (section.fetch) params.fetch = section.fetch
|
|
294
|
-
|
|
291
|
+
// A section whose file names no type gets one once the tree is collected (`fillSectionTypes`).
|
|
292
|
+
const data = { type: section.type }
|
|
295
293
|
setIf(data, 'stable_id', section.stableId ?? undefined)
|
|
296
294
|
// ⭐ A draft section — the store's own `hidden`, which an editor also writes [2026-09-28].
|
|
297
295
|
if (section.hidden === true) data.hidden = true
|
|
@@ -522,6 +520,26 @@ function withIdentity(id, fields) {
|
|
|
522
520
|
return Object.assign({ $id: id }, fields)
|
|
523
521
|
}
|
|
524
522
|
|
|
523
|
+
// ⭐ A SECTION WHOSE FILE NAMES NO TYPE IS SENT THE TYPE IT RENDERS WITH — the site's foundation's
|
|
524
|
+
// `defaultSection`, else `Section` (`siteDefaultSectionType`, by `@uniweb/core/section-type`, the
|
|
525
|
+
// rule the site's own render asks). The store requires a type, and a host renders what the store
|
|
526
|
+
// holds. ⛔ Until 2026-10-23 this sent `Content`, which no starter template's foundation has: every
|
|
527
|
+
// such section — every page of a mounted docs folder among them — read "Component not found:
|
|
528
|
+
// Content" on a hosted site while the files rendered `Section`.
|
|
529
|
+
function fillSectionTypes(records, type) {
|
|
530
|
+
for (const record of records || []) {
|
|
531
|
+
if (!record.type) record.type = type // the collector says `null` for a file that names none
|
|
532
|
+
fillSectionTypes(record.$children, type)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function fillPageSectionTypes(pages, type) {
|
|
537
|
+
for (const page of pages || []) {
|
|
538
|
+
fillSectionTypes(page.page_sections, type)
|
|
539
|
+
fillPageSectionTypes(page.$children, type)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
525
543
|
// One collected section → its `$`-record, recursing `subsections` into `$children`
|
|
526
544
|
// (page_sections is self_nesting). `$id` is the section's stableId (rename-stable),
|
|
527
545
|
// falling back to a positional handle only for a truly anonymous section.
|
|
@@ -1596,6 +1614,10 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1596
1614
|
}
|
|
1597
1615
|
|
|
1598
1616
|
const layoutSections = await collectLayoutNested(layoutDir, siteRoot)
|
|
1617
|
+
// Before anything reads a section's type — localizing a data block asks it for the block's model.
|
|
1618
|
+
const sectionType = await siteDefaultSectionType(siteRoot, siteYml)
|
|
1619
|
+
fillPageSectionTypes(pages, sectionType)
|
|
1620
|
+
fillSectionTypes(layoutSections, sectionType)
|
|
1599
1621
|
|
|
1600
1622
|
// Wrap each section's content into its per-locale form (source doc + target
|
|
1601
1623
|
// structural maps from locales/{locale}.json, or a free-form body override from
|