@uniweb/runtime 1.2.4 → 1.2.6
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 +18 -20
- package/dist/ssr.js.map +1 -1
- package/package.json +4 -6
- package/src/components/PageRenderer.jsx +1 -2
- package/src/hooks/useHeadMeta.js +1 -17
- 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 +19 -13
- package/src/start-page.js +26 -0
- package/src/head-meta.js +0 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/runtime",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "Minimal runtime for loading Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,16 +36,15 @@
|
|
|
36
36
|
"node": ">=22.12"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@uniweb/core": "~1.4.
|
|
40
|
-
"@uniweb/theming": "~1.2.1"
|
|
41
|
-
"@uniweb/projections": "~1.2.2"
|
|
39
|
+
"@uniweb/core": "~1.4.9",
|
|
40
|
+
"@uniweb/theming": "~1.2.1"
|
|
42
41
|
},
|
|
43
42
|
"devDependencies": {
|
|
44
43
|
"@vitejs/plugin-react": "^4.5.2",
|
|
45
44
|
"esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0 || ^0.28.0",
|
|
46
45
|
"vite": "^7.3.1",
|
|
47
46
|
"vitest": "^4.1.7",
|
|
48
|
-
"@uniweb/build": "1.10.
|
|
47
|
+
"@uniweb/build": "1.10.8"
|
|
49
48
|
},
|
|
50
49
|
"peerDependencies": {
|
|
51
50
|
"react": "^19.0.0",
|
|
@@ -55,7 +54,6 @@
|
|
|
55
54
|
"uniweb": {
|
|
56
55
|
"bundledDeps": [
|
|
57
56
|
"@uniweb/core",
|
|
58
|
-
"@uniweb/projections",
|
|
59
57
|
"@uniweb/theming"
|
|
60
58
|
]
|
|
61
59
|
},
|
|
@@ -11,7 +11,6 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
|
|
11
11
|
import BlockRenderer from './BlockRenderer.jsx'
|
|
12
12
|
import Layout from './Layout.jsx'
|
|
13
13
|
import { useHeadMeta } from '../hooks/useHeadMeta.js'
|
|
14
|
-
import { pageHeadMeta } from '../head-meta.js'
|
|
15
14
|
import { useSectionViews } from '../hooks/useSectionViews.js'
|
|
16
15
|
import { buildSectionOverrides } from '@uniweb/theming'
|
|
17
16
|
import { carryQuery } from '@uniweb/core/resolve-route'
|
|
@@ -183,7 +182,7 @@ export default function PageRenderer() {
|
|
|
183
182
|
}
|
|
184
183
|
|
|
185
184
|
// Head metadata — compute for all cases (redirect pages get a fallback)
|
|
186
|
-
const headMeta =
|
|
185
|
+
const headMeta = page?.getHeadMeta?.() || {
|
|
187
186
|
title: isNotFound ? 'Page Not Found' : (page?.title || 'Website'),
|
|
188
187
|
description: page?.description || ''
|
|
189
188
|
}
|
package/src/hooks/useHeadMeta.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Updates meta tags when page changes in SPA navigation.
|
|
6
6
|
*
|
|
7
7
|
* ⭐ The browser twin of `injectPageContent`'s head tags, from the same source
|
|
8
|
-
* (`
|
|
8
|
+
* (`Page#getHeadMeta`), so a crawler that runs the page's script reads what one that
|
|
9
9
|
* does not read. These tags are the framework's: each is set when the page has a value
|
|
10
10
|
* and removed when it has none — as it must be, or a tag of the page before survives a
|
|
11
11
|
* navigation. ⛔ Until 2026-10-25 this removed the canonical of every page that did not
|
|
@@ -116,20 +116,6 @@ function setAlternates(alternates) {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
/**
|
|
120
|
-
* The link to the page's markdown, replacing whatever the document carried
|
|
121
|
-
* @param {string|null} href
|
|
122
|
-
*/
|
|
123
|
-
function setMarkdownLink(href) {
|
|
124
|
-
for (const link of document.querySelectorAll('link[rel="alternate"][type="text/markdown"]')) link.remove()
|
|
125
|
-
if (!href) return
|
|
126
|
-
const link = document.createElement('link')
|
|
127
|
-
link.setAttribute('rel', 'alternate')
|
|
128
|
-
link.setAttribute('type', 'text/markdown')
|
|
129
|
-
link.setAttribute('href', href)
|
|
130
|
-
document.head.appendChild(link)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
119
|
/**
|
|
134
120
|
* useHeadMeta hook
|
|
135
121
|
*
|
|
@@ -203,7 +189,6 @@ export function useHeadMeta(meta, options = {}) {
|
|
|
203
189
|
}
|
|
204
190
|
|
|
205
191
|
setAlternates(meta.alternates)
|
|
206
|
-
setMarkdownLink(meta.markdown || null)
|
|
207
192
|
|
|
208
193
|
// Cleanup function - reset to defaults on unmount
|
|
209
194
|
return () => {
|
|
@@ -224,7 +209,6 @@ export function useHeadMeta(meta, options = {}) {
|
|
|
224
209
|
meta?.og?.siteName,
|
|
225
210
|
meta?.twitter?.site,
|
|
226
211
|
JSON.stringify(meta?.alternates || []),
|
|
227
|
-
meta?.markdown,
|
|
228
212
|
siteName
|
|
229
213
|
])
|
|
230
214
|
}
|
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,7 +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 {
|
|
40
|
+
import { recordStartPage } from './start-page.js'
|
|
41
41
|
import { escapeHtml } from '@uniweb/core/page-shell'
|
|
42
42
|
|
|
43
43
|
// Re-export L2 helpers so the public `@uniweb/runtime/ssr` surface
|
|
@@ -361,6 +361,8 @@ export function initPrerender(content, foundation, extensionsOrOptions, maybeOpt
|
|
|
361
361
|
// Uniweb constructor wires foundation, capabilities, layoutMeta, handlers,
|
|
362
362
|
// and extensions at construction time — see `@uniweb/core`'s src/uniweb.js.
|
|
363
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)
|
|
364
366
|
|
|
365
367
|
// Set base path from site config for subdirectory deployments
|
|
366
368
|
if (content.config?.base && uniweb.activeWebsite?.setBasePath) {
|
|
@@ -399,7 +401,11 @@ export function initPrerender(content, foundation, extensionsOrOptions, maybeOpt
|
|
|
399
401
|
|
|
400
402
|
// Register SSR-safe routing so useRouting()/useActiveRoute() work during prerender.
|
|
401
403
|
// renderPage() calls website.setActivePage() before rendering each page,
|
|
402
|
-
// 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.
|
|
403
409
|
const website = uniweb.activeWebsite
|
|
404
410
|
uniweb.routingComponents = {
|
|
405
411
|
useLocation: () => {
|
|
@@ -525,7 +531,11 @@ export function classifyRenderError(err) {
|
|
|
525
531
|
* @returns {{ renderedContent: string, sectionOverrideCSS: string } | { error: { type: string, message: string } }}
|
|
526
532
|
*/
|
|
527
533
|
export function renderPage(page, website) {
|
|
528
|
-
|
|
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)
|
|
529
539
|
|
|
530
540
|
// A page that claims content but yields no blocks has not been loaded — it is
|
|
531
541
|
// not an empty page. `Page#bodyBlocks` returns [] when its sections are absent
|
|
@@ -585,8 +595,7 @@ export { escapeHtml }
|
|
|
585
595
|
* Common operations shared by both build and cloud:
|
|
586
596
|
* - Replace #root div with rendered HTML
|
|
587
597
|
* - Update page title
|
|
588
|
-
* - Write the page's head tags — description, canonical, hreflang, Open Graph
|
|
589
|
-
* markdown (`writeHeadMeta`)
|
|
598
|
+
* - Write the page's head tags — description, canonical, hreflang, Open Graph (`writeHeadMeta`)
|
|
590
599
|
* - Inject section override CSS
|
|
591
600
|
* - Inject the pre-paint appearance script
|
|
592
601
|
*
|
|
@@ -678,10 +687,9 @@ export function injectPageContent(html, renderedContent, page, options = {}) {
|
|
|
678
687
|
)
|
|
679
688
|
}
|
|
680
689
|
|
|
681
|
-
// ⭐ THE PAGE'S HEAD TAGS — description, canonical, hreflang, Open Graph, Twitter
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
// read the page's own.
|
|
690
|
+
// ⭐ THE PAGE'S HEAD TAGS — description, canonical, hreflang, Open Graph, Twitter — from the
|
|
691
|
+
// page's own head metadata (`Page#getHeadMeta`, which the browser writes the same tags from in
|
|
692
|
+
// `useHeadMeta`), so crawlers and social unfurlers, which run no script, read the page's own.
|
|
685
693
|
// ⛔ Until 2026-10-25 these were APPENDED, after the static build had written the HOME page's
|
|
686
694
|
// into the one shell every page is rendered into: each page named the home page its canonical
|
|
687
695
|
// and carried two og:title and two og:url, the home page's first.
|
|
@@ -701,8 +709,8 @@ export function injectPageContent(html, renderedContent, page, options = {}) {
|
|
|
701
709
|
* @returns {string}
|
|
702
710
|
*/
|
|
703
711
|
function writeHeadMeta(html, page) {
|
|
704
|
-
// A Page states its own (`
|
|
705
|
-
const meta = typeof page?.getHeadMeta === 'function' ?
|
|
712
|
+
// A Page states its own (`getHeadMeta`); plain page data, which callers may hand in, its fields.
|
|
713
|
+
const meta = typeof page?.getHeadMeta === 'function' ? page.getHeadMeta() : { description: page?.description || null }
|
|
706
714
|
const end = html.indexOf('</head>')
|
|
707
715
|
if (!meta || end === -1) return html
|
|
708
716
|
const og = meta.og || {}
|
|
@@ -736,8 +744,6 @@ function writeHeadMeta(html, page) {
|
|
|
736
744
|
for (const { hreflang, url } of meta.alternates || []) {
|
|
737
745
|
written.push(`<link rel="alternate" hreflang="${escapeHtml(hreflang)}" href="${escapeHtml(url)}">`)
|
|
738
746
|
}
|
|
739
|
-
head = head.replace(/<link\b(?=[^>]*\brel=["']alternate["'])(?=[^>]*\btype=["']text\/markdown["'])[^>]*>\s*/gi, '')
|
|
740
|
-
if (meta.markdown) written.push(`<link rel="alternate" type="text/markdown" href="${escapeHtml(meta.markdown)}">`)
|
|
741
747
|
return `${head}${written.join('\n')}\n${html.slice(end)}`
|
|
742
748
|
}
|
|
743
749
|
|
|
@@ -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
|
+
}
|
package/src/head-meta.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A page's head metadata, as both of its writers state it: the prerender (`injectPageContent`)
|
|
3
|
-
* and the browser (`useHeadMeta`).
|
|
4
|
-
*
|
|
5
|
-
* ⭐ `Page#getHeadMeta`'s, plus the address of the page's markdown (`markdown`), which the head
|
|
6
|
-
* links with `<link rel="alternate" type="text/markdown">` so an agent that lands on the HTML finds
|
|
7
|
-
* the document. The address is the one the site index gives the page, for the pages a markdown
|
|
8
|
-
* file is written for (`pageMarkdownUrls`, `@uniweb/projections`). It is added here rather than
|
|
9
|
-
* in core because core cannot depend on the package whose rules decide it.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { pageMarkdownUrls } from '@uniweb/projections/markdown-urls'
|
|
13
|
-
|
|
14
|
-
// A site's markdown addresses, worked out once for its content. `rebuild({ content })` gives the
|
|
15
|
-
// Website a new `pages` array, so that array is the key and a rebuilt site is worked out again.
|
|
16
|
-
const urlsOfPages = new WeakMap()
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {Object} page - a Page
|
|
20
|
-
* @returns {Object} `page.getHeadMeta()`, and `markdown`: the URL of the page's markdown, or null —
|
|
21
|
-
* for a page the index leaves out and the not-found page, and for a record's page made on
|
|
22
|
-
* request (`uniweb dev`, a host), which is not among the content's pages. A static build's
|
|
23
|
-
* record pages are: its prerender writes them into the content each page boots with.
|
|
24
|
-
*/
|
|
25
|
-
export function pageHeadMeta(page) {
|
|
26
|
-
return { ...page.getHeadMeta(), markdown: markdownUrlOf(page) }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function markdownUrlOf(page) {
|
|
30
|
-
const website = page?.website
|
|
31
|
-
if (!website) return null
|
|
32
|
-
let urls = urlsOfPages.get(website.pages)
|
|
33
|
-
if (!urls) {
|
|
34
|
-
urls = pageMarkdownUrls({ pages: website.getContentPages(), config: website.config })
|
|
35
|
-
urlsOfPages.set(website.pages, urls)
|
|
36
|
-
}
|
|
37
|
-
return urls.get(page.route) ?? null
|
|
38
|
-
}
|