astro-font-preload
v1.0.0
Published
Astro integration: inject <link rel=preload> for your fonts and render-blocking CSS into every built page, plus _headers Link lines so Early Hints (103) fetches them during server-think time. Fails loud instead of silently shipping a FOIT regression.
Downloads
140
Maintainers
Readme
astro-font-preload
Astro integration that injects <link rel="preload"> for your fonts and
render-blocking CSS into every built page — plus Link: lines in _headers
so Early Hints (103) starts those fetches during server-think time on a
cold cache.
// astro.config.mjs
import { defineConfig } from "astro/config";
import fontPreload from "astro-font-preload";
export default defineConfig({
integrations: [fontPreload()],
});Why
Astro's Fonts API has shipped builds where <Font preload> emits the
@font-face styles but no <link rel=preload> at all (verified against
Astro 6.3.1 production HTML: zero preload tags site-wide). Without the
preload, the browser discovers the font URL only after parsing the inline
<style> — and with font-display: block that discovery gap paints
invisible text (FOIT).
CSS gets the same treatment: your render-blocking stylesheets are the FCP
gate, but the browser can't fetch them before it has parsed enough HTML to
find them. A preload raises per-page discovery priority, and the _headers
Link lines let a CDN with Early Hints enabled (Cloudflare, etc.) start the
fetch before the HTML body arrives. Measured on a production site: mobile
cold-cache FCP 852 ms → ~300 ms. Sheets stay render-blocking — nothing goes
async, so zero FOUC and zero CLS; they just arrive earlier.
What it does at astro:build:done
- Parses the
@font-faceblocks Astro emitted (handles Astro's unquoted hashed family names — the shape that silently breaks naive regexes). - Injects
<link rel="preload" as="font" crossorigin>for the faces matching your families/weights/subsets into every HTML. - Injects
<link rel="preload" as="style">for each page's same-origin stylesheets. - Appends
Link:headers under/*in_headersfor the fonts and the stylesheets present on every page (the intersection — never a sheet that would be an "unused preload" somewhere).
Options
fontPreload({
families: ["Inter"], // default: every family found in the build
weights: ["400", "500", "600"], // your above-the-fold weights
subsets: ["latin"], // latin-ext etc. keep lazy-loading
cssPreload: true, // per-page stylesheet preloads
earlyHints: true, // _headers Link lines
});Fail-loud contract
Silent skips are how font regressions ship. The build throws when:
- the HTML references
.woff2files but zero@font-faceblocks parse (Astro changed its output shape — better a red build than weeks of FOIT); - you set
familiesexplicitly and none of them match.
With no explicit families, an empty match only warns (the integration
can't know your intent).
Notes
crossoriginon font preloads is not optional — browsers silently discardas="font"preloads without it._headersis the Cloudflare Pages / Workers-assets / Netlify convention. On Cloudflare, also switch Early Hints ON for the zone — the 103 only replays what the response advertises viaLink.- Idempotent: re-running on the same output adds nothing.
License
MIT.
Extracted from the production build of RoundCut — free, in-browser image tools — where it feeds Early Hints on every page across 29 languages.
