@uniweb/build 1.8.1 → 1.8.3
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/content-collector.js +20 -4
- 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 +30 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.3",
|
|
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/
|
|
60
|
-
"@uniweb/
|
|
61
|
-
"@uniweb/projections": "~1.1.6",
|
|
59
|
+
"@uniweb/content-reader": "~1.5.5",
|
|
60
|
+
"@uniweb/projections": "~1.1.7",
|
|
62
61
|
"@uniweb/content-writer": "~1.3.0",
|
|
63
|
-
"@uniweb/
|
|
62
|
+
"@uniweb/schemas": "~1.6.5",
|
|
63
|
+
"@uniweb/semantic-parser": "~1.13.0",
|
|
64
64
|
"@uniweb/theming": "~1.2.1"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
|
-
"@uniweb/runtime": "~1.1.
|
|
67
|
+
"@uniweb/runtime": "~1.1.18"
|
|
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.3",
|
|
77
|
+
"@uniweb/runtime": "~1.1.18"
|
|
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>/,
|
|
@@ -241,6 +241,22 @@ function detectVersions(folderNames) {
|
|
|
241
241
|
return versions
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
/**
|
|
245
|
+
* An id an author wrote — a page's `id:` in `page.yml`, a section's in its frontmatter — as the
|
|
246
|
+
* string it names. YAML reads `id: 404` as a number, and an id is never one: a `page:404` link
|
|
247
|
+
* names the string, and a store holds a stable id as text.
|
|
248
|
+
*
|
|
249
|
+
* ⛔ *Until 2026-10-24 the parsed value was kept, so a page with `id: 404` matched no `page:`
|
|
250
|
+
* link, and a push sent its stable id as a number, which a backend refuses (measured that day:
|
|
251
|
+
* HTTP 400, "item.data.stable_id must be a string").*
|
|
252
|
+
*
|
|
253
|
+
* @param {*} value
|
|
254
|
+
* @returns {string|null} null for an id not written, or written empty
|
|
255
|
+
*/
|
|
256
|
+
export function authoredId(value) {
|
|
257
|
+
return value === undefined || value === null || value === '' ? null : String(value)
|
|
258
|
+
}
|
|
259
|
+
|
|
244
260
|
/**
|
|
245
261
|
* `query:` — the shorthand for `fetch: { query }`, at every level that declares
|
|
246
262
|
* data: a section's frontmatter, `page.yml`, `folder.yml` and `site.yml`, on the
|
|
@@ -1168,7 +1184,7 @@ async function processMarkdownFile(filePath, id, siteRoot, defaultStableId = nul
|
|
|
1168
1184
|
|
|
1169
1185
|
// Stable ID for scroll targeting: frontmatter id > filename-derived > null
|
|
1170
1186
|
// This ID is stable across reordering (unlike the positional id)
|
|
1171
|
-
const stableId = frontmatterId || defaultStableId || null
|
|
1187
|
+
const stableId = authoredId(frontmatterId) || defaultStableId || null
|
|
1172
1188
|
|
|
1173
1189
|
const section = {
|
|
1174
1190
|
id,
|
|
@@ -1434,7 +1450,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
1434
1450
|
page: {
|
|
1435
1451
|
route,
|
|
1436
1452
|
sourcePath: parentRoute === '/' ? `/${pageName}` : `${parentRoute}/${pageName}`,
|
|
1437
|
-
id: pageConfig.id
|
|
1453
|
+
id: authoredId(pageConfig.id),
|
|
1438
1454
|
isIndex,
|
|
1439
1455
|
title: resolveDisplayTitle(pageConfig.title, pageName, null),
|
|
1440
1456
|
description: pageConfig.description || '',
|
|
@@ -1460,7 +1476,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
1460
1476
|
page: {
|
|
1461
1477
|
route,
|
|
1462
1478
|
sourcePath: parentRoute === '/' ? `/${pageName}` : `${parentRoute}/${pageName}`,
|
|
1463
|
-
id: pageConfig.id
|
|
1479
|
+
id: authoredId(pageConfig.id),
|
|
1464
1480
|
isIndex,
|
|
1465
1481
|
title: resolveDisplayTitle(pageConfig.title, pageName, null),
|
|
1466
1482
|
description: pageConfig.description || '',
|
|
@@ -1731,7 +1747,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
1731
1747
|
page: {
|
|
1732
1748
|
route,
|
|
1733
1749
|
sourcePath, // Original folder-based path (for ancestor checking in navigation)
|
|
1734
|
-
id: pageConfig.id
|
|
1750
|
+
id: authoredId(pageConfig.id), // Stable page ID for page: links (survives reorganization)
|
|
1735
1751
|
isIndex, // Marks this page as the index for its parent route
|
|
1736
1752
|
title: resolveDisplayTitle(pageConfig.title, pageName, hierarchicalSections),
|
|
1737
1753
|
description: pageConfig.description || '',
|
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
|
@@ -71,10 +71,12 @@ import {
|
|
|
71
71
|
rootOrderConfig,
|
|
72
72
|
composeLocalizedRoute,
|
|
73
73
|
isValidSlugSegment,
|
|
74
|
+
authoredId,
|
|
74
75
|
} from '../site/content-collector.js'
|
|
75
76
|
import { refuseBinding, refuseUnder, refuseOutsideLanguage, warnDuplicateBindings } from '../site/data-fetcher.js'
|
|
76
77
|
import { readLayoutFolder, layoutAreaRoute } from '../site/layout-folder.js'
|
|
77
78
|
import { normalizeHideIn } from '../site/nav-visibility.js'
|
|
79
|
+
import { siteDefaultSectionType } from '../site/queries-config.js'
|
|
78
80
|
import { resolveDefaultLocale, validateLanguageConfig, queryDataUrl } from '@uniweb/core'
|
|
79
81
|
import { emitEntitySyncPackage } from './entity-document.js'
|
|
80
82
|
import { loadLocaleTranslations, localizeScalar, localizeScalarList, localizeContentDoc, localesDir, isLocalizedContent, plainTextTargets } from './locale-sync.js'
|
|
@@ -116,10 +118,6 @@ function scalarString(value) {
|
|
|
116
118
|
// processMarkdownFile takes only type/hidden/props/fetch/query/data/id out of the
|
|
117
119
|
// frontmatter (and drops preset/input), so `background:` and `theme:` stay
|
|
118
120
|
// 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
121
|
|
|
124
122
|
const AUTHORED_SLUG = Symbol('authoredSlug')
|
|
125
123
|
const isPlainRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
@@ -291,7 +289,8 @@ function mapSectionData(section) {
|
|
|
291
289
|
// the `fetch` field beside the params, equal; a pull still reads a stored field where `params`
|
|
292
290
|
// has no `fetch` (`sectionRecordToFile`). uwx-format.md § A page section's fields.
|
|
293
291
|
if (section.fetch) params.fetch = section.fetch
|
|
294
|
-
|
|
292
|
+
// A section whose file names no type gets one once the tree is collected (`fillSectionTypes`).
|
|
293
|
+
const data = { type: section.type }
|
|
295
294
|
setIf(data, 'stable_id', section.stableId ?? undefined)
|
|
296
295
|
// ⭐ A draft section — the store's own `hidden`, which an editor also writes [2026-09-28].
|
|
297
296
|
if (section.hidden === true) data.hidden = true
|
|
@@ -314,7 +313,8 @@ function buildPageData(config, ctx) {
|
|
|
314
313
|
// `localizeRouteSlugs`, once the whole tree is known. `mode` is the plain
|
|
315
314
|
// delivery mode.
|
|
316
315
|
const data = { slug: { [sourceLocale]: slug }, mode } // both required by the entity type
|
|
317
|
-
|
|
316
|
+
// An authored id is a string (`authoredId`): `id: 404` reads as a number, which a store refuses.
|
|
317
|
+
setIf(data, 'stable_id', authoredId(config.id) ?? undefined)
|
|
318
318
|
setIf(data, 'title', localizeScalar(config.title, sourceLocale, translations))
|
|
319
319
|
setIf(data, 'description', localizeScalar(config.description, sourceLocale, translations))
|
|
320
320
|
setIf(data, 'label', localizeScalar(config.label, sourceLocale, translations))
|
|
@@ -522,6 +522,26 @@ function withIdentity(id, fields) {
|
|
|
522
522
|
return Object.assign({ $id: id }, fields)
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
// ⭐ A SECTION WHOSE FILE NAMES NO TYPE IS SENT THE TYPE IT RENDERS WITH — the site's foundation's
|
|
526
|
+
// `defaultSection`, else `Section` (`siteDefaultSectionType`, by `@uniweb/core/section-type`, the
|
|
527
|
+
// rule the site's own render asks). The store requires a type, and a host renders what the store
|
|
528
|
+
// holds. ⛔ Until 2026-10-23 this sent `Content`, which no starter template's foundation has: every
|
|
529
|
+
// such section — every page of a mounted docs folder among them — read "Component not found:
|
|
530
|
+
// Content" on a hosted site while the files rendered `Section`.
|
|
531
|
+
function fillSectionTypes(records, type) {
|
|
532
|
+
for (const record of records || []) {
|
|
533
|
+
if (!record.type) record.type = type // the collector says `null` for a file that names none
|
|
534
|
+
fillSectionTypes(record.$children, type)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function fillPageSectionTypes(pages, type) {
|
|
539
|
+
for (const page of pages || []) {
|
|
540
|
+
fillSectionTypes(page.page_sections, type)
|
|
541
|
+
fillPageSectionTypes(page.$children, type)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
525
545
|
// One collected section → its `$`-record, recursing `subsections` into `$children`
|
|
526
546
|
// (page_sections is self_nesting). `$id` is the section's stableId (rename-stable),
|
|
527
547
|
// falling back to a positional handle only for a truly anonymous section.
|
|
@@ -1596,6 +1616,10 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1596
1616
|
}
|
|
1597
1617
|
|
|
1598
1618
|
const layoutSections = await collectLayoutNested(layoutDir, siteRoot)
|
|
1619
|
+
// Before anything reads a section's type — localizing a data block asks it for the block's model.
|
|
1620
|
+
const sectionType = await siteDefaultSectionType(siteRoot, siteYml)
|
|
1621
|
+
fillPageSectionTypes(pages, sectionType)
|
|
1622
|
+
fillSectionTypes(layoutSections, sectionType)
|
|
1599
1623
|
|
|
1600
1624
|
// Wrap each section's content into its per-locale form (source doc + target
|
|
1601
1625
|
// structural maps from locales/{locale}.json, or a free-form body override from
|