@uniweb/runtime 1.1.14 → 1.1.16

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/runtime",
3
- "version": "1.1.14",
3
+ "version": "1.1.16",
4
4
  "description": "Minimal runtime for loading Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -36,7 +36,7 @@
36
36
  "node": ">=22.12"
37
37
  },
38
38
  "dependencies": {
39
- "@uniweb/core": "~1.4.0",
39
+ "@uniweb/core": "~1.4.1",
40
40
  "@uniweb/theming": "~1.2.1"
41
41
  },
42
42
  "devDependencies": {
@@ -44,7 +44,7 @@
44
44
  "esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0 || ^0.28.0",
45
45
  "vite": "^7.3.1",
46
46
  "vitest": "^4.1.7",
47
- "@uniweb/build": "1.7.0"
47
+ "@uniweb/build": "1.8.1"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "react": "^19.0.0",
@@ -10,7 +10,7 @@ import React, { useState, useEffect } from 'react'
10
10
  import { sectionDomId } from '@uniweb/core/section-id'
11
11
  import { prepareProps, getComponentMeta } from '../prepare-props.js'
12
12
  import Background from './Background.jsx'
13
- import { sectionStickyProps } from '../sticky.js'
13
+ import { sectionStickyProps, sectionScrollMargin } from '../sticky.js'
14
14
  import { noteBodySection } from '../sticky-top.js'
15
15
 
16
16
  /**
@@ -169,6 +169,9 @@ export default function BlockRenderer({ block, as = 'section', stickySelf = fals
169
169
  wrapperProps.style = { ...wrapperProps.style, ...sticky.style }
170
170
  wrapperProps['data-uw-sticky'] = sticky['data-uw-sticky']
171
171
  }
172
+ // On a page whose layout has a sticky area, a link to this section lands below it.
173
+ const margin = sectionScrollMargin(block, stickySelf)
174
+ if (margin) wrapperProps.style = { ...wrapperProps.style, ...margin }
172
175
  // A body section's element, for the measurement of what is stuck over the body (`../sticky-top.js`).
173
176
  if (stickySelf === 'section' || stickySelf === true) wrapperProps.ref = (element) => noteBodySection(block, element)
174
177
 
@@ -5,6 +5,8 @@
5
5
  * with CSS loading in parallel.
6
6
  */
7
7
 
8
+ import { foundationCssUrl } from '@uniweb/core/page-shell'
9
+
8
10
  /**
9
11
  * Resolve a foundation/extension URL so that absolute-path forms
10
12
  * (`/_module/...`) anchor to the document's origin, not to the importing
@@ -75,11 +77,11 @@ export async function loadFoundationCSS(url) {
75
77
  */
76
78
  export async function loadFoundation(source) {
77
79
  const rawUrl = typeof source === 'string' ? source : source.url
78
- // Auto-derive CSS URL from JS URL by convention: entry.js → assets/style.css.
80
+ // Auto-derive CSS URL from JS URL by the one convention, entry.js → assets/style.css
81
+ // (`foundationCssUrl`, which a page's shell links the primary's stylesheet by too).
79
82
  // Pre-Phase-5 foundations were named foundation.js + assets/foundation.css;
80
83
  // those keep working when an explicit `cssUrl` is passed in `source`.
81
- const rawCssUrl = typeof source === 'object' ? source.cssUrl
82
- : rawUrl.replace(/[^/]+\.js$/, 'assets/style.css')
84
+ const rawCssUrl = typeof source === 'object' ? source.cssUrl : foundationCssUrl(rawUrl)
83
85
 
84
86
  // Phase 5: when the runtime is loaded from a different host than the
85
87
  // document (e.g. runtime served from cdn.uniweb.app, page on
@@ -119,6 +119,12 @@ export const ISOLATE_API = Object.freeze({
119
119
  RESOLUTION_STATUS: '0.28.6',
120
120
  // a redirect the host answers itself, with the visitor's query and fragment on it
121
121
  carryQuery: '1.0.1',
122
+ // the `__DATA__` element a host writes into a page's shell, and the escaping it uses
123
+ dataScript: UNRELEASED,
124
+ jsonForScript: UNRELEASED,
125
+ // the page's whole shell, and the foundation stylesheet convention it links by
126
+ pageShell: UNRELEASED,
127
+ foundationCssUrl: UNRELEASED,
122
128
  })
123
129
 
124
130
  /**
package/src/setup.js CHANGED
@@ -355,10 +355,16 @@ export async function decodeData() {
355
355
  const raw = el.textContent
356
356
 
357
357
  // Plain JSON (uncompressed)
358
+ // ⛔ A payload that is there and does not parse is said as such: the boot then finds no
359
+ // configuration, and "No site configuration found" alone sent a reader looking for a payload
360
+ // that was present. Measured 2026-10-23: a host escaped `<!--` as `<\!--`, which JSON has no
361
+ // escape for, and every page stayed its server render with nothing naming the cause. JSON
362
+ // inside a <script> escapes `<` as `<` (`build/src/prerender.js`).
358
363
  if (el.type === 'application/json') {
359
364
  try {
360
365
  return JSON.parse(raw)
361
- } catch {
366
+ } catch (err) {
367
+ console.error(`[Runtime] __DATA__ is not valid JSON, so the site cannot start: ${err.message}`)
362
368
  return null
363
369
  }
364
370
  }
@@ -31,10 +31,11 @@ import {
31
31
  ensureThemeCss,
32
32
  } from './wire-foundation.js'
33
33
  import { resolveLayoutTransitions, resolveLayoutLayers, areaWrapperProps } from './area-wrappers.js'
34
- import { layoutStickiness, sectionStickyProps, zoneProps } from './sticky.js'
34
+ import { layoutStickiness, sectionStickyProps, sectionScrollMargin, zoneProps } from './sticky.js'
35
35
  import { DefaultLayout } from './default-layout.js'
36
36
  import { renderAppearanceBootScript } from './appearance.js'
37
37
  import { documentTitle } from './document-title.js'
38
+ import { escapeHtml } from '@uniweb/core/page-shell'
38
39
 
39
40
  // Re-export L2 helpers so the public `@uniweb/runtime/ssr` surface
40
41
  // carries everything an SSR consumer needs from one entry point.
@@ -274,6 +275,9 @@ export function renderBlock(block, { as = 'section', stickySelf = false } = {})
274
275
  wrapperProps.style = { ...wrapperProps.style, ...sticky.style }
275
276
  wrapperProps['data-uw-sticky'] = sticky['data-uw-sticky']
276
277
  }
278
+ // On a page whose layout has a sticky area, a link to this section lands below it.
279
+ const margin = sectionScrollMargin(block, stickySelf)
280
+ if (margin) wrapperProps.style = { ...wrapperProps.style, ...margin }
277
281
 
278
282
  // Merge Component.className (static classes declared on the component function)
279
283
  const componentClassName = Component.className
@@ -652,18 +656,9 @@ export function renderPage(page, website) {
652
656
  // HTML injection
653
657
  // ============================================================================
654
658
 
655
- /**
656
- * Escape HTML special characters.
657
- */
658
- export function escapeHtml(str) {
659
- if (!str) return ''
660
- return String(str)
661
- .replace(/&/g, '&amp;')
662
- .replace(/</g, '&lt;')
663
- .replace(/>/g, '&gt;')
664
- .replace(/"/g, '&quot;')
665
- .replace(/'/g, '&#39;')
666
- }
659
+ // Escape HTML special characters — the one implementation, which a page's shell writes with too
660
+ // (`@uniweb/core/page-shell`). Still exported from here, as it has been since 0.6.14.
661
+ export { escapeHtml }
667
662
 
668
663
  /**
669
664
  * Inject prerendered content into an HTML shell.
package/src/ssr.js CHANGED
@@ -100,3 +100,20 @@ export { collectSiteRecords } from './collect-records.js'
100
100
  // Appearance. injectPageContent() already emits this for every prerendered
101
101
  // page; exported for lanes that assemble a shell without a per-page render.
102
102
  export { renderAppearanceBootScript } from './appearance.js'
103
+
104
+ // ⭐ THE `__DATA__` ELEMENT A HOST WRITES INTO A PAGE'S SHELL — the boot payload as JSON a
105
+ // `<script>` cannot end or misparse (every `<` as `<`), carrying only the served page's
106
+ // sections when the content names where the others are (`config.pageHashes`). The rule a static
107
+ // build writes its own page data by (`@uniweb/core/data-script`). ⛔ A host wrote `__DATA__` with
108
+ // its own escaping until 2026-10-23, escaping `<!--` as `<\!--` — a JavaScript-string escape that
109
+ // is invalid JSON — and no page of the site it served could start (`decodeData`).
110
+ // `jsonForScript` is the escaping alone, for any other JSON a host writes into a `<script>`.
111
+ export { dataScript, jsonForScript } from '@uniweb/core/data-script'
112
+
113
+ // ⭐ THE PAGE'S SHELL — the whole HTML a page starts as, from what only the host holds (its
114
+ // addresses, the runtime's manifest, the content it read): the head, an empty `#root`, and
115
+ // `__DATA__`. Page assembly is framework's [Diego, 2026-10-23]. In an isolate it is the `shell`
116
+ // `createPageRenderer` renders into, versioned with the runtime; a host links the same function
117
+ // (`@uniweb/core/page-shell`) for a page it serves without rendering. `foundationCssUrl` is the
118
+ // one convention for a foundation's stylesheet, which the loader and the shell both follow.
119
+ export { pageShell, foundationCssUrl } from '@uniweb/core/page-shell'
package/src/sticky.js CHANGED
@@ -111,6 +111,34 @@ export function sectionStickyStyle(block) {
111
111
  return block?.sticky ? { ...stickyStyle(block.stickyTop || STICKY_SECTION_TOP, 1), alignSelf: 'start' } : null
112
112
  }
113
113
 
114
+ /**
115
+ * Whether any area of a page's layout sticks — its sections' components initialized, as both
116
+ * renderers do before they render.
117
+ *
118
+ * @param {Object} page
119
+ * @returns {boolean}
120
+ */
121
+ export function hasStickyArea(page) {
122
+ const areas = page?.getLayoutAreas?.() || {}
123
+ return Object.values(areas).some((blocks) => areaStickiness(blocks) !== null)
124
+ }
125
+
126
+ /**
127
+ * Where a link to a section lands — below what is stuck at the top, `--uw-sticky-top` — given to a
128
+ * body section's box, and a child section's, on a page whose layout has a sticky area. A link to
129
+ * `#section-…` (a search result, a contents entry, a pasted deep link) scrolls the section's top
130
+ * edge to the top of the viewport, and without the margin the stuck header covers it — measured
131
+ * 2026-10-20: a section's top at 0px under an 80px header, at 80px with the margin. On other pages
132
+ * the markup is unchanged, and unmeasured the margin is `0px`.
133
+ *
134
+ * @param {Object} block - its component initialized
135
+ * @param {'section'|'child'|boolean} kind - a body section's box, or a child section's; else none
136
+ * @returns {{ scrollMarginTop: string }|null}
137
+ */
138
+ export function sectionScrollMargin(block, kind) {
139
+ return kind && hasStickyArea(block?.page) ? { scrollMarginTop: STICKY_SECTION_TOP } : null
140
+ }
141
+
114
142
  /**
115
143
  * What a section's wrapper adds when it sticks: its style, and which kind of box it is.
116
144
  *