fonts-only
Minimal example: @pro-laico/fonts in isolation, served with next/font/local.
A minimal Atomic Payload template that exercises @pro-laico/fonts on its own. Upload font files to the Font collection, choose the active four in the fontSet global, run the font download step, and the site serves them with next/font/local.
What it shows
The only @pro-laico dependencies are @pro-laico/fonts and @pro-laico/core, so you see the fonts plugin in isolation, set up exactly as the fonts plugin's Standalone guide describes:
fontsPlugin({ includeFontSet: true })registers theFontupload collection (woff/woff2/ttf/otf) and thefontSetglobal, the activesans/serif/mono/displayselection (the font analog of an active icon set).POST /api/seeduploads four bundled open-source fonts (one per role) and points thefontSetglobal at them. It is auth-gated and idempotent, run from the admin dashboard, so you have fonts to pick without finding your own.pnpm generate:fonts(wired asprebuild) reads the active selection from a running site, downloads the fonts topublic/fonts, and writessrc/app/definition.tsfornext/font/local.- The layout applies those
next/fontvariables to<html>and maps each role to--font-<role>./renders a specimen per active role (pangram, size ramp, glyphs) viavar(--font-<role>).
Scaffold it
npx @pro-laico/create-atomic-payload my-fonts --template fonts-onlypnpm dlx @pro-laico/create-atomic-payload my-fonts --template fonts-onlyyarn dlx @pro-laico/create-atomic-payload my-fonts --template fonts-onlyThen set a long PAYLOAD_SECRET in .env, generate types and the import map, and start dev:
pnpm generate:types # generates src/payload-types.ts + augment
pnpm generate:importmap # populates src/app/(payload)/admin/importMap.js
pnpm devShips with SQLite (@payloadcms/db-sqlite) at ./font-only.db, with no DB server. Swap adapters by editing src/payload.config.ts.
The flow:
- Visit
/, an explainer page. Logged out, it points you to the admin. - Open
/admin, create your first user. - On the dashboard, click Seed sample fonts (or upload your own and pick them in the
fontSetglobal). - Run
pnpm setup:fontsto download the active fonts fornext/font/local. It boots a temporary server, runs the download against it (authenticating with yourPAYLOAD_SECRET), and shuts it down. - Run
pnpm devagain and open/to see the four active fonts rendered as specimens.
How it works
upload Font (admin) ──► Payload stores the file (FONT_STATIC_DIR / ./media)
│
seed: POST /api/seed uploads public/seed-fonts/*.woff2
then updateGlobal('fontSet', { sans, serif, mono, display }) ◄── active set
│
pnpm generate:fonts ── GET /api/fonts/export @ FONT_DOWNLOAD_URL ──► public/fonts/* + src/app/definition.ts
│
layout.tsx → applies the next/font variables from definition.ts to <html>,
maps --font-<role>: var(--font-set<Role>)
│
page.tsx → getActiveFonts() [fontSet global] ──► a specimen per role via var(--font-<role>)
route is force-dynamic ──► seeding / swapping the active set shows up on the next renderThe active selection lives in a global because it's the active-selection layer, mirroring icons-only's active iconSet. The Font collection can hold many uploads; the fontSet global picks the one sans / serif / mono / display the site uses, editable at /admin/globals/fontSet.
The download step
pnpm build runs prebuild → generate:fonts, which calls the fonts plugin's export endpoint on a reachable Payload instance and writes the fonts to disk for next/font/local. It authenticates with PAYLOAD_SECRET and fetches from FONT_DOWNLOAD_URL, the running instance to pull the active fonts from (see .env.example):
FONT_DOWNLOAD_URL=http://localhost:3000 # the running instance to fetch from
PAYLOAD_SECRET=... # authenticates the requestThe endpoint reads each font server-side, from disk for local storage, or by fetching the URL Payload reports for cloud adapters (Vercel Blob, S3, and the like), so there's no storage token or login to manage. When neither the URL nor the secret resolves, the download is skipped (the build still succeeds) and the specimens fall back to system fonts until you run it.
Local dev, one command. generate:fonts needs a running Payload to fetch from, which is awkward before anything's deployed. pnpm setup:fonts handles it: it boots a temporary next dev server, points FONT_DOWNLOAD_URL at it, runs the download, then shuts the server down. No credentials needed: it authenticates with your existing PAYLOAD_SECRET.
What to look at
| File | What's there |
|---|---|
src/payload.config.ts | buildConfig: fontsPlugin({ includeFontSet: true }) + staticDir |
src/lib/fontDir.ts | FONT_STATIC_DIR (shared with the config, no @payload-config import) |
src/lib/fonts.ts | getActiveFonts(): reads the active selection from the fontSet global |
src/seed/sampleFonts.ts | manifest mapping bundled files → title + role |
src/components/admin/ | BeforeDashboard + SeedControls (seed/reset on the dashboard) |
src/app/definition.ts | generated by generate:fonts (gitignored): the next/font/local declarations |
src/app/(frontend)/layout.tsx | applies the next/font variables + maps --font-<role> |
src/app/(frontend)/page.tsx | explainer + a specimen per active role + "use them together" |
src/app/(payload)/api/seed/route.ts | POST /api/seed → upload fonts + set the fontSet global |
src/app/(payload)/api/reset/route.ts | POST /api/reset → clear the global + delete fonts |
public/seed-fonts/*.woff2 | bundled OFL fonts (committed) + LICENSES.md |
FONT_STATIC_DIR (./media, gitignored) is where Payload writes uploads, shared via src/lib/fontDir.ts so the config and the download step never drift. The bundled seed fonts (Inter, Lora, JetBrains Mono, Abril Fatface) are OFL: see public/seed-fonts/LICENSES.md, and replace them before shipping a real project.
Related
atomic-payload
The full Payload + Next.js + Tailwind starter that wires up every Atomic Payload plugin: environment setup, deployment, and optional Mux/Resend integrations.
icons-only
Minimal example: @pro-laico/icons in isolation, with the Icon and IconSet collections and a small page that renders them.