@uniweb/runtime 1.2.9 → 1.2.11

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.9",
3
+ "version": "1.2.11",
4
4
  "description": "Minimal runtime for loading Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,23 @@
1
+ import { FONT_LINKS_MARKER } from '@uniweb/theming'
2
+
3
+ /**
4
+ * Give a page the theme's font links when it arrived without them.
5
+ *
6
+ * A built page carries them in its `<head>` (`@uniweb/build`), and so does a prerendered
7
+ * one (`injectPageContent` in `ssr-renderer.js`). A host that serves the shell alone and
8
+ * leaves every page to the browser carries neither. `ensureThemeCss` computes the links
9
+ * onto the graph for such a lane, but nothing put them on the page: the browser added the
10
+ * theme's CSS (`ThemeProvider`) and never its fonts, so every family the theme imports
11
+ * fell back to a system face. This is the browser's half of the prerender's font block,
12
+ * deduped on the same marker, so a page that already has them is left alone.
13
+ *
14
+ * @param {object} themeData - `website.themeData`, with the `links` block `@uniweb/theming` generated
15
+ * @param {Document|null} [doc]
16
+ * @returns {boolean} whether it added them
17
+ */
18
+ export function injectFontLinks(themeData, doc = typeof document === 'undefined' ? null : document) {
19
+ const head = doc?.head
20
+ if (!head || !themeData?.links || head.innerHTML.includes(FONT_LINKS_MARKER)) return false
21
+ head.insertAdjacentHTML('beforeend', `${FONT_LINKS_MARKER}\n${themeData.links}`)
22
+ return true
23
+ }
@@ -110,7 +110,8 @@ export function inAppLink(anchor) {
110
110
 
111
111
  /**
112
112
  * Whether a page of `website` can have something to fetch before it is shown: content not here
113
- * yet, on a site whose content is split, or data, which a site declaring queries reads.
113
+ * yet, on a site whose content is split, or data a page declares where it can be seen
114
+ * (`readsData`).
114
115
  */
115
116
  function hasPagesToFetch(website) {
116
117
  if (readsData(website)) return true
@@ -2,9 +2,9 @@
2
2
  * Prefetch on intent: a link's page is fetched while the reader is about to follow it.
3
3
  *
4
4
  * Only a site with something to fetch before a page is shown loads this module
5
- * (`useLinkInterceptor`): split content not here yet, or data, which a site declaring queries
6
- * reads. A site with neither never downloads it, so the runtime every site carries pays only for
7
- * the decision.
5
+ * (`useLinkInterceptor`): split content not here yet, or data a page declares where it can be seen
6
+ * (`readsData`, `navigation.js`). A site with neither never downloads it, so the runtime every site
7
+ * carries pays only for the decision.
8
8
  *
9
9
  * A pointer resting on a link, or focus staying on one, for INTENT_DELAY fetches what the page
10
10
  * the link goes to will read: its split content, then its sections' data, whatever is not held
package/src/navigation.js CHANGED
@@ -80,29 +80,47 @@ function isReady(page) {
80
80
  }
81
81
 
82
82
  /**
83
- * Whether the site declares a query. Data comes from a named query (`config.queries`), so a site
84
- * that declares none reads none, and its pages are not walked for it.
83
+ * Whether a fetch can be seen reaching `page` without building its sections: the site declares
84
+ * queries (`config.queries`), or a fetch sits on the page, its parent page or the site, or the page
85
+ * is parametric and asks for its own record. A section's own `fetch:` is read only once its block is
86
+ * built, so a page whose data is declared on its sections alone is not seen here.
87
+ *
88
+ * ⛔ `config.queries` alone is not the test. A hosted site's schema-less query is not projected
89
+ * there: its fetch carries the file's `path` (measured 2026-10-27 on a site's blog: no
90
+ * `config.queries`, the page's fetch `{ query, path, as }`). A gate on it skipped that blog's data.
91
+ *
92
+ * @param {Object} page
93
+ * @returns {boolean}
94
+ */
95
+ function declaresData(page) {
96
+ const config = page.website?.config
97
+ const queries = config?.queries
98
+ if (queries && typeof queries === 'object' && Object.keys(queries).length > 0) return true
99
+ return !!(page.fetch || page.parent?.fetch || config?.fetch || page.dynamicContext)
100
+ }
101
+
102
+ /**
103
+ * Whether a page of `website` declares data where it can be seen without building sections (see
104
+ * `declaresData`): what decides whether prefetching on intent is worth its module.
85
105
  *
86
106
  * @param {Object|null|undefined} website
87
107
  * @returns {boolean}
88
108
  */
89
109
  export function readsData(website) {
90
- const queries = website?.config?.queries
91
- return !!queries && typeof queries === 'object' && Object.keys(queries).length > 0
110
+ return !!website?.pages?.some((page) => declaresData(page))
92
111
  }
93
112
 
94
113
  /**
95
114
  * Ask what `page`'s render will read and the cache does not hold: off a parametric page, the
96
115
  * page's own record, which names it; and each block's data, by the same call the renderer makes
97
- * after mount (`EntityStore.fetch`).
116
+ * after mount (`EntityStore.fetch`). It builds the page's blocks, which its render is about to do.
98
117
  *
99
118
  * @param {Object} page - its content here
100
119
  * @returns {Promise|undefined} undefined when nothing was asked
101
120
  */
102
121
  function dataOf(page) {
103
- const website = page.website
104
- const store = website?.entityStore
105
- if (!store || !readsData(website)) return
122
+ const store = page.website?.entityStore
123
+ if (!store) return
106
124
  const asks = []
107
125
  if (page.dynamicContext) asks.push(store.fetchPageRecord(page))
108
126
  for (const block of blocksOf(page)) {
@@ -116,13 +134,19 @@ function dataOf(page) {
116
134
  * The wait for what `page`'s render reads and is not here yet: its split content, then, its
117
135
  * sections known, their data.
118
136
  *
137
+ * ⚖️ A navigation asks for the data of every page it goes to, since building the blocks is work its
138
+ * render does a moment later. A prefetch is a guess the reader may not follow, so it asks only where
139
+ * data can be seen reaching the page (`declaresData`), and leaves sections unbuilt elsewhere.
140
+ *
119
141
  * @param {Object|null} page
142
+ * @param {{ speculative?: boolean }} [options]
120
143
  * @returns {Promise|undefined} undefined when it is all here
121
144
  */
122
- function loadPage(page) {
145
+ function loadPage(page, { speculative = false } = {}) {
123
146
  if (!page) return
124
- if (!isReady(page)) return page.loadContent().then(() => dataOf(page))
125
- return dataOf(page)
147
+ const data = () => (speculative && !declaresData(page) ? undefined : dataOf(page))
148
+ if (!isReady(page)) return page.loadContent().then(data)
149
+ return data()
126
150
  }
127
151
 
128
152
  /** `wait`, or the end of HOLD_TIMEOUT, whichever comes first. */
@@ -152,7 +176,7 @@ export function prefetchContent(route) {
152
176
  * @returns {Promise|undefined} undefined when it is all here
153
177
  */
154
178
  export function prefetchPage(route) {
155
- return loadPage(pageAt(route))?.catch(() => {})
179
+ return loadPage(pageAt(route), { speculative: true })?.catch(() => {})
156
180
  }
157
181
 
158
182
  // The navigation the next swap belongs to. A later navigation supersedes one still waiting for
package/src/setup.js CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  import { ChildBlocks } from './components/PageRenderer.jsx'
32
32
  import Background from './components/Background.jsx'
33
33
  import { wireFoundationCapabilities, hydrateDataStore, ensureThemeCss } from './wire-foundation.js'
34
+ import { injectFontLinks } from './font-links.js'
34
35
  import { navigateInApp } from './navigation.js'
35
36
 
36
37
  // Re-export so existing consumers importing hydrateDataStore from
@@ -274,6 +275,9 @@ export function initUniweb({ content, foundation, extensions = [], routingCompon
274
275
  if (typeof document === 'undefined' || !document.getElementById('uniweb-theme')) {
275
276
  ensureThemeCss(uniweb, foundation)
276
277
  }
278
+ // And the theme's font links, which the CSS alone does not bring: a page that came
279
+ // with neither would otherwise set every imported family in a system face.
280
+ injectFontLinks(uniweb.activeWebsite?.themeData)
277
281
 
278
282
  uniweb.routingComponents = routingComponents
279
283
  uniweb.localeSwitch = localeSwitch