Skip to content

Repository files navigation

astro-font-preload

OpenSSF Best Practices

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

  1. Parses the @font-face blocks Astro emitted (handles Astro's unquoted hashed family names — the shape that silently breaks naive regexes).
  2. Injects <link rel="preload" as="font" crossorigin> for the faces matching your families/weights/subsets into every HTML.
  3. Injects <link rel="preload" as="style"> for each page's same-origin stylesheets.
  4. Appends Link: headers under /* in _headers for 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 .woff2 files but zero @font-face blocks parse (Astro changed its output shape — better a red build than weeks of FOIT);
  • you set families explicitly and none of them match.

With no explicit families, an empty match only warns (the integration can't know your intent).

Notes

  • crossorigin on font preloads is not optional — browsers silently discard as="font" preloads without it.
  • _headers is 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 via Link.
  • 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.

About

Astro integration: font + critical-CSS preloads in every built page, plus _headers Link lines that fuel Early Hints (103). Fails loud instead of shipping a FOIT regression

Topics

Resources

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages