@bison-lab/fonts
v3.27.1
Published
A curated, self-hosted font catalogue for Bison Lab sites: serves only the families a theme names
Maintainers
Readme
@bison-lab/fonts
A curated, self-hosted font catalogue for Bison Lab sites. A theme names a
family by id; the site serves that family's files itself and declares only
the families it names. No request goes to Google Fonts, and a family the
theme does not name is never downloaded by a visitor: it costs disk in
node_modules and nothing at load time.
Every family is variable weight, latin subset, licensed under the SIL Open
Font License, with the licence beside its files under files/<id>/.
The catalogue
| Id | Family | Category | Weights |
| --- | --- | --- | --- |
| atkinson-hyperlegible-next | Atkinson Hyperlegible Next | sans | 200–800 |
| inter | Inter | sans | 100–900 |
| montserrat | Montserrat | sans | 100–900 |
| work-sans | Work Sans | sans | 100–900 |
| dm-sans | DM Sans | sans | 100–1000 |
| manrope | Manrope | sans | 200–800 |
| nunito | Nunito | sans | 200–1000 |
| source-serif-4 | Source Serif 4 | serif | 200–900 |
| lora | Lora | serif | 400–700 |
| merriweather | Merriweather | serif | 300–900 |
| fredoka | Fredoka | display | 300–700 |
| space-grotesk | Space Grotesk | display | 300–700 |
| playfair-display | Playfair Display | display | 400–900 |
| fraunces | Fraunces | display | 100–900 |
| jetbrains-mono | JetBrains Mono | mono | 100–800 |
| fira-code | Fira Code | mono | 300–700 |
Each entry carries a one-line hint for a picker, and the CLI and the
Theme admin show it beside the name.
Wiring a Next site
Three pieces: the route that serves the files, the config line that keeps the files reachable, and the theme that names the families.
1. The route. Answers /fonts/<id>/<file> from the package's own
files/ directory with Cache-Control: public, max-age=31536000,
immutable. Only a file the catalogue lists is served; a traversal or an
unknown file is 404.
// app/fonts/[...path]/route.ts
import { serveFont } from "@bison-lab/fonts/server";
export const GET = serveFont;
export const HEAD = serveFont;2. Keep the package external. serveFont finds the files relative to
its own module, so the package must not be bundled into the route:
// next.config.ts
const nextConfig = {
serverExternalPackages: ["@bison-lab/fonts"],
};3. Name the families. @bison-lab/create-theme writes them into
bison.config.json as catalogue ids and puts the @font-face rules at
the top of bison-theme.css, so a CLI site is done here. A site that
builds its theme at request time declares them itself:
import { fontFaceCss, fontPreloads } from "@bison-lab/fonts";
import { buildThemeCss, themeFontIds } from "@bison-lab/tokens";
const ids = themeFontIds(config); // ["atkinson-hyperlegible-next", "montserrat"]
const preloads = fontPreloads(ids, "/fonts");
<head>
{preloads.map((p) => (
<link key={p.href} rel="preload" as="font" type={p.type} href={p.href} crossOrigin="anonymous" />
))}
<style>{fontFaceCss(ids, "/fonts")}</style>
<style>{buildThemeCss(config)}</style>
</head>buildThemeCss(config, { fontsBaseUrl: "/fonts" }) does the @font-face
part for you when one stylesheet is simpler.
BisonProvider in @bison-lab/ui takes the same ids (bodyFont="inter")
and emits the rules and preloads itself under fontsBaseUrl.
Serving from a different path is one option in each place:
serveFont(request, { baseUrl: "/static/type" }) and the same base URL to
fontFaceCss / fontPreloads.
API
@bison-lab/fonts (no Node import; safe in the browser):
| Export | What it is |
| --- | --- |
| catalog, fontIds | Every FontEntry, in the order pickers list them |
| isFontId(value) | Whether a value is a catalogue id |
| getFont(id) | The entry; throws on an unknown id |
| findFont(id), findFontByFamily(name) | The entry or undefined; the second is how a legacy family name finds its id |
| fontStack(id) | The font-family value: "Inter", ui-sans-serif, system-ui, sans-serif |
| fontFaceCss(ids, baseUrl) | @font-face rules for exactly those families, font-display: swap |
| fontPreloads(ids, baseUrl) | { href, type } per family, the normal-style file a first paint needs |
| fontFilePaths | Every path serveFont may answer |
@bison-lab/fonts/server (Node):
| Export | What it is |
| --- | --- |
| serveFont(request, { baseUrl }) | The Fetch handler: GET and HEAD, 404 outside the catalogue, 405 otherwise |
| FONT_FILES_DIR | The directory the files live in |
Adding a family
- Add the
@fontsource-variable/<id>package as a dev dependency here. Fontsource's id is the catalogue id. - Add the entry to
src/catalog.ts: family, category, the wght axis from the package'smetadata.json, whether it ships an italic, a fallback and a hint. pnpm --filter @bison-lab/fonts synccopies the latin woff2 files and the licence intofiles/<id>/. Commit them.pnpm --filter @bison-lab/fonts testholds the entry to the package's metadata and checks every file is on disk.
A family that is not on Fontsource, or not OFL, is a different conversation: the catalogue is curated on purpose.
