@uniweb/runtime 1.2.5 → 1.2.7
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 +19 -4
- package/dist/ssr.js.map +1 -1
- package/package.json +3 -3
- package/src/load-page-data.js +9 -4
- package/src/page-renderer.js +43 -15
- package/src/resolve-page.js +19 -0
- package/src/ssr-renderer.js +13 -2
- package/src/start-page.js +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/runtime",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
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.
|
|
39
|
+
"@uniweb/core": "~1.4.10",
|
|
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.10.
|
|
47
|
+
"@uniweb/build": "1.10.8"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": "^19.0.0",
|
package/src/load-page-data.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* @module
|
|
24
24
|
*/
|
|
25
25
|
import DataStore, { deriveCacheKey } from '@uniweb/core/datastore'
|
|
26
|
-
import {
|
|
26
|
+
import { resolveRequestedPage } from './resolve-page.js'
|
|
27
27
|
import { getComponentMeta } from './prepare-props.js'
|
|
28
28
|
import { createDefaultFetcher } from './default-fetcher.js'
|
|
29
29
|
import { requireTransport } from './require-transport.js'
|
|
@@ -53,7 +53,9 @@ function blocksOf(page) {
|
|
|
53
53
|
*
|
|
54
54
|
* @param {Object} options
|
|
55
55
|
* @param {Object} options.website - the initialized Website the render will use (`initPrerender`)
|
|
56
|
-
* @param {string|Object} options.route - the route, or an already-resolved Page
|
|
56
|
+
* @param {string|Object} options.route - the route, or an already-resolved Page. A route that names
|
|
57
|
+
* no page, or a page served from elsewhere (`rewrite:`), asks for the site's not-found page's
|
|
58
|
+
* data, the page its render draws.
|
|
57
59
|
* @param {Function} options.fetch - the transport, per request. Required: a missing one is refused,
|
|
58
60
|
* never replaced by the ambient `fetch` (`require-transport.js`)
|
|
59
61
|
* @param {boolean} [options.dev]
|
|
@@ -93,8 +95,11 @@ export async function loadPageData({ website, route, fetch, dev = false, locale
|
|
|
93
95
|
// through the global, and a host holding several graphs in one isolate has it naming the newest
|
|
94
96
|
// (`Website#makeCurrent`).
|
|
95
97
|
website.makeCurrent()
|
|
96
|
-
const
|
|
97
|
-
//
|
|
98
|
+
const named = typeof route === 'string' ? resolveRequestedPage(website, route) : route
|
|
99
|
+
// ⭐ A route that names no page, or a page served from elsewhere (`resolveRequestedPage`), renders
|
|
100
|
+
// the site's not-found page (`createPageRenderer`), so it is that page's data the render reads.
|
|
101
|
+
// With no such page: a genuine 404, the caller's to turn into one, and nothing to fetch.
|
|
102
|
+
const resolved = named ?? (typeof route === 'string' ? website.getNotFoundPage() : null)
|
|
98
103
|
if (!resolved) return []
|
|
99
104
|
// ⭐ The same promotion the render applies: a content-less folder renders its designated child,
|
|
100
105
|
// and it is that child's data the page needs.
|
package/src/page-renderer.js
CHANGED
|
@@ -33,10 +33,12 @@
|
|
|
33
33
|
* - **Anything build-only.** `injectBuildData` stays in the build lane; it is the
|
|
34
34
|
* other half of the head seam and has its own parity guard.
|
|
35
35
|
*/
|
|
36
|
-
import {
|
|
36
|
+
import { renderPage, classifyRenderError, injectPageContent } from './ssr-renderer.js'
|
|
37
|
+
import { resolveRequestedPage } from './resolve-page.js'
|
|
37
38
|
import { hydrateDataStore } from './wire-foundation.js'
|
|
38
39
|
import { loadPageData } from './load-page-data.js'
|
|
39
40
|
import { requireTransport } from './require-transport.js'
|
|
41
|
+
import { restoreStartPage } from './start-page.js'
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* A renderer bound to one initialized Website and one shell.
|
|
@@ -64,7 +66,11 @@ export function createPageRenderer({ website, shell }) {
|
|
|
64
66
|
*
|
|
65
67
|
* @param {string|Object} target - a route (`/blog/1`, resolved through the same
|
|
66
68
|
* matcher the browser uses, so a dynamic route works) or an already-resolved
|
|
67
|
-
* Page, which the build lane already holds from its own loop.
|
|
69
|
+
* Page, which the build lane already holds from its own loop. A route that names
|
|
70
|
+
* no page renders the site's not-found page (`website.getNotFoundPage()`), as the
|
|
71
|
+
* browser does for it, and so does a route naming a page served from elsewhere
|
|
72
|
+
* (`rewrite:`): a request for it that reaches a renderer was not proxied, and the
|
|
73
|
+
* browser's first load of it draws the not-found page too.
|
|
68
74
|
* @param {Object} [options]
|
|
69
75
|
* @param {Object} [options.inject] - extra options forwarded to `injectPageContent`
|
|
70
76
|
* @param {Array<{config: Object, data: *, meta?: Object}>} [options.fetchedData] - the entries
|
|
@@ -76,12 +82,16 @@ export function createPageRenderer({ website, shell }) {
|
|
|
76
82
|
* @returns {{ outcome: 'rendered'|'notFound'|'failed', html: string|null,
|
|
77
83
|
* page: Object|null, error: {type: string, message: string}|null }}
|
|
78
84
|
* `notFound` means the URL names nothing, and `html` says whether there is a page to
|
|
79
|
-
* send for it
|
|
80
|
-
*
|
|
81
|
-
* - `page` set, `html` set — the
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
+
* send for it. Send `html` with your not-found status when it is set, and the shell
|
|
86
|
+
* when it is not:
|
|
87
|
+
* - `page` set, `html` set — the site's not-found page, for a route that names no page
|
|
88
|
+
* or a page served from elsewhere; or a page whose record does not exist, rendered in
|
|
89
|
+
* its own not-found state.
|
|
90
|
+
* - `page: null`, `html: null` — no route matched, or it names a page served from
|
|
91
|
+
* elsewhere, and the site has no not-found page. The browser shows its default
|
|
92
|
+
* not-found message.
|
|
93
|
+
* - `page` set, `html: null`, `error` set — that page broke while rendering; `error`
|
|
94
|
+
* says what broke.
|
|
85
95
|
*/
|
|
86
96
|
function render(target, { inject = {}, fetchedData } = {}) {
|
|
87
97
|
if (fetchedData !== undefined && !Array.isArray(fetchedData)) {
|
|
@@ -101,11 +111,19 @@ export function createPageRenderer({ website, shell }) {
|
|
|
101
111
|
website.forgetData()
|
|
102
112
|
hydrateDataStore(website, fetchedData)
|
|
103
113
|
}
|
|
104
|
-
const
|
|
114
|
+
const named = typeof target === 'string' ? resolveRequestedPage(website, target) : target
|
|
115
|
+
// ⭐ A ROUTE THAT NAMES NO PAGE RENDERS THE SITE'S NOT-FOUND PAGE, as the browser does for
|
|
116
|
+
// it (`PageRenderer.jsx`), and the data step asks for that page's data (`loadPageData`). No
|
|
117
|
+
// route names it (`/404` is never routable), so this is the only way a route reaches it.
|
|
118
|
+
// A route naming a page served from elsewhere is answered the same way: the host did not
|
|
119
|
+
// proxy it (`resolveRequestedPage`).
|
|
120
|
+
// ⛔ Until 2026-10-26 this answered `notFound` with nothing to send, and a host that wanted
|
|
121
|
+
// the site's page had to fetch its data and render it as a Page itself.
|
|
122
|
+
const resolved = named ?? (typeof target === 'string' ? website.getNotFoundPage() : null)
|
|
105
123
|
|
|
106
|
-
// ⛔ Not an error: nothing matched
|
|
107
|
-
// turn into one
|
|
108
|
-
// status. Returning `failed` here would make those indistinguishable.
|
|
124
|
+
// ⛔ Not an error: nothing matched and the site has no not-found page — a genuine 404,
|
|
125
|
+
// and the caller's to turn into one: a build skips it, an isolate sends the shell with a
|
|
126
|
+
// 404 status. Returning `failed` here would make those indistinguishable.
|
|
109
127
|
if (!resolved) return { outcome: 'notFound', html: null, page: null, error: null }
|
|
110
128
|
|
|
111
129
|
// ⭐ **THE INDEX-CHILD PROMOTION APPLIES TO BOTH TARGET FORMS** — ruled
|
|
@@ -125,11 +143,21 @@ export function createPageRenderer({ website, shell }) {
|
|
|
125
143
|
// (`Website#_createDynamicPage`), never on a failed or missing fetch. It renders the
|
|
126
144
|
// page's own not-found state — a record miss is not a route miss — and the outcome
|
|
127
145
|
// says `notFound` all the same, so a caller keying its status on the outcome sends a
|
|
128
|
-
// not-found response without reading anything new.
|
|
129
|
-
//
|
|
146
|
+
// not-found response without reading anything new. The site's not-found page, rendered
|
|
147
|
+
// for a route that names no page, is `notFound` the same way. Known before rendering, so
|
|
148
|
+
// it holds even when that render breaks. ⛔ Until 2026-09-30 this was `rendered`, and a
|
|
130
149
|
// host that answered by the outcome sent 200 for a URL naming no record.
|
|
131
|
-
const missing = page?.notFound === true
|
|
150
|
+
const missing = !named || page?.notFound === true
|
|
132
151
|
|
|
152
|
+
// ⭐ EVERY RENDER STARTS FROM THE ACTIVE PAGE THE WEBSITE STARTED WITH (`start-page.js`),
|
|
153
|
+
// then makes active the page the route names, as the browser's boot does (`index.jsx`).
|
|
154
|
+
// `renderPage` then makes the page it renders active by its route, and the not-found page
|
|
155
|
+
// has none: it renders with the page a route served from elsewhere names, as in the browser,
|
|
156
|
+
// or with the start page. ⛔ Until 2026-10-26 it rendered with whatever page this Website
|
|
157
|
+
// rendered last, so on a Website reused across requests its header could show the previous
|
|
158
|
+
// request's page.
|
|
159
|
+
restoreStartPage(website)
|
|
160
|
+
if (typeof target === 'string') website.setActivePage(target)
|
|
133
161
|
let result
|
|
134
162
|
try {
|
|
135
163
|
result = renderPage(page, website)
|
package/src/resolve-page.js
CHANGED
|
@@ -39,3 +39,22 @@
|
|
|
39
39
|
export function resolvePage(website, route) {
|
|
40
40
|
return website.getPage(route)
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The page a request for `route` renders, for the doors that render the page a visit starts on
|
|
45
|
+
* (`createPageRenderer`, and its data step `loadPageData`): `resolvePage`, except a page served from
|
|
46
|
+
* elsewhere.
|
|
47
|
+
*
|
|
48
|
+
* ⭐ A page with `rewrite:` is the host's to proxy, before the app loads. A request for one that
|
|
49
|
+
* reaches a renderer was not proxied, so the doors answer it as the browser's first load of it does
|
|
50
|
+
* (`PageRenderer.jsx`): with the site's not-found page, as a static build emits no file there.
|
|
51
|
+
* ⛔ Until 2026-10-27 they rendered that page's own content as an ordinary page, and the browser
|
|
52
|
+
* replaced it with the not-found page at boot.
|
|
53
|
+
*
|
|
54
|
+
* @param {Website} website
|
|
55
|
+
* @param {string} route - The requested path
|
|
56
|
+
* @returns {Page|undefined} undefined when the route names no page, or a page served from elsewhere
|
|
57
|
+
*/
|
|
58
|
+
export function resolveRequestedPage(website, route) {
|
|
59
|
+
return website.resolveRoute(route).kind === 'rewrite' ? undefined : resolvePage(website, route)
|
|
60
|
+
}
|
package/src/ssr-renderer.js
CHANGED
|
@@ -37,6 +37,7 @@ import { layoutStickiness, sectionStickyProps, sectionScrollMargin, zoneProps }
|
|
|
37
37
|
import { DefaultLayout } from './default-layout.js'
|
|
38
38
|
import { renderAppearanceBootScript } from './appearance.js'
|
|
39
39
|
import { documentTitle } from './document-title.js'
|
|
40
|
+
import { recordStartPage } from './start-page.js'
|
|
40
41
|
import { escapeHtml } from '@uniweb/core/page-shell'
|
|
41
42
|
|
|
42
43
|
// Re-export L2 helpers so the public `@uniweb/runtime/ssr` surface
|
|
@@ -360,6 +361,8 @@ export function initPrerender(content, foundation, extensionsOrOptions, maybeOpt
|
|
|
360
361
|
// Uniweb constructor wires foundation, capabilities, layoutMeta, handlers,
|
|
361
362
|
// and extensions at construction time — see `@uniweb/core`'s src/uniweb.js.
|
|
362
363
|
const uniweb = createUniweb(content, foundation, extensions)
|
|
364
|
+
// Before anything renders: the active page every render starts from (`start-page.js`).
|
|
365
|
+
recordStartPage(uniweb.activeWebsite)
|
|
363
366
|
|
|
364
367
|
// Set base path from site config for subdirectory deployments
|
|
365
368
|
if (content.config?.base && uniweb.activeWebsite?.setBasePath) {
|
|
@@ -398,7 +401,11 @@ export function initPrerender(content, foundation, extensionsOrOptions, maybeOpt
|
|
|
398
401
|
|
|
399
402
|
// Register SSR-safe routing so useRouting()/useActiveRoute() work during prerender.
|
|
400
403
|
// renderPage() calls website.setActivePage() before rendering each page,
|
|
401
|
-
// so activePage.route
|
|
404
|
+
// so activePage.route reflects the page being rendered — except the site's
|
|
405
|
+
// not-found page, which no route names. Through `createPageRenderer` it renders
|
|
406
|
+
// with the page a route served from elsewhere names, else the page the Website
|
|
407
|
+
// starts with (`start-page.js`); through `generate404Html`, with the page
|
|
408
|
+
// rendered last.
|
|
402
409
|
const website = uniweb.activeWebsite
|
|
403
410
|
uniweb.routingComponents = {
|
|
404
411
|
useLocation: () => {
|
|
@@ -524,7 +531,11 @@ export function classifyRenderError(err) {
|
|
|
524
531
|
* @returns {{ renderedContent: string, sectionOverrideCSS: string } | { error: { type: string, message: string } }}
|
|
525
532
|
*/
|
|
526
533
|
export function renderPage(page, website) {
|
|
527
|
-
|
|
534
|
+
// ⛔ The site's not-found page is never made active by its route. No route names it, and on a
|
|
535
|
+
// site whose parametric page matches `/404` (a root `/:slug`) that route names THAT page, which
|
|
536
|
+
// then rendered as the active one. It renders with the active page it finds: the Website's
|
|
537
|
+
// first, through `createPageRenderer` (`start-page.js`), as in the browser.
|
|
538
|
+
if (page !== website.getNotFoundPage()) website.setActivePage(page.route)
|
|
528
539
|
|
|
529
540
|
// A page that claims content but yields no blocks has not been loaded — it is
|
|
530
541
|
// not an empty page. `Page#bodyBlocks` returns [] when its sections are absent
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
}
|