@uniweb/runtime 1.2.3 → 1.2.4

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.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Minimal runtime for loading Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -36,15 +36,16 @@
36
36
  "node": ">=22.12"
37
37
  },
38
38
  "dependencies": {
39
- "@uniweb/core": "~1.4.7",
40
- "@uniweb/theming": "~1.2.1"
39
+ "@uniweb/core": "~1.4.8",
40
+ "@uniweb/theming": "~1.2.1",
41
+ "@uniweb/projections": "~1.2.2"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@vitejs/plugin-react": "^4.5.2",
44
45
  "esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0 || ^0.28.0",
45
46
  "vite": "^7.3.1",
46
47
  "vitest": "^4.1.7",
47
- "@uniweb/build": "1.10.0"
48
+ "@uniweb/build": "1.10.5"
48
49
  },
49
50
  "peerDependencies": {
50
51
  "react": "^19.0.0",
@@ -54,6 +55,7 @@
54
55
  "uniweb": {
55
56
  "bundledDeps": [
56
57
  "@uniweb/core",
58
+ "@uniweb/projections",
57
59
  "@uniweb/theming"
58
60
  ]
59
61
  },
@@ -11,6 +11,7 @@ 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'
14
15
  import { useSectionViews } from '../hooks/useSectionViews.js'
15
16
  import { buildSectionOverrides } from '@uniweb/theming'
16
17
  import { carryQuery } from '@uniweb/core/resolve-route'
@@ -182,7 +183,7 @@ export default function PageRenderer() {
182
183
  }
183
184
 
184
185
  // Head metadata — compute for all cases (redirect pages get a fallback)
185
- const headMeta = page?.getHeadMeta?.() || {
186
+ const headMeta = (typeof page?.getHeadMeta === 'function' && pageHeadMeta(page)) || {
186
187
  title: isNotFound ? 'Page Not Found' : (page?.title || 'Website'),
187
188
  description: page?.description || ''
188
189
  }
@@ -0,0 +1,38 @@
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
+ }
@@ -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
- * (`Page#getHeadMeta`), so a crawler that runs the page's script reads what one that
8
+ * (`pageHeadMeta`), 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,6 +116,20 @@ 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
+
119
133
  /**
120
134
  * useHeadMeta hook
121
135
  *
@@ -189,6 +203,7 @@ export function useHeadMeta(meta, options = {}) {
189
203
  }
190
204
 
191
205
  setAlternates(meta.alternates)
206
+ setMarkdownLink(meta.markdown || null)
192
207
 
193
208
  // Cleanup function - reset to defaults on unmount
194
209
  return () => {
@@ -209,6 +224,7 @@ export function useHeadMeta(meta, options = {}) {
209
224
  meta?.og?.siteName,
210
225
  meta?.twitter?.site,
211
226
  JSON.stringify(meta?.alternates || []),
227
+ meta?.markdown,
212
228
  siteName
213
229
  ])
214
230
  }
@@ -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 { pageHeadMeta } from './head-meta.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
@@ -584,7 +585,8 @@ export { escapeHtml }
584
585
  * Common operations shared by both build and cloud:
585
586
  * - Replace #root div with rendered HTML
586
587
  * - Update page title
587
- * - Write the page's head tags — description, canonical, hreflang, Open Graph (`writeHeadMeta`)
588
+ * - Write the page's head tags — description, canonical, hreflang, Open Graph, the link to its
589
+ * markdown (`writeHeadMeta`)
588
590
  * - Inject section override CSS
589
591
  * - Inject the pre-paint appearance script
590
592
  *
@@ -676,9 +678,10 @@ export function injectPageContent(html, renderedContent, page, options = {}) {
676
678
  )
677
679
  }
678
680
 
679
- // ⭐ THE PAGE'S HEAD TAGS — description, canonical, hreflang, Open Graph, Twitter — from the
680
- // page's own head metadata (`Page#getHeadMeta`, which the browser writes the same tags from in
681
- // `useHeadMeta`), so crawlers and social unfurlers, which run no script, read the page's own.
681
+ // ⭐ THE PAGE'S HEAD TAGS — description, canonical, hreflang, Open Graph, Twitter, the link to its
682
+ // markdown — from the page's own head metadata (`pageHeadMeta`, which the browser writes the same
683
+ // tags from in `useHeadMeta`), so crawlers, social unfurlers and agents, which run no script,
684
+ // read the page's own.
682
685
  // ⛔ Until 2026-10-25 these were APPENDED, after the static build had written the HOME page's
683
686
  // into the one shell every page is rendered into: each page named the home page its canonical
684
687
  // and carried two og:title and two og:url, the home page's first.
@@ -698,8 +701,8 @@ export function injectPageContent(html, renderedContent, page, options = {}) {
698
701
  * @returns {string}
699
702
  */
700
703
  function writeHeadMeta(html, page) {
701
- // A Page states its own (`getHeadMeta`); plain page data, which callers may hand in, its fields.
702
- const meta = typeof page?.getHeadMeta === 'function' ? page.getHeadMeta() : { description: page?.description || null }
704
+ // A Page states its own (`pageHeadMeta`); plain page data, which callers may hand in, its fields.
705
+ const meta = typeof page?.getHeadMeta === 'function' ? pageHeadMeta(page) : { description: page?.description || null }
703
706
  const end = html.indexOf('</head>')
704
707
  if (!meta || end === -1) return html
705
708
  const og = meta.og || {}
@@ -733,6 +736,8 @@ function writeHeadMeta(html, page) {
733
736
  for (const { hreflang, url } of meta.alternates || []) {
734
737
  written.push(`<link rel="alternate" hreflang="${escapeHtml(hreflang)}" href="${escapeHtml(url)}">`)
735
738
  }
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)}">`)
736
741
  return `${head}${written.join('\n')}\n${html.slice(end)}`
737
742
  }
738
743