@uniweb/runtime 1.5.0 → 1.5.1
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/dist/ssr.js +1 -11
- package/dist/ssr.js.map +1 -1
- package/package.json +4 -4
- package/src/components/PageRenderer.jsx +9 -3
- package/src/page-renderer.js +5 -10
- package/src/ssr-renderer.js +9 -14
- package/src/sticky-check.js +36 -0
- package/src/under-header.js +3 -1
- package/src/start-page.js +0 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/runtime",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Minimal runtime for loading Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,15 +36,15 @@
|
|
|
36
36
|
"node": ">=22.12"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@uniweb/core": "~1.6.
|
|
40
|
-
"@uniweb/theming": "~1.3.
|
|
39
|
+
"@uniweb/core": "~1.6.7",
|
|
40
|
+
"@uniweb/theming": "~1.3.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@vitejs/plugin-react": "^4.5.2",
|
|
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.
|
|
47
|
+
"@uniweb/build": "1.18.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": "^19.0.0",
|
|
@@ -126,13 +126,12 @@ export default function PageRenderer() {
|
|
|
126
126
|
|
|
127
127
|
// ⭐ WHAT THE URL NAMES — `Website#resolveRoute`, the one rule every lane calls
|
|
128
128
|
// (`@uniweb/core/resolve-route`): a page, a redirect, a page served from elsewhere, or
|
|
129
|
-
// nothing. Resolved synchronously (not in an effect)
|
|
130
|
-
//
|
|
129
|
+
// nothing. Resolved synchronously (not in an effect), and the page drawn made active below,
|
|
130
|
+
// so child components like Header see it during the same render cycle.
|
|
131
131
|
const navigate = useNavigate()
|
|
132
132
|
|
|
133
133
|
const resolution = website?.resolveRoute?.(location.pathname) ?? null
|
|
134
134
|
let page = resolution && resolution.kind !== 'notFound' ? resolution.page : null
|
|
135
|
-
if (page && website) website.setActivePage(location.pathname)
|
|
136
135
|
|
|
137
136
|
// ─── Content loading (split page content) ───
|
|
138
137
|
// On first load, the current page's sections are pre-embedded → contentReady = true.
|
|
@@ -182,6 +181,13 @@ export default function PageRenderer() {
|
|
|
182
181
|
page = website?.getNotFoundPage?.() || null
|
|
183
182
|
}
|
|
184
183
|
|
|
184
|
+
// ⭐ THE PAGE DRAWN IS THE ACTIVE ONE, by the Page itself, as the prerender makes it
|
|
185
|
+
// (`renderPage`): a header reading its next block, the language switcher, page state and a
|
|
186
|
+
// contents rail all read the page on screen, the not-found page included. ⛔ Until 2026-11-06 it
|
|
187
|
+
// was made active by the URL, so the not-found page drew with another page active, and a
|
|
188
|
+
// parametric page whose records were not held yet was looked up anew, another Page than this one.
|
|
189
|
+
if (page && website) website.setActivePage(page)
|
|
190
|
+
|
|
185
191
|
// Head metadata — compute for all cases (redirect pages get a fallback)
|
|
186
192
|
const headMeta = page?.getHeadMeta?.() || {
|
|
187
193
|
title: isNotFound ? 'Page Not Found' : (page?.title || 'Website'),
|
package/src/page-renderer.js
CHANGED
|
@@ -38,7 +38,6 @@ import { resolveRequestedPage } from './resolve-page.js'
|
|
|
38
38
|
import { hydrateDataStore } from './wire-foundation.js'
|
|
39
39
|
import { loadPageData } from './load-page-data.js'
|
|
40
40
|
import { requireTransport } from './require-transport.js'
|
|
41
|
-
import { restoreStartPage } from './start-page.js'
|
|
42
41
|
import { whileReadingIcons } from './page-icons.js'
|
|
43
42
|
|
|
44
43
|
/**
|
|
@@ -152,15 +151,11 @@ export function createPageRenderer({ website, shell }) {
|
|
|
152
151
|
// host that answered by the outcome sent 200 for a URL naming no record.
|
|
153
152
|
const missing = !named || page?.notFound === true
|
|
154
153
|
|
|
155
|
-
// ⭐
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// rendered last, so on a Website reused across requests its header could show the previous
|
|
161
|
-
// request's page.
|
|
162
|
-
restoreStartPage(website)
|
|
163
|
-
if (typeof target === 'string') website.setActivePage(target)
|
|
154
|
+
// ⭐ `renderPage` makes the page it renders active, by the Page, the not-found page included,
|
|
155
|
+
// so no render sees the page an earlier request rendered. ⛔ Until 2026-10-26 the not-found
|
|
156
|
+
// page rendered with whatever page this Website rendered last, so on a Website reused across
|
|
157
|
+
// requests its header could show the previous request's page; until 2026-11-06 it rendered with
|
|
158
|
+
// the page the Website started with (or a page served from elsewhere that the route named).
|
|
164
159
|
// ⭐ The page carries the icons it drew, so the browser's first render draws them too.
|
|
165
160
|
const icons = new Map()
|
|
166
161
|
let result
|
package/src/ssr-renderer.js
CHANGED
|
@@ -38,7 +38,6 @@ import { headerOverlay, overlayBoxProps, openingUnderHeader, underHeaderProps }
|
|
|
38
38
|
import { DefaultLayout } from './default-layout.js'
|
|
39
39
|
import { renderAppearanceBootScript } from './appearance.js'
|
|
40
40
|
import { documentTitle } from './document-title.js'
|
|
41
|
-
import { recordStartPage } from './start-page.js'
|
|
42
41
|
import { escapeHtml } from '@uniweb/core/page-shell'
|
|
43
42
|
import { jsonForScript } from '@uniweb/core/data-script'
|
|
44
43
|
|
|
@@ -376,8 +375,6 @@ export function initPrerender(content, foundation, extensionsOrOptions, maybeOpt
|
|
|
376
375
|
// Uniweb constructor wires foundation, capabilities, layoutMeta, handlers,
|
|
377
376
|
// and extensions at construction time — see `@uniweb/core`'s src/uniweb.js.
|
|
378
377
|
const uniweb = createUniweb(content, foundation, extensions)
|
|
379
|
-
// Before anything renders: the active page every render starts from (`start-page.js`).
|
|
380
|
-
recordStartPage(uniweb.activeWebsite)
|
|
381
378
|
|
|
382
379
|
// Set base path from site config for subdirectory deployments
|
|
383
380
|
if (content.config?.base && uniweb.activeWebsite?.setBasePath) {
|
|
@@ -415,12 +412,8 @@ export function initPrerender(content, foundation, extensionsOrOptions, maybeOpt
|
|
|
415
412
|
ensureThemeCss(uniweb, foundation)
|
|
416
413
|
|
|
417
414
|
// Register SSR-safe routing so useRouting()/useActiveRoute() work during prerender.
|
|
418
|
-
// renderPage()
|
|
419
|
-
//
|
|
420
|
-
// not-found page, which no route names. Through `createPageRenderer` it renders
|
|
421
|
-
// with the page a route served from elsewhere names, else the page the Website
|
|
422
|
-
// starts with (`start-page.js`); through `generate404Html`, with the page
|
|
423
|
-
// rendered last.
|
|
415
|
+
// renderPage() makes the page it renders active before rendering it, so
|
|
416
|
+
// activePage.route is the page being rendered, the not-found page included.
|
|
424
417
|
const website = uniweb.activeWebsite
|
|
425
418
|
uniweb.routingComponents = {
|
|
426
419
|
useLocation: () => {
|
|
@@ -548,11 +541,13 @@ export function classifyRenderError(err) {
|
|
|
548
541
|
* @returns {{ renderedContent: string, sectionOverrideCSS: string } | { error: { type: string, message: string } }}
|
|
549
542
|
*/
|
|
550
543
|
export function renderPage(page, website) {
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
|
|
544
|
+
// ⭐ The page it renders is the active one, by the Page itself, as in the browser
|
|
545
|
+
// (`PageRenderer.jsx`): whatever adapts to the page — a header reading its next block, the
|
|
546
|
+
// language switcher, page state, a contents rail — reads the page on screen. ⛔ Until 2026-11-06
|
|
547
|
+
// it was made active by its route. The site's not-found page has none (on a site whose
|
|
548
|
+
// parametric page matches `/404`, that route names THAT page), so it rendered with another page
|
|
549
|
+
// active, and a parametric page whose records were not held yet was made anew by the lookup.
|
|
550
|
+
website.setActivePage(page)
|
|
556
551
|
|
|
557
552
|
// A page that claims content but yields no blocks has not been loaded — it is
|
|
558
553
|
// not an empty page. `Page#bodyBlocks` returns [] when its sections are absent
|
package/src/sticky-check.js
CHANGED
|
@@ -25,6 +25,13 @@
|
|
|
25
25
|
* Table parts are left out: a sticky table header cell sticks within its table, though its row is
|
|
26
26
|
* no taller than it is. So is a child section the runtime made sticky (`data-uw-sticky="child"`):
|
|
27
27
|
* with no room — stacked at one column, or the tallest in its row — it does not stick, by design.
|
|
28
|
+
* And so is an element whose box is sticky itself: the box moves, and the element with it — the
|
|
29
|
+
* runtime's area wrapper inside a layout's own element that `page.isSticky(area)` made sticky, as
|
|
30
|
+
* the docs say to. ⛔ Until 2026-11-06 that layout was told to do what it had done.
|
|
31
|
+
*
|
|
32
|
+
* ⭐ A header over the page's opening section is checked too (`under-header.js`): when what it
|
|
33
|
+
* holds takes no room, a bar made `fixed` or `absolute`, the runtime measures 0px, so the section
|
|
34
|
+
* is not cleared and a link to a section lands under the bar — and nothing else says why.
|
|
28
35
|
*
|
|
29
36
|
* And an element that sits in a box that CLIPS — `overflow: hidden`, or `overflow-x: hidden`
|
|
30
37
|
* alone, which makes the box scroll vertically too — sticks to that box in place of the page,
|
|
@@ -33,6 +40,8 @@
|
|
|
33
40
|
* (a layout's own scrolling `<main>`) is the element's scroller, and is left alone.
|
|
34
41
|
*/
|
|
35
42
|
|
|
43
|
+
import { measureHeaderOverlay } from './sticky-top.js'
|
|
44
|
+
|
|
36
45
|
const warned = new WeakSet()
|
|
37
46
|
|
|
38
47
|
/** Every section wrapper the browser's renderer drew, noted in dev (`BlockRenderer.jsx`). */
|
|
@@ -125,10 +134,37 @@ export function unmovableAdvice(el) {
|
|
|
125
134
|
)
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
/**
|
|
138
|
+
* The box of a header over the opening section, when what it holds takes no room though some of
|
|
139
|
+
* it is drawn: a bar out of the flow — or null.
|
|
140
|
+
*
|
|
141
|
+
* @param {Document} doc
|
|
142
|
+
* @returns {Element|null}
|
|
143
|
+
*/
|
|
144
|
+
export function headerOutOfFlow(doc) {
|
|
145
|
+
const box = doc.querySelector?.('[data-uw-header="over"]')
|
|
146
|
+
if (!box || measureHeaderOverlay(doc) !== 0) return null
|
|
147
|
+
const drawn = [...box.querySelectorAll('*')].some((el) => el.getBoundingClientRect().height > 0)
|
|
148
|
+
return drawn ? box : null
|
|
149
|
+
}
|
|
150
|
+
|
|
128
151
|
/** Warn, once per element, about each sticky element on the page that cannot move. */
|
|
129
152
|
function checkPage(doc) {
|
|
153
|
+
const header = headerOutOfFlow(doc)
|
|
154
|
+
if (header && !warned.has(header)) {
|
|
155
|
+
warned.add(header)
|
|
156
|
+
console.warn(
|
|
157
|
+
'[uniweb] The header lies over the page\'s opening section, but what it holds takes no room: a bar ' +
|
|
158
|
+
'made `fixed` or `absolute` leaves the runtime nothing to measure, so the section is cleared by ' +
|
|
159
|
+
'0px and a link to a section lands under the bar. Keep the bar in the flow; the runtime lays ' +
|
|
160
|
+
'the header over the section and keeps it stuck.',
|
|
161
|
+
header,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
130
164
|
for (const el of doc.body.querySelectorAll('*')) {
|
|
131
165
|
if (warned.has(el) || getComputedStyle(el).position !== 'sticky') continue
|
|
166
|
+
const parent = boxParent(el)
|
|
167
|
+
if (parent && getComputedStyle(parent).position === 'sticky') continue
|
|
132
168
|
const clip = clippingAncestor(el)
|
|
133
169
|
if (clip) {
|
|
134
170
|
warned.add(el)
|
package/src/under-header.js
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* The opening section keeps its content clear of the header by `--uw-header-height`, which the
|
|
18
18
|
* runtime gives its wrapper: the header type's `height` on the first paint, which no prerender can
|
|
19
19
|
* measure, then what the browser measures. The header reads `getNextBlockInfo().underHeader` to
|
|
20
|
-
* paint itself see-through, and its box says `data-uw-header="over"` for a stylesheet.
|
|
20
|
+
* paint itself see-through, and its box says `data-uw-header="over"` for a stylesheet. Its type
|
|
21
|
+
* says `background: 'self'` and paints its own bar: otherwise the theme paints the header's section
|
|
22
|
+
* with its surface, a solid band over the opening section (measured 2026-11-06).
|
|
21
23
|
*/
|
|
22
24
|
|
|
23
25
|
/** The header's height, on the opening section's wrapper: what its content is padded by. */
|
package/src/start-page.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The page a Website starts with as active — recorded when the SSR init builds the graph, and put
|
|
3
|
-
* back before each render (`createPageRenderer`).
|
|
4
|
-
*
|
|
5
|
-
* ⭐ It is what the browser has on a URL that names no page. Core makes the site's index page (or
|
|
6
|
-
* its first page) active when it builds the Website, and neither the boot nor `PageRenderer` makes
|
|
7
|
-
* the not-found page active, since no route names it.
|
|
8
|
-
*
|
|
9
|
-
* ⛔ `renderPage` makes the page it renders active by its route, so for the not-found page it
|
|
10
|
-
* changes nothing, and that page rendered with whatever page the Website rendered last: on a
|
|
11
|
-
* Website reused across requests, the previous request's. Anything that adapts to the active page
|
|
12
|
-
* — a header, a language switcher, a docs sidebar — showed that page.
|
|
13
|
-
*
|
|
14
|
-
* A WeakMap rather than a slot on core's sealed Website: it is SSR state, and core is in every site.
|
|
15
|
-
*/
|
|
16
|
-
const startPages = new WeakMap()
|
|
17
|
-
|
|
18
|
-
/** Remember `website`'s active page as the one it starts with. Call it before anything renders. */
|
|
19
|
-
export function recordStartPage(website) {
|
|
20
|
-
if (website && !startPages.has(website)) startPages.set(website, website.activePage)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Make `website`'s start page active again. A Website the SSR init did not build is left alone. */
|
|
24
|
-
export function restoreStartPage(website) {
|
|
25
|
-
if (startPages.has(website)) website.activePage = startPages.get(website)
|
|
26
|
-
}
|