Fonts

SkillDev tools

Add, swap, or remove a self-hosted web font (latin variable woff2 from fontsource, localized via next/font/local). Use when adding a font role, or debugging font loading, preload, or CLS.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Fonts skill

What this skill tells your AI

The instructions your AI receives, as published by nrjdalal/zerostarter in .agents/skills/fonts/SKILL.md and read by ahel’s review.

Every font is self-hosted through next/font/local. That loader is what generates the metric-adjusted "<family> Fallback" faces that keep CLS near zero, so it is not optional. Do NOT switch to fontsource CSS imports: the bundler resolves their url() references to node_modules paths that 404 in dev, and they emit no compiler-only fallback metrics.

Layout

  • web/next/src/fonts/*.woff2, the vendored latin variable files
  • web/next/src/lib/fonts.ts, one localFont per family, each exporting a CSS variable
  • web/next/src/app/layout.tsx, where the variables are applied on <html>
  • web/next/src/app/globals.css @theme inline, where roles chain to the font variables (--font-sans: var(--font-dm-sans), sans-serif)

Add or swap a font

  1. Fetch the latin variable file from the fontsource CDN directly, no dependency needed:

    curl -L -o web/next/src/fonts/<name>-latin-wght-normal.woff2 \
      https://cdn.jsdelivr.net/npm/@fontsource-variable/<name>/files/<name>-latin-wght-normal.woff2
    
  2. Declare it in web/next/src/lib/fonts.ts: localFont({ src: "../fonts/<file>", variable: "--font-<name>", weight: "<min> <max>" }). The weight range is MANDATORY for a variable font: omit it and the face pins to 400, so every bold glyph becomes faux-bold synthesis. Read the range from the fontsource CSS, curl -s https://cdn.jsdelivr.net/npm/@fontsource-variable/<name>/index.css | grep font-weight. A serif also sets adjustFontFallback: "Times New Roman" (the metrics default to Arial).

  3. Apply the export's .variable on <html> in layout.tsx.

  4. Wire the role in globals.css @theme inline: --font-<role>: var(--font-<name>), <generic>.

  5. Verify loading: dev CSS emits hashed /_next/static/media/*.woff2 urls plus the generated "<family> Fallback" faces, and a production build (Vercel preview, protection-bypass header) emits one <link rel="preload" as="font"> per file, with no font 404s.

  6. If a public page changed, check CLS and LCP for layout shift before shipping.

Notes

  • The data table measures header labels from bundled metrics of the sans woff2, generated by packages/scripts/src/data-table-metrics.ts into .generated/data-table-metrics.json (in the web build, dev, and check-types chains). Swapping the sans re-measures automatically on the next run, but the script's FONT path must point at the new file: update it when the filename changes.
  • Preload is scoped by import. Any font in web/next/src/lib/fonts.ts preloads on every page, because the root layout imports it. To scope a font to specific routes, declare it in a module only those routes import: author-only fonts live in web/next/src/lib/marketing/fonts.ts, with woff2 under web/next/src/fonts/marketing/.
  • DM Sans vendors its italics: src is an array of { path, style } entries under one top-level weight. Mono renders synthetic oblique for italics, by choice.
  • A non-variable font takes the per-weight files, with src as an array carrying a weight per entry.
  • To remove a font, delete the woff2, its localFont definition, the <html> variable, and the globals.css role, then grep the variable name to catch stragglers.

Signals

GitHub stars
63
Forks
11
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
fonts
Source
github.com/nrjdalal/zerostarter