@iterant/site-runtime 3.7.0 → 3.8.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.
@@ -1,9 +1,11 @@
1
1
  ---
2
+ import { Font, fontData } from "astro:assets";
2
3
  import { getCollection, getEntry } from "astro:content";
3
4
  import type { AstroComponentFactory } from "astro/runtime/server/index.js";
4
5
  import RelatedLinks from "../components/RelatedLinks.astro";
5
6
  import { SEO, type PageType } from "../components/seo";
6
7
  import type { SeoJsonSchema } from "../components/seo-json";
8
+ import { fontCssVariable } from "../fonts/catalog";
7
9
  import type { HreflangAlternate } from "../lib/hreflang";
8
10
  import { DEFAULT_LOCALE, localeFromPath } from "../lib/locales";
9
11
  import { SITE_RUNTIME_VERSION } from "../version";
@@ -14,6 +16,7 @@ import {
14
16
  resolveAbsoluteUrl,
15
17
  resolveStructuredData,
16
18
  routePathFromPathname,
19
+ selectFontFamilies,
17
20
  type LayoutSiteConfig,
18
21
  type SiteShell,
19
22
  } from "./layout-core";
@@ -151,6 +154,41 @@ const shellId = shell ?? pageEntry?.data.shell;
151
154
  const linksEntry = await getEntry("links", "links");
152
155
  const linksSlice = linksEntry?.data.routes[routePath];
153
156
 
157
+ // Web fonts (3.8.0): a `@font-face` block per family the site names in
158
+ // SITE_CONFIG.fonts, and a preload link for each. Never the whole catalog:
159
+ // every family rendered here inlines its faces into EVERY page.
160
+ //
161
+ // `fontData` is Astro's own runtime view of the resolved font config, keyed by
162
+ // CSS variable, so it is what decides whether a family can render. The rule and
163
+ // the fallback live in selectFontFamilies, which is testable without a render.
164
+ const fonts = selectFontFamilies(siteConfig.fonts, (cssVariable) =>
165
+ Object.hasOwn(fontData, cssVariable),
166
+ );
167
+
168
+ // A named family nothing declared is a repo whose astro.config and site config
169
+ // disagree, which no gate catches and no render shows. Said once, in dev, where
170
+ // the author is looking.
171
+ if (import.meta.env.DEV && fonts.undeclared.length > 0) {
172
+ console.warn(
173
+ `[site-runtime] SITE_CONFIG.fonts names ${fonts.undeclared.join(", ")}, which astro.config did not declare. Pass the same list to iterantStarter({ fonts }).`,
174
+ );
175
+ }
176
+
177
+ // The cast is Astro's own type: once a repo configures fonts, `astro sync` narrows
178
+ // CssVariable to the exact literals in that config. Every variable here is one
179
+ // fontData carries, which is the same set, but the compiler cannot see that
180
+ // through a runtime string.
181
+ const fontVariables = fonts.families.map((family) =>
182
+ fontCssVariable(family),
183
+ ) as Array<import("astro:assets").CssVariable>;
184
+
185
+ // Preload the upright latin face of each named family and nothing else. Every
186
+ // face stays in the @font-face block, so the browser still fetches a slanted or
187
+ // extended-latin one the moment a glyph needs it. Preloading every face instead
188
+ // measured 8 files and 436K of blocking requests for two families, on a page
189
+ // that renders four words of plain latin text.
190
+ const fontPreload = fonts.named ? [{ style: "normal", subset: "latin" }] : false;
191
+
154
192
  // Version identity: the installed package version IS the site's runtime
155
193
  // version. `it-astro-starter-version` keeps emitting the same value while the
156
194
  // plugin loader and the platform's page indexer still read that name, and
@@ -176,6 +214,11 @@ const linksSlice = linksEntry?.data.routes[routePath];
176
214
  <meta name="generator" content={Astro.generator} />
177
215
  <meta name="it-site-runtime" content={SITE_RUNTIME_VERSION} />
178
216
  <meta name="it-astro-starter-version" content={SITE_RUNTIME_VERSION} />
217
+ {
218
+ fontVariables.map((cssVariable) => (
219
+ <Font cssVariable={cssVariable} preload={fontPreload} />
220
+ ))
221
+ }
179
222
  <SEO
180
223
  title={title}
181
224
  description={description}
@@ -1,4 +1,5 @@
1
1
  import type { PageType } from "../components/seo";
2
+ import { DEFAULT_FONT_FAMILIES, fontCssVariable } from "../fonts/catalog";
2
3
  import type { ContentProps } from "../lib/content-values";
3
4
 
4
5
  // The pure half of LayoutCore.astro: URL resolution, chrome lookup, and the
@@ -16,6 +17,14 @@ export interface LayoutSiteConfig {
16
17
  description: string;
17
18
  logo?: string;
18
19
  sameAs?: string[];
20
+ /**
21
+ * Font families this site uses, by catalog name (`["Fraunces", "Inter"]`).
22
+ * The head carries a `@font-face` block per family, so this is the list of
23
+ * families the site actually sets somewhere, not everything it may choose
24
+ * from. The repo passes the same array to `iterantStarter({ fonts })`; a
25
+ * family missing there has no CSS variable to render and is skipped.
26
+ */
27
+ fonts?: string[];
19
28
  }
20
29
 
21
30
  /**
@@ -141,3 +150,50 @@ export function resolveStructuredData(
141
150
  }
142
151
 
143
152
  export { LAYOUT_CONTRACT } from "./layout-contract";
153
+
154
+ /** What the head renders for web fonts, decided off the site's list. */
155
+ export interface FontSelection {
156
+ /** Families to render a `<Font>` for, in order, deduped. */
157
+ families: string[];
158
+ /** Whether the families are the site's own list rather than the fallback. */
159
+ named: boolean;
160
+ /** Families the site named that no font config declared. */
161
+ undeclared: string[];
162
+ }
163
+
164
+ /**
165
+ * The families the head renders, and whether to preload them.
166
+ *
167
+ * `isDeclared` is the authority, not the catalog: Astro's `<Font>` THROWS
168
+ * (`FontFamilyNotFound`) on a CSS variable no family registered, so a repo
169
+ * whose astro.config never got the site's list would render an error page
170
+ * rather than a missing face. Filtering through what Astro actually resolved
171
+ * makes that structurally impossible, and it also lets a diverged repo declare
172
+ * a family of its own and have it render.
173
+ *
174
+ * A site that names nothing falls back to the default set: a repo's globals.css
175
+ * is written against the CSS variables, so a stylesheet already saying
176
+ * `var(--font-fraunces)` would otherwise get the system stack, silently. The
177
+ * fallback is reported through `named`, because a page that has not said which
178
+ * families it uses must not spend a preload on each of eleven guesses.
179
+ */
180
+ export function selectFontFamilies(
181
+ fonts: readonly string[] | undefined,
182
+ isDeclared: (cssVariable: string) => boolean,
183
+ ): FontSelection {
184
+ const wanted = [...new Set(fonts ?? [])];
185
+ const declared = wanted.filter((family) =>
186
+ isDeclared(fontCssVariable(family)),
187
+ );
188
+ const undeclared = wanted.filter((family) => !declared.includes(family));
189
+ if (declared.length > 0) {
190
+ return { families: declared, named: true, undeclared };
191
+ }
192
+ return {
193
+ families: DEFAULT_FONT_FAMILIES.filter((family) =>
194
+ isDeclared(fontCssVariable(family)),
195
+ ),
196
+ named: false,
197
+ undeclared,
198
+ };
199
+ }