Payload Plugins
Pluginspayload-fonts

Collections

The font collections that store your typefaces, and the fontSet global that chooses which one fills each family.

For AI / LLMs: View Markdown

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 fontfontOriginal (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:

SlotPicks the typeface used for…
sansvar(--font-setSans)
serifvar(--font-setSerif)
monovar(--font-setMono)
displayvar(--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.

On this page