From f646b763bd393918d66e40c63b2fe088c5be6e20 Mon Sep 17 00:00:00 2001
From: Madraka <8898640+Madraka@users.noreply.github.com>
Date: Wed, 21 Oct 2026 18:06:59 +0300
Subject: [PATCH] perf(starter): send font preloads as a Link header for Early
Hints
Pages now send their type pairing's font preloads as a `Link` response
header as well as `` in the head. On a Cloudflare zone with
Early Hints on, the edge answers with a 103 carrying those links before the
page, so the fonts start loading while the Worker renders.
Co-Authored-By: Claude Opus 5.5
Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
---
.changeset/font-early-hints.md | 5 +++++
starters/blog/src/fonts.ts | 10 ++++++++++
starters/blog/src/pages/[...path].astro | 7 +++++++
3 files changed, 22 insertions(+)
create mode 100644 .changeset/font-early-hints.md
diff --git a/.changeset/font-early-hints.md b/.changeset/font-early-hints.md
new file mode 100644
index 00000000..a613bb66
--- /dev/null
+++ b/.changeset/font-early-hints.md
@@ -0,0 +1,5 @@
+---
+"@cloudhal/cli": patch
+---
+
+Blog starter: pages send their type pairing's font preloads as a `Link` header too, so a Cloudflare zone with Early Hints on starts loading the fonts with a 103 before the page arrives.
diff --git a/starters/blog/src/fonts.ts b/starters/blog/src/fonts.ts
index a48cca92..9d982122 100644
--- a/starters/blog/src/fonts.ts
+++ b/starters/blog/src/fonts.ts
@@ -19,3 +19,13 @@ export const PAIRING_PRELOADS: Readonly> = {
],
classic: ["/fonts/v1/source-serif-4-latin-wght-normal.woff2"],
};
+
+/**
+ * The same preloads as a `Link` response header. On a Cloudflare zone with Early Hints on, the
+ * edge sends it as a 103 before the page, so the fonts load while the Worker is still rendering.
+ */
+export function fontLinkHeader(hrefs: readonly string[]): string {
+ return hrefs
+ .map((h) => `<${h}>; rel=preload; as=font; type="font/woff2"; crossorigin`)
+ .join(", ");
+}
diff --git a/starters/blog/src/pages/[...path].astro b/starters/blog/src/pages/[...path].astro
index 990f859b..53216e29 100644
--- a/starters/blog/src/pages/[...path].astro
+++ b/starters/blog/src/pages/[...path].astro
@@ -51,6 +51,7 @@ import {
relatedEntries,
termAncestors,
} from "@cloudhal/astro-renderer/taxonomy";
+import { themeVariables } from "@cloudhal/astro-renderer/theme-settings";
import type { ContentEntry } from "@cloudhal/core-contracts";
/**
* One route for the whole graph (RFC-0002 §7 order): a Page answers at its `path`; an article at
@@ -73,8 +74,10 @@ import NotFound from "../components/NotFound.astro";
import PostTerms from "../components/PostTerms.astro";
import RelatedPosts from "../components/RelatedPosts.astro";
import Search from "../components/Search.astro";
+import { fontLinkHeader, PAIRING_PRELOADS } from "../fonts.ts";
import { iconSet } from "../icons.ts";
import Base from "../layouts/Base.astro";
+import { themeSettings } from "../settings.ts";
import {
ARCHIVE_PAGE_SIZE,
blocks,
@@ -354,6 +357,10 @@ const localized = localizeHead(head, site, {
});
const meta = fallback || notFound ? { ...localized, robots: "noindex,follow" } : localized;
Astro.response.headers.set("content-language", shownIn);
+// The pairing's fonts as a Link header too (Early Hints), not only as in .
+const pairing = themeVariables(site, themeSettings).attributes["data-ch-type-pairing"];
+const preloads = PAIRING_PRELOADS[pairing ?? ""] ?? [];
+if (preloads.length) Astro.response.headers.set("link", fontLinkHeader(preloads));
// The reader's notice (RFC-0033 §4): machine output until a person reviews it (or always, when
// the site asks), a page shown in its original language, each with a link to the original.
const original = alternates.find((a) => a.original && !a.current);