@pro-laico/fonts
Manage custom fonts in the Payload admin and use them with next/font/local: upload your fonts, pick the active ones, and a build step delivers them to your app.
@pro-laico/fonts lets you manage custom fonts from the Payload admin and use them with next/font/local. Upload your font files, choose the active sans / serif / mono / display faces in the dashboard, and a build step pulls them into your app, with no font files hardcoded in your repo.
Installation
pnpm add @pro-laico/fontsnpm install @pro-laico/fontsyarn add @pro-laico/fontspayload is a peer you already have in a Payload app. dotenv and tsx are only needed when you run the font download step (below), so install them if you don't have them yet.
Setup
Adding fonts to your own Payload + Next.js project.
@pro-laico/core comes bundled with this plugin — it's a dependency, not a separate install — and provides the shared fields, hooks, and utilities this plugin builds on. See @pro-laico/core → Setup.
Add the plugin to your Payload config
import { buildConfig } from 'payload'
import { fontsPlugin } from '@pro-laico/fonts'
export default buildConfig({
plugins: [fontsPlugin({ includeFontSet: true })],
})This registers the Font collection (where you upload font files) and the fontSet global (where you choose the active font for each role: sans / serif / mono / display).
Already using @pro-laico/styles? Leave includeFontSet off and pick fonts in the design set's Fonts tab instead. That's how the template does it (see the other tab).
Upload and choose your fonts in the admin
- Open the Font collection and upload your font files (
.woff,.woff2,.ttf, or.otf). - In the
fontSetglobal, pick the active font for each role: sans, serif, mono, and display.
Add the font download step to your build
next/font/local reads fonts from files in your project, not from the CMS, so you need a step that copies the chosen fonts into your project before each build. Add it to your package.json and wire it as a prebuild script so it runs automatically every time you build:
{
"scripts": {
"prebuild": "pnpm generate:fonts",
"generate:fonts": "atomic-fonts-download"
}
}atomic-fonts-download is the CLI @pro-laico/fonts installs (its bin), so it's available to your scripts with no extra dev dependency. When it runs, it calls the fonts plugin's export endpoint on a running Payload instance (your deploy, or a local dev server), authenticating with your PAYLOAD_SECRET, and:
- Downloads the active font files into
public/fonts. - Writes a small generated file at
src/app/definition.tsthat hooks those fonts intonext/font/local.
Because it reads a running Payload instance, it needs a few environment variables and a reachable site. It runs on every build; you can also run it any time with pnpm generate:fonts after changing the active fonts. Working locally with nothing deployed yet? See Notes for a one-command wrapper.
If a download fails (for example the site is unreachable or a font file can't be written), the step prints a short message by default and leaves any existing fonts in place. To see the full error while debugging, re-run it with --verbose (or -v):
pnpm generate:fonts --verboseYou can also set ATOMIC_FONTS_VERBOSE=true to turn this on without passing the flag.
Ignore the generated files
The download step regenerates the font files and the definition module on every build, so they don't belong in version control. Add them to your .gitignore:
# .gitignore: generated by @pro-laico/fonts
public/fonts
src/app/definition.tsUse the fonts in your app
The generated definition.ts default-exports your fonts as next/font/local objects, each carrying a CSS variable (--font-setSans, --font-setSerif, and so on). Pass that export through the package's extractFonts helper to get the combined className for your root <html> (it returns undefined until the fonts are generated), and the rest of your styles can reference the variables:
// app/(frontend)/layout.tsx
import { extractFonts } from '@pro-laico/fonts'
import fonts from '@/app/definition'
export default function RootLayout({ children }: { children: React.ReactNode }) {
const fontVariables = extractFonts(fonts)
return (
<html lang="en" className={fontVariables}>
<body>{children}</body>
</html>
)
}With the variables in place, your design set (or your own CSS) can point Tailwind's font families at them (--font-sans: var(--font-setSans)), so picking a different font in the admin restyles the whole site.
The atomic-payload template already wires everything up: the Font collection, the build step (prebuild runs generate:fonts), and the layout. You just choose your fonts and build.
Upload your fonts
Open the Font collection in the admin and upload your font files (.woff, .woff2, .ttf, or .otf).
Pick the active fonts
In the active design set, open the Fonts tab and choose the font for each role: sans, serif, mono, and display.
Build (or refresh)
The template's prebuild script runs the font download automatically on next build, so a normal build picks up your choices. To refresh during development after changing the active fonts, run it on demand:
pnpm generate:fontsIt needs the environment variables below set, so it can read your fonts from the deployed site.
Options
fontsPlugin(options?) accepts:
Prop
Type
fontOptions and fontSetOptions are pass-through Payload CollectionConfig / GlobalConfig partials, so they have no fixed key set of their own.
All options at their defaults, as a working starting point:
fontsPlugin({
enabled: true,
includeFontSet: false,
// fontOptions and fontSetOptions are unset by default (no overrides)
})Environment variables
The download step calls the plugin's /api/fonts/export endpoint to fetch the active fonts, using:
| Variable | Purpose |
|---|---|
FONT_DOWNLOAD_URL | The running Payload instance to fetch from (your dev server or deploy). |
PAYLOAD_SECRET | Authenticates the request: sent as Authorization: Bearer, compared against the server's own secret. |
ATOMIC_FONTS_VERBOSE | Set to true to print the full error on a failed download (same as passing --verbose). Off by default, so failures show only a short message. |
You can change where the files and the generated module go with optional overrides: ATOMIC_FONTS_OUTPUT_DIR (default ./public/fonts), ATOMIC_FONTS_DEFINITION_FILE (default ./src/app/definition.ts), and ATOMIC_FONTS_CSS_VAR_PREFIX (default --font-set), among others.
Notes
generate:fonts fetches the active fonts from a running, reachable Payload instance: it calls the plugin's export endpoint at FONT_DOWNLOAD_URL, authenticating with PAYLOAD_SECRET, so it can't read them offline. In CI or on a deploy, point it at your site and the prebuild script runs it on every build.
For local development, before anything's deployed, add a one-command wrapper that boots a temporary dev server, runs the download against it, then shuts it down:
{
"scripts": {
"setup:fonts": "cross-env FONT_DOWNLOAD_URL=http://localhost:3000 start-server-and-test dev http://localhost:3000 generate:fonts"
}
}It needs start-server-and-test and cross-env as dev dependencies. There are no credentials to set up (the download authenticates with your existing PAYLOAD_SECRET), so just run pnpm setup:fonts. The fonts-only example wires this up.
The request sends PAYLOAD_SECRET in the Authorization header, so point FONT_DOWNLOAD_URL at an https URL in production (http is fine on localhost). The endpoint exposes nothing beyond the active font bytes and rejects any request whose secret doesn't match.
Exports
Everything @pro-laico/fonts exports, grouped by what you reach for it.
The plugin
Export
Type
fontsPluginplugin
/api/fonts/export endpoint, and (with includeFontSet) the fontSet global. Default and named export.Parameters
options?:FontsPluginOptionsSee the Options table above. All keys are optional; includeFontSet defaults to false.Returns
PluginA Payload config plugin you add to buildConfig({ plugins: [...] }).Example
import { buildConfig } from 'payload'import { fontsPlugin } from '@pro-laico/fonts'export default buildConfig({plugins: [fontsPlugin({ includeFontSet: true })],})Location
@pro-laico/fontsFontsPluginOptionstype
Location
@pro-laico/fontsFields
Export
Type
fontUploadFieldfunction
font group field for a @pro-laico/styles design set's Fonts tab: four upload slots (sans / serif / mono / display), each filtered to fonts of the matching family. It lives here, not in styles, so styles carries no hard dependency on a font collection. Pass it to stylesPlugin to opt the design set into font uploads.Parameters
args?:{ fontSlug?: string }Override fontSlug (default font) when your Font collection uses a non-default slug, so every slot relationTo points at it.Returns
FieldA Payload group field named font you add to the design set via stylesPlugin.Example
import { stylesPlugin } from '@pro-laico/styles'import { fontUploadField } from '@pro-laico/fonts'import { atomicHook } from '@pro-laico/atomic/hook'import { generateLivePreviewPath } from '@pro-laico/core'// src/plugins/styles.ts: opt the design set's Fonts tab into font uploadsexport const stylesPluginConfig = stylesPlugin({atomicHook,generateLivePreviewPath,designSet: { fontField: fontUploadField() },})Location
@pro-laico/fontsFunctions
Export
Type
extractFontsfunction
definition.ts default export into a single space-separated next/font/local className for your root <html>. It collects every font CSS-variable class and returns undefined before generate:fonts has run, so it drops straight into className.Parameters
definitionFonts:Record<string, { variable?: string }>The default export from your generated @/app/definition module.Returns
string | undefinedThe combined variable classes, or undefined until the fonts are generated.Example
import { extractFonts } from '@pro-laico/fonts'import definitionFonts from '@/app/definition'// app/(frontend)/layout.tsxexport default function RootLayout({ children }: { children: React.ReactNode }) {const fontVariables = extractFonts(definitionFonts)return ( <html lang="en" className={fontVariables}> <body>{children}</body> </html>)}Location
@pro-laico/fontsexportFontsEndpointfunction
GET /api/fonts/export endpoint the plugin registers. It resolves the active fonts (the active design set's font group, else the standalone fontSet global) and returns their bytes as base64, secured by PAYLOAD_SECRET. Registered for you by fontsPlugin; exported for advanced use (custom slugs, a hand-built config).Parameters
opts?:ExportFontsEndpointOptionsOverride path (default /fonts/export), fontSetGlobalSlug, fontCollectionSlug, designSetSlug, or designSetFontField when your project uses non-default slugs.Returns
EndpointA Payload endpoint you add to config.endpoints (the plugin does this automatically).Example
import { buildConfig } from 'payload'import { exportFontsEndpoint } from '@pro-laico/fonts'// only needed for a hand-built config; fontsPlugin registers this for youexport default buildConfig({endpoints: [exportFontsEndpoint({ fontCollectionSlug: 'font' })],})Location
@pro-laico/fontsrunDownloadFontsfunction
PAYLOAD_SECRET), writes them into public/fonts, and generates the src/app/definition.ts module. The generate:fonts command is a thin wrapper around it; call this directly from your own build script when you need to pass options in code.Parameters
overrides?:RunDownloadFontsOptionsOverride the defaults in code: siteUrl, fontsOutputDir, definitionFile, cssVariablePrefix, endpointPath, and more. Each falls back to its env var (e.g. ATOMIC_FONTS_OUTPUT_DIR) then a built-in default. Pass verbose: true (falls back to ATOMIC_FONTS_VERBOSE) to print the full error when a download fails instead of just the short message.Returns
Promise<void>Resolves once the font files and the generated module are written (or skipped when FONT_DOWNLOAD_URL / PAYLOAD_SECRET are missing).Example
import { runDownloadFonts } from '@pro-laico/fonts/scripts/downloadFonts'// scripts/fonts.ts: your own build step, with the site URL passed in codeawait runDownloadFonts({ siteUrl: process.env.FONT_DOWNLOAD_URL })Location
@pro-laico/fonts/scripts/downloadFontsTypes
Export
Type
Font (type)type
Location
@pro-laico/fonts/schemaFontSet (type)type
Location
@pro-laico/fonts/schemaRelated
@pro-laico/icons
Manage your SVG icons in the Payload admin: upload them, group them into reusable sets, and render any icon by name on your site.
@pro-laico/tracking
Turn analytics on and off from the Payload admin: flip a switch for PostHog, Google Tag Manager, or Vercel Analytics and the right scripts load on your site.