Collections
The font collections that store your typefaces, and the fontSet global that chooses which one fills each family.
payload-fonts adds three collections and one global under an Assets admin group.
You only ever edit one collection, font — one document per typeface. Two hidden upload collections
derive from it, and the fontSet global decides which typeface fills each family. The pipeline is
font → fontOriginal (archive) → fontOptimized (served), each font file flowing one slot at a
time.
The slugs on this page are the defaults. Any of them — and the fontSet global's — can be renamed
with a slug override, and every internal reference follows: see
Renaming.
font
One document per typeface (e.g. "Inter"), and the only one you touch. It's not an upload
collection; it holds Payload upload slots pointing at fontOriginal, carried one of two
mutually-exclusive ways: a single variable file or a weights array.
On save, every referenced original is subsetted to a fontOptimized WOFF2. Variable fonts keep
their wght axis range; static files take the row's weight/style. Delete cascades to both derived
collections. Reads and writes require a logged-in user.
Fields
Prop
Type
Hooks
Prop
Type
fontOriginal
The hidden archive of truth: the raw, untouched files editors drop into the font slots. Kept so
the lossy, subsetted output can be re-derived with a different charset later. An upload collection
whitelisting the four web-font formats; you never see it in nav, only through the font fields.
Fields: none beyond the upload itself (the stored original bytes). Accepts woff / woff2 /
ttf / otf / ttc via an upload.mimeTypes whitelist. That list carries more than the obvious
four: OTF/TTF arrive under several sfnt mime strings, plus the file extensions (the admin builds the
picker's accept from this list) and application/octet-stream — because Windows registers no
content type for a font, so browsers there report exactly that. Treat it as a picker filter, not a
gate: the collection is logged-in-only, and anything that isn't really a font fails the subsetter on
save and serves no weight.
Hooks: none; uploading here is a plain store, which is exactly what lets it run as a client-upload (direct-to-Blob) collection in production. Each original belongs to exactly one typeface.
fontOptimized
The hidden, derived collection of WOFF2 bytes the site actually serves: one upload doc per
weight/style (or variable file), written by font's save hook, never hand-uploaded. Read is
public so the build-time export can fetch them on cloud storage; writes stay gated.
Fields — beyond the upload itself (the subsetted WOFF2 bytes):
Prop
Type
Hooks: none; the bytes are derived and rebuilt from the originals on every font save.
The fontSet global
Uploading a typeface doesn't put it on the site; it just adds it to the library. fontSet is
how you choose which uploaded typeface fills each family. fontsPlugin() registers it as a singleton
global under the Assets group with one single-relationship slot per family (four by default).
Every slot offers every typeface — a typeface's family is a preference, not a filter, and appears
in the option text (Inter (Sans)) so it guides the choice without hiding anything:
| Slot | Picks the typeface used for… |
|---|---|
sans | var(--font-setSans) |
serif | var(--font-setSerif) |
mono | var(--font-setMono) |
display | var(--font-setDisplay) |
Pick one Font per slot and save; that selection is the source of truth both serving paths read.
Change it and the next download (or, with <PreviewFonts>, the next refresh) swaps the served fonts, no code change.
Leave a family empty to fall back to whatever your CSS defines for that variable.
// or set it without the admin: server-side / in a migration
await payload.updateGlobal({ slug: 'fontSet', data: { sans: interId, mono: jetbrainsMonoId } })Seeds set it the same way with ref() tokens (see Seeding).
Driving the selection some other way? Pass globals: { fontSet: false } to skip the global and
resolve the active typefaces yourself. Without it, the export endpoint has nothing to resolve.
Custom families
families is the complete list: whatever you pass replaces the four defaults wholesale. It flows
through everything in lockstep — the family options, the fontSet slots, the export JSON keys, and
the generated font<Key> / --font-set<Key> names.
fontsPlugin({
options: {
families: [
{ key: 'sans' }, // label defaults to "Sans"
{ key: 'display' },
{ key: 'brand', label: 'Brand', fallback: 'Georgia, serif' }, // a new family
],
},
})
// EXACTLY these three; serif and mono are dropped. family options + fontSet slots
// sans/display/brand; exports fontSans/fontDisplay/fontBrand; vars --font-set{Sans,Display,Brand}.To add a family while keeping the defaults, spread DEFAULT_FONT_FAMILIES:
import { fontsPlugin, DEFAULT_FONT_FAMILIES } from '@pro-laico/payload-fonts'
fontsPlugin({ options: { families: [...DEFAULT_FONT_FAMILIES, { key: 'brand', fallback: 'Georgia, serif' }] } })<PreviewFonts> auto-discovers the slots from the fontSet global, so custom families just work. Pass
families to it only if you set custom per-family fallback stacks and want the preview to match
(see PreviewFonts props).
Changing families strands existing data. Dropping a family removes its fontSet slot (the stored
selection silently vanishes) and leaves font docs whose family value is no longer an option —
they fail validation on their next save. Migrate those docs, or keep the old key, when you reshape
the list.
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.
How it works
How the subset pipeline, one-file italics, the baked next/font path, and the PreviewFonts escape hatch fit together — so you can reason about what happens on save, on build, and on refresh.