Fonts
Self-host your fonts with zero layout shift — Nifra's equivalent of next/font. Either let the build downloader fetch a Google font for you, or hand-write a @font-face from a file you already have. Both paths produce the same thing: a self-hosted, content-hashed font with font-display: swap, a preload, and metric overrides — no runtime CDN hotlink, and Google never learns what your users read.
Automated: download a Google font at build time
loadGoogleFont runs in your build (it touches the network and writes files, so it never sits on the request path). It downloads the font, content-hashes each .woff2 into outDir, and returns a self-hosted @font-face stylesheet plus the matching preloads:
// fonts.build.ts — run once at build time (a prebuild step). It hits the network.
import { loadGoogleFont } from "@nifrajs/web/fonts"
const inter = await loadGoogleFont(
{ family: "Inter", weights: [400, 700], subsets: ["latin"], sizeAdjust: "107%" },
{ outDir: "public/fonts", publicPath: "/fonts" },
)
await Bun.write("app/fonts.css", inter.css) // the @font-face stylesheet
await Bun.write("app/fonts.preloads.json", JSON.stringify(inter.preloads))Then wire the two outputs into your app:
// app entry — bundled + content-hashed by nifra's CSS pipeline
import "./fonts.css"
// root layout — preload the primary file (one less render-blocking round trip)
import preloads from "./fonts.preloads.json" with { type: "json" }
export const meta = { link: preloads }Re-run the script when you change weights or subsets; commit the hashed public/fonts/*.woff2 (or regenerate them in CI). A complete runnable example lives in examples/fonts-google.
- weights — numbers (
400), a variable range ("100 900"), or the keywordsnormal/bold - styles —
["normal", "italic"]; defaults to["normal"] - subsets — keep only the ones you serve (
["latin"]); Google returns every subset as its own@font-facewith aunicode-range - text — glyph subsetting for a logo/heading: request only the characters you render, and you get one tiny file
Manual: self-host a file you already have
Already have the .woff2? fontFace builds a CLS-safe rule directly. Put the result in a stylesheet your app imports — Nifra's CSS pipeline bundles and content-hashes it.
// fonts.css — you dropped inter.woff2 into public/fonts/ yourself
import { fontFace } from "@nifrajs/web"
export default fontFace({
family: "Inter",
src: [{ url: "/fonts/inter.woff2" }], // format() inferred from the extension
weight: "100 900", // a variable-font range
display: "swap", // the default — paints fallback text instantly
sizeAdjust: "107%", // metric override → no fallback→web-font shift
})Preload the font file
fontPreload emits a <link rel="preload" as="font"> for a root layout's meta.link — the browser would otherwise discover the font only after parsing the CSS, a wasted round trip. It defaults to crossorigin="anonymous" because fonts are always fetched in CORS mode (a mismatched preload is downloaded twice).
// a root layout's meta — becomes <link rel="preload" as="font" crossorigin> in <head>
import { fontPreload } from "@nifrajs/web"
export const meta = { link: [fontPreload({ href: "/fonts/inter.woff2" })] }Preload the primary face only (e.g. latin 400). Preloading every weight and subset forces downloads the page may never use.
loadGoogleFont validates the family, weights, and subsets, allowlists the font-file host to fonts.gstatic.com over https (a tampered stylesheet can't redirect the download elsewhere — an SSRF gate), and caps each download's size. Filenames are derived only from validated tokens plus a content hash, so there's no path traversal.