@uniweb/build 0.70.0 → 0.71.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.70.0",
3
+ "version": "0.71.0",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -57,15 +57,15 @@
57
57
  "js-yaml": "^4.1.0",
58
58
  "sharp": "^0.35.3",
59
59
  "yaml": "^2.5.0",
60
- "@uniweb/content-reader": "^1.2.7",
61
60
  "@uniweb/content-writer": "^0.3.4",
62
61
  "@uniweb/semantic-parser": "^1.4.1",
63
- "@uniweb/schemas": "^0.9.0",
64
- "@uniweb/projections": "^0.7.9",
65
- "@uniweb/theming": "^0.1.16"
62
+ "@uniweb/projections": "^0.7.10",
63
+ "@uniweb/theming": "^0.1.16",
64
+ "@uniweb/content-reader": "^1.2.7",
65
+ "@uniweb/schemas": "^0.9.1"
66
66
  },
67
67
  "optionalDependencies": {
68
- "@uniweb/runtime": "^0.28.0"
68
+ "@uniweb/runtime": "^0.28.1"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -74,8 +74,8 @@
74
74
  "@tailwindcss/vite": "^4.0.0",
75
75
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
76
76
  "vite-plugin-svgr": "^4.0.0",
77
- "@uniweb/core": "^0.33.0",
78
- "@uniweb/runtime": "^0.28.0"
77
+ "@uniweb/core": "^0.34.0",
78
+ "@uniweb/runtime": "^0.28.1"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
81
  "vite": {
@@ -749,8 +749,13 @@ function prettifySlug(slug) {
749
749
  *
750
750
  * A node with no sections of its own (a group/container node, a redirect stub)
751
751
  * has nothing to humanize from, so it shows the identifier the author actually
752
- * wrote — `v1` stays `v1`. A node with content falls back to its opening H1,
753
- * and only then to a prettified slug.
752
+ * wrote — `v1` stays `v1`. A node with content falls back to the opening H1 of
753
+ * its first section that publishes, and only then to a prettified slug.
754
+ *
755
+ * ⭐ "That publishes": a `hidden: true` section is a draft, so its heading never
756
+ * titles the page — in dev as in a published build, so the title does not change
757
+ * between them, and as a backend's publish does [2026-09-28]. A node whose sections
758
+ * are all hidden takes the prettified slug.
754
759
  *
755
760
  * The runtime has no fallback of its own: an empty title renders empty. The
756
761
  * fill belongs here, at the producer, because `title` also feeds `<title>`,
@@ -765,7 +770,8 @@ function prettifySlug(slug) {
765
770
  function resolveDisplayTitle(declaredTitle, segment, sections) {
766
771
  if (declaredTitle) return declaredTitle
767
772
  if (!sections?.length) return segment
768
- return extractH1(sections[0]?.content) || prettifySlug(segment)
773
+ const first = sections.find((section) => section?.hidden !== true)
774
+ return extractH1(first?.content) || prettifySlug(segment)
769
775
  }
770
776
 
771
777
  // ⭐ The rule now lives in `utils/numeric-prefix.js` — `records/folder.yml` needs the
@@ -957,7 +963,9 @@ async function processFileAsPage(filePath, fileName, siteRoot, parentRoute) {
957
963
  version: null,
958
964
  versionMeta: null,
959
965
  versionScope: null,
960
- hidden: false,
966
+ // The file IS the page, so its `hidden: true` drafts the page — left out of a published
967
+ // build, kept in dev — rather than leaving a page with no section (2026-09-28).
968
+ hidden: section.hidden === true,
961
969
  hideIn: [],
962
970
  layout: {},
963
971
  seo: {
@@ -2806,7 +2814,7 @@ export async function collectSiteContent(sitePath, options = {}) {
2806
2814
  // Dev keeps them so drafts stay previewable.
2807
2815
  pages: dropUnpublished ? dropUnpublishedPages(pages).map(withoutHiddenSections) : pages,
2808
2816
  // Layout area sets: { default: { header: page, footer: page, ... }, marketing: { ... } }
2809
- layouts: dropUnpublished ? mapLayoutAreas(layouts, withoutHiddenSections) : layouts,
2817
+ layouts: dropUnpublished ? withoutHiddenAreas(layouts) : layouts,
2810
2818
  // ⭐ THE SAME REACHABILITY AXIS, for the 404 slot. `hidden: true` means DRAFT —
2811
2819
  // "excluded from the published site" (`docs/reference/page-configuration.md`);
2812
2820
  // `hideIn: ['*']` is the control for "routed but in no nav".
@@ -2836,16 +2844,23 @@ function withoutHiddenSections(page) {
2836
2844
  return { ...page, sections: dropHiddenSections(page.sections) }
2837
2845
  }
2838
2846
 
2839
- // Apply `fn` to every area of every layout: `{ default: { header: page, … }, … }`.
2840
- function mapLayoutAreas(layouts, fn) {
2847
+ // Every layout's areas without their hidden sections — and without an area whose sections
2848
+ // were all hidden: drafting every section of an area drafts the area, so a layout gets no area
2849
+ // there rather than an empty one (a backend's publish does the same).
2850
+ function withoutHiddenAreas(layouts) {
2841
2851
  if (!layouts || typeof layouts !== 'object') return layouts
2842
2852
  return Object.fromEntries(
2843
- Object.entries(layouts).map(([name, areas]) => [
2844
- name,
2845
- areas && typeof areas === 'object'
2846
- ? Object.fromEntries(Object.entries(areas).map(([area, page]) => [area, fn(page)]))
2847
- : areas,
2848
- ])
2853
+ Object.entries(layouts).map(([name, areas]) => {
2854
+ if (!areas || typeof areas !== 'object') return [name, areas]
2855
+ const kept = {}
2856
+ for (const [area, page] of Object.entries(areas)) {
2857
+ const shown = withoutHiddenSections(page)
2858
+ const hadSections = Array.isArray(page?.sections) && page.sections.length > 0
2859
+ if (hadSections && shown.sections.length === 0) continue
2860
+ kept[area] = shown
2861
+ }
2862
+ return [name, kept]
2863
+ })
2849
2864
  )
2850
2865
  }
2851
2866
 
@@ -32,7 +32,7 @@ export function sameMarkdownDocument(markdown, doc) {
32
32
  }
33
33
 
34
34
  // A document without the attributes that only restate a default: an inset's `embedKind: 'visual'`,
35
- // which the parser writes and the pull's re-inlining leaves out (`reinlineInsets`) — so a body holding
35
+ // which the parser writes and the pull's re-inlining (retired 2026-09-28) left out — so a body holding
36
36
  // an inset never compared as unchanged (measured 2026-09-26 on the `marketing` template's hero).
37
37
  function withoutDefaults(doc) {
38
38
  return JSON.parse(JSON.stringify(doc), (_, v) => {
@@ -29,8 +29,8 @@
29
29
  // It needs TWO bases, and that is the non-obvious part.
30
30
  //
31
31
  // Our document and the backend's are not byte-comparable renderings of the same
32
- // unit: the backend's copy carries fields we don't emit (`params`,
33
- // `theme_override`, its own `$uuid`) and serializes in its own key order. So a hash
32
+ // unit: the backend's copy carries fields we don't emit (its own `$uuid`, for
33
+ // one) and serializes in its own key order. So a hash
34
34
  // taken on our side and one taken on theirs differ for a unit NEITHER side touched.
35
35
  // A single base therefore validates exactly one comparison and silently corrupts
36
36
  // the other — with a local-representation base every unit looks "changed
@@ -383,50 +383,12 @@ export function siteInfoToConfig({ document, siteRoot, backend = null, sourceLoc
383
383
  // Section records → section .md files
384
384
  // ---------------------------------------------------------------------------
385
385
 
386
- /**
387
- * Re-inline a section's extracted insets back into its ProseMirror content —
388
- * the exact inverse of content-collector's `extractInsets`. The producer pulls
389
- * each `![alt](@Component){params}` ref out of the body into an `insets[]` array
390
- * and leaves an `inset_placeholder` behind; content-writer only serializes
391
- * `inset_ref` nodes, so we restore them before serializing or the inset would
392
- * be reported as unmappable and dropped.
393
- *
394
- * @param {object} content - the section's ProseMirror document (placeholders in)
395
- * @param {Array} insets - `[{ refId, type, params, title, embedKind }]`
396
- * @returns {object} a content document with `inset_ref` nodes restored
397
- */
398
- function reinlineInsets(content, insets) {
399
- if (!content || !Array.isArray(content.content) || !Array.isArray(insets) || insets.length === 0) {
400
- return content
401
- }
402
- const byRef = new Map(insets.map((i) => [i.refId, i]))
403
-
404
- const visit = (nodes) =>
405
- nodes.map((node) => {
406
- if (!node) return node
407
- if (node.type === 'inset_placeholder') {
408
- const inset = byRef.get(node.attrs?.refId)
409
- if (!inset) return node // no match → leave the placeholder (the guard reports it)
410
- const attrs = { component: inset.type, ...(inset.params || {}) }
411
- if (inset.title != null) attrs.alt = inset.title
412
- // `visual` is the extractor's default — omit it so the projected markdown
413
- // doesn't gain a spurious `{embedKind=visual}` the source never had.
414
- if (inset.embedKind && inset.embedKind !== 'visual') attrs.embedKind = inset.embedKind
415
- return { type: 'inset_ref', attrs }
416
- }
417
- if (Array.isArray(node.content)) return { ...node, content: visit(node.content) }
418
- return node
419
- })
420
-
421
- return { ...content, content: visit(content.content) }
422
- }
423
-
424
386
  /**
425
387
  * Project one section `$`-record (from `page_sections` / `layout_sections`) to a
426
388
  * section `.md` file — the inverse of site.js `mapSectionData`. Frontmatter is
427
- * `type` + the flat `params` + `background` / `theme` (`theme_override`) /
428
- * `fetch` / `id` (`stable_id`); the body is the section's
429
- * content (insets re-inlined) serialized to markdown. Idempotent.
389
+ * `type` + `hidden` + the flat `params` (`background` and `theme` among them) +
390
+ * the data declaration (`params.fetch`, else the `fetch` field) + `id` (`stable_id`);
391
+ * the body is the section's content serialized to markdown. Idempotent.
430
392
  *
431
393
  * Note: `$children` (a section's child sections) are NOT written here — the page
432
394
  * walk (`pageSectionsToFiles`) writes each to its own file and nests it under its
@@ -444,7 +406,10 @@ function reinlineInsets(content, insets) {
444
406
  export function sectionRecordToFile({ filePath, record, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.defaultSourceLocale, collector, freeformRelPath, freeformCandidates = null, writeId = true, context = null }) {
445
407
  // ⛔ A stored `preset` or `input` is not written back: neither has an effect, and the build
446
408
  // would only warn about it (2026-09-27).
447
- const { type, stable_id, hidden, params, content, insets, fetch, background, theme_override } = record || {}
409
+ // ⛔ No `background` / `theme_override` / `insets`: the site-content Model dropped all three
410
+ // (2026-09-28), with a stored background and theme copied into `params`. Until then a field
411
+ // filled in where `params` lacked the key, and a stored `insets[]` was re-inlined into the body.
412
+ const { type, stable_id, hidden, params, content, fetch } = record || {}
448
413
 
449
414
  // A localized `content` field unwraps to the source-locale doc for the body; its
450
415
  // target-locale structural maps are captured into the locales/ collector, and any
@@ -452,7 +417,7 @@ export function sectionRecordToFile({ filePath, record, sourceLocale = LOCALIZED
452
417
  // locales/freeform/. A bare doc (source-only / pre-localization) passes through.
453
418
  const sourceContent = unwrapLocalizedContent(content, sourceLocale, collector, freeformRelPath, freeformCandidates, context)
454
419
 
455
- const { hidden: paramHidden, ...paramsRest } = params && typeof params === 'object' ? params : {}
420
+ const { hidden: paramHidden, fetch: paramFetch, ...paramsRest } = params && typeof params === 'object' ? params : {}
456
421
  const frontmatter = {}
457
422
  if (type !== undefined) frontmatter.type = type
458
423
  // ⭐ `hidden:` says what the store says: written for a hidden section, and removed from the
@@ -461,25 +426,22 @@ export function sectionRecordToFile({ filePath, record, sourceLocale = LOCALIZED
461
426
  // `params`, so one stored there counts when the field is absent.
462
427
  frontmatter.hidden = (typeof hidden === 'boolean' ? hidden : paramHidden === true) ? true : undefined
463
428
  Object.assign(frontmatter, paramsRest)
464
- // ⚠️ A stored `background` / `theme_override` field wins over the same key in `params` while
465
- // one is stored: an editor may still write the fields rather than `params` (2026-09-28;
466
- // uwx-format.md § A page section's fields, Order).
467
- if (background !== undefined) frontmatter.background = background
468
- if (theme_override !== undefined) frontmatter.theme = theme_override
469
429
  // Invert the build's resolution rather than copy it — see fetch-shapes.js. A new
470
430
  // file gets `query:` for a declaration of names alone; a section that declares
471
431
  // its data locally keeps it (`writeSectionFile`, the declaration keys).
472
- if (fetch !== undefined) {
473
- const { key, value } = authorableDeclaration(fetch)
432
+ // ⭐ From `params.fetch` when present — where a section carries its data since 2026-09-28 —
433
+ // else the stored field.
434
+ const ownFetch = paramFetch !== undefined ? paramFetch : fetch
435
+ if (ownFetch !== undefined) {
436
+ const { key, value } = authorableDeclaration(ownFetch)
474
437
  frontmatter[key] = value
475
438
  }
476
439
  // `id:` only where the file's name does not already give it (`writeId`, `pageSectionsToFiles`).
477
440
  if (stable_id !== undefined && writeId) frontmatter.id = stable_id
478
441
 
479
- const body = insets ? reinlineInsets(sourceContent, insets) : sourceContent
480
442
  // ⛔ Not `type: Content` into a file that names no type: it is what the push sent for it, and
481
443
  // absent, the foundation's `defaultSection` renders it. Until 2026-09-26 a pull wrote it back.
482
- return writeSectionFile({ filePath, content: body, params: frontmatter, implied: { type: FILLED_SECTION_TYPE } })
444
+ return writeSectionFile({ filePath, content: sourceContent, params: frontmatter, implied: { type: FILLED_SECTION_TYPE } })
483
445
  }
484
446
 
485
447
  // ---------------------------------------------------------------------------
package/src/uwx/site.js CHANGED
@@ -302,20 +302,25 @@ function localizePageMeta(page, route, sourceLocale, translations) {
302
302
 
303
303
  function mapSectionData(section) {
304
304
  const params = { ...section.params }
305
+ // ⭐ The section's own data rides in `params.fetch` [Diego, 2026-09-28] — nothing on a store's
306
+ // side acts on it by name. ⚠️ Also sent as the `fetch` field it had until then, until an
307
+ // editor's reader looks in `params.fetch` (uwx-format.md § A page section's fields).
308
+ if (section.fetch) params.fetch = section.fetch
305
309
  const data = { type: section.type || FILLED_SECTION_TYPE } // entity type requires `type`
306
310
  setIf(data, 'stable_id', section.stableId ?? undefined)
307
311
  // ⭐ A draft section — the store's own `hidden`, which an editor also writes [2026-09-28].
308
312
  if (section.hidden === true) data.hidden = true
309
313
  // No `preset` or `input`: nothing read either, and the collector drops them (2026-09-27).
310
- // ⭐ `background` and `theme` stay IN `params` [Diego, 2026-09-28] — where the runtime reads
311
- // them, and where a section stores them from now on. ⚠️ Also sent as the two fields they had
312
- // until today, until every reader of a stored section reads `params`; a pull prefers a
313
- // stored field meanwhile (`sectionRecordToFile`). uwx-format.md § A page section's fields.
314
+ // ⭐ `background` and `theme` ride IN `params` [Diego, 2026-09-28] — where the runtime reads
315
+ // them, and where a section stores them. ⛔ Not also as the `background` / `theme_override`
316
+ // fields, as build 0.69 sent them: every reader of a stored section reads `params` — a publish
317
+ // carries `params` whole, and the app reads a stored field first, then `params` — so nothing
318
+ // needs them, and a push that stops sending them lets a backend retire them without refusing
319
+ // it. A pull still prefers a stored field while one exists (`sectionRecordToFile`).
320
+ // uwx-format.md § A page section's fields, Order.
314
321
  if (Object.keys(params).length > 0) data.params = params
315
322
  data.content = section.content // leaf insets in it as written — no `insets` field since 2026-09-27
316
323
  setIf(data, 'fetch', section.fetch ?? undefined)
317
- setIf(data, 'background', params.background)
318
- setIf(data, 'theme_override', params.theme)
319
324
  return data
320
325
  }
321
326