# Collections

URL: /docs/plugins/payload-fonts/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](/docs/plugins/payload-fonts/reference#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**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `text` |  | The typeface name. Required, and what the admin list searches. The displayed title is this plus the preferred family when one is set. |
| `family` | `radio` |  | The generic family this typeface is offered for first: sans / serif / mono / display by default (customisable via the plugin's options.families). Optional — every Font Set slot offers every typeface either way; the preference is appended to the option text (Inter (Sans)) rather than filtering the list. |
| `optionLabel` | `text (hidden)` |  | Hidden, derived label the Font Set slot options display — the title plus the preferred family when one is set. Maintained by hooks; not editable. |
| `servedFiles` | `number (virtual)` |  | Read-only sidebar count of the fontOptimized files this typeface currently serves, counted per read. 0 after a save means nothing optimized — re-save, and check the server logs if it stays 0. |
| `variable` | `group` |  | A single variable file per upright / italic (covers many weights). Hidden once weights are added; use this OR weights, not both. |
| `weights` | `array` |  | One file per weight (100–900) + style (normal/italic); each weight+style pair must be unique. Hidden once a variable file is added. |

**Hooks**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `requireFontFiles` | `beforeValidate` |  | Requires at least one file and rejects mixing a variable font with specific weights. |
| `rejectSharedOriginals` | `beforeValidate` |  | Enforces one fontOriginal per typeface, rejecting a save that references an original already used by another typeface (keeps asset cleanup safe). |
| `optimizeFromOriginals` | `afterChange` |  | Subsets each referenced original to a served fontOptimized WOFF2, and cleans up any original a swapped/removed slot de-referenced. |
| `cleanupFontAssets` | `beforeDelete` |  | Cascades the delete to the served fontOptimized files and the archived fontOriginal bytes. |

## `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):

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `font` | `relationship` |  | The owning typeface. The export endpoint and reconcile hook query by it. Read-only. |
| `original` | `relationship` |  | The source fontOriginal this was subsetted from, the reconcile key (one optimized per original). Read-only. |
| `weight` | `text` |  | CSS font-weight: a single step (400) or a variable range (100 900); served verbatim to next/font. Read-only. |
| `style` | `radio` |  | normal / italic. Read-only. |
| `isVariable` | `checkbox` |  | Whether this file is a variable font. Read-only. |
| `italCapable` | `checkbox` |  | This upright variable file ALSO carries italics via its axes (a true ital axis, or a negative slnt range) — the serving layers emit a second, italic @font-face from the same file unless the typeface has an explicit italic file. Read-only. |
| `obliqueAngle` | `number` |  | For slnt-based italics: the positive CSS oblique angle (deg) matching the axis extreme, e.g. 15 for slnt 0…-15. Read-only. |

**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.

```ts
// 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](/docs/plugins/payload-fonts/how-it-works#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.

```ts
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`:

```ts
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](/docs/plugins/payload-fonts/serving#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.
