Docs

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:

TS
// 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:

TS
// 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 keywords normal/bold
  • styles["normal", "italic"]; defaults to ["normal"]
  • subsets — keep only the ones you serve (["latin"]); Google returns every subset as its own @font-face with a unicode-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.

TS
// 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).

TS
// 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.

Security: 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.
Proudly built with Nifra — server-rendered on Cloudflare Pages.MIT