Atomic Payload
Plugins

@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/fonts
npm install @pro-laico/fonts
yarn add @pro-laico/fonts

payload 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

  1. Open the Font collection and upload your font files (.woff, .woff2, .ttf, or .otf).
  2. In the fontSet global, 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.ts that hooks those fonts into next/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 --verbose

You 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.ts

Use 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:fonts

It 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:

VariablePurpose
FONT_DOWNLOAD_URLThe running Payload instance to fetch from (your dev server or deploy).
PAYLOAD_SECRETAuthenticates the request: sent as Authorization: Bearer, compared against the server's own secret.
ATOMIC_FONTS_VERBOSESet 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
The plugin itself. Registers the Font collection, the /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/fonts
FontsPluginOptionstype
The TypeScript type for the plugin's options.

Location

@pro-laico/fonts

Fields

Export

Type

fontUploadFieldfunction
Builds the 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/fonts

Functions

Export

Type

extractFontsfunction
Turns the generated 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/fonts
exportFontsEndpointfunction
Factory for the 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/fonts
runDownloadFontsfunction
The font download step as a function. It fetches the active fonts from the plugin's export endpoint (authenticating with 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/downloadFonts

Types

Export

Type

Font (type)type
TypeScript type for typed access to your Font documents.

Location

@pro-laico/fonts/schema
FontSet (type)type
TypeScript type for the active font selection.

Location

@pro-laico/fonts/schema

On this page