Serving fonts
How the fonts you pick reach the browser: the baked next/font path that ships your site, the PreviewFonts escape hatch for previewing a live selection, plus every env var and prop that tunes them.
The active fonts apply as the --font-set{Sans,Serif,Mono,Display} CSS variables, so your app just
uses font-family: var(--font-setSans).
There's one main path — baked next/font, the same in dev and production — and one escape
hatch, <PreviewFonts>, for the narrow case where you want the live selection without a rebuild.
This page covers both, the env vars the download reads, and the <PreviewFonts> props.
The main path: baked next/font
This is how you ship fonts, and it's identical in development and production. A build step resolves
the fontSet selection and writes the fonts to disk; your layout puts them on <html>.
payload fonts:downloadreads the active families straight from the database through Payload's Local API and writespublic/fonts/*.woff2+src/app/definition.ts(anext/font/localmodule). Nothing needs to be running, and there's no URL or bearer token to configure — it boots your own Payload config. Fonts on cloud storage work the same way: the read goes through your collection's storage adapter.extractFontsputs the generated classes (which define the--font-set*variables) on<html>.
import definitionFonts from '@/app/definition'
import { extractFonts } from '@pro-laico/payload-fonts'
<html className={extractFonts(definitionFonts)}> {/* next/font: self-hosted, preloaded, no layout shift */}
{/* … */}
</html>Wire the download as predev and prebuild so definition.ts exists (and is current) before
either server starts. This is the whole story in a real site — service-co runs exactly this:
// package.json — no extra env; it boots your Payload config
{
"scripts": {
"predev": "payload fonts:download",
"prebuild": "payload fonts:download"
}
}next/font/local gives precise preloading, size-adjusted fallbacks, and content-hashed static
assets — no third-party CDN and no layout shift. The tradeoff is that a font change is baked: it
needs a rebuild (re-run the download, rebuild) to publish. To automate that, trigger a redeploy from
a globals.fontSet.hooks.afterChange deploy hook.
definition.ts and public/fonts/ are generated, so gitignore them. predev regenerates them on
every dev boot, so there's no file to hand-create.
Alternative to predev: commit an empty definition.ts as a baseline and skip predev
entirely. The download still overwrites it on prebuild. (The fonts-sandbox example skips the
baked path altogether and previews live — see below.)
Failures empty the definition. Any error during the download (an unreachable database, no fonts
chosen yet, an unexpected throw) resets definition.ts to an empty module so the build still
compiles, rather than leaving a stale one that imports font files no longer on disk (a fresh
checkout gitignores public/fonts/).
Building where the database isn't reachable
For a build box that genuinely can't reach the database, the payload-fonts-download CLI does
the same job over HTTP: it fetches GET /api/fonts/export — gated by PAYLOAD_SECRET (Bearer) —
from a running site named by FONT_DOWNLOAD_URL. That instance must be up while the script runs; if
it isn't, the CLI writes an empty definition and the build ships without fonts.
payload fonts:download (Local API) is the recommended path; reach for the HTTP CLI only when the
build has no database. It reads several env vars — two required. See
Environment variables.
<PreviewFonts>: the live escape hatch
<PreviewFonts> reads the live fontSet selection from the database on every render and inlines
the matching @font-face + --font-set* variables. It works in dev and production — it isn't a
dev-only component and it doesn't defer to the baked path; wherever you mount it, it serves the live
selection. Seeding or editing a font shows up on refresh with no build step.
Use it in the one context where you're previewing a font — a preview route, a draftMode()
branch, a font playground (the fonts-sandbox example) — instead of the baked extractFonts()
className. It does its own thing wherever you mount it and stays out of the main render path; it
doesn't replace the baked path for the rest of your site.
import config from '@payload-config'
import { getPayload } from 'payload'
import { Suspense } from 'react'
import { PreviewFonts } from '@pro-laico/payload-fonts/PreviewFonts'
<html lang="en">
<head>
<Suspense fallback={null}>
<PreviewFonts payload={getPayload({ config })} />
</Suspense>
</head>
{/* … */}
</html>Because it reads per render, render it inside <Suspense> — a dynamic hole — and enable
cacheComponents in next.config so the rest of the page still prerenders around it. Don't mark the
whole route dynamic — the Suspense boundary is all it needs.
<PreviewFonts> is a server component. Pass it a Payload instance via the payload prop, e.g.
payload={getPayload({ config })}.
PreviewFonts props
payloadPayload | Promise<Payload>requiredThe Payload instance (or its promise), e.g. getPayload({ config }).
cssVarPrefixstringdefault '--font-set'CSS family-variable prefix; must match the download CLI cssVariablePrefix so a preview and the baked path emit the same variable names.
fontSetSlugstringdefault 'fontSet'Slug of the font-selection global. Auto-discovered from the plugin marker; an explicit prop wins.
optimizedSlugstringdefault 'fontOptimized'Slug of the optimized (served) upload collection. Auto-discovered from the plugin marker; an explicit prop wins.
familiesFontFamilyConfig[]default auto-discoveredOptional. The slots are auto-discovered from the fontSet global, so you only need this to match custom per-family fallback stacks in the preview.
cssVarPrefix and cssVariablePrefix are the same setting under two names — not a typo. The
<PreviewFonts cssVarPrefix> prop and the download CLI's cssVariablePrefix (env var
PAYLOAD_FONTS_CSS_VAR_PREFIX, default --font-set) both set the prefix for the emitted CSS family
variables. Keep them equal, or a preview and the baked path emit different variable names and your
font-family: var(--font-setSans) rules resolve on only one of them.
Environment variables
The download reads these env vars — full table in Reference. The two required by the HTTP CLI: FONT_DOWNLOAD_URL, PAYLOAD_SECRET. (payload fonts:download boots your config, so it only needs the env your Payload app already has.)