Payload Plugins
Pluginspayload-fonts

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.

For AI / LLMs: View Markdown

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:download reads the active families straight from the database through Payload's Local API and writes public/fonts/*.woff2 + src/app/definition.ts (a next/font/local module). 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.
  • extractFonts puts 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>required

The 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-discovered

Optional. 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.)

On this page