Font Optimisation: How We Fixed Silent Font Fallback with next/font
Font Optimisation: How We Fixed Silent Font Fallback with next/font
Release: v1.0.57 · Ticket: PERF-03
The Problem
Geist Sans and Geist Mono are the intended typefaces for BlockManOS. Both fonts were referenced throughout globals.css using CSS custom properties:
@theme inline {
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
These variables — --font-geist-sans and --font-geist-mono — were never actually defined anywhere in the application. The root layout was setting fonts via a plain inline style attribute:
// Before — inline style, CSS variables never defined
<body
className="antialiased"
style={{ fontFamily: "ui-sans-serif, system-ui, -apple-system, ..." }}
>
Because the CSS variables resolved to undefined, every Tailwind utility that relied on font-sans or font-mono (which is most of the application) had zero effect. The browser silently fell back to its default font. There was no error, no warning — the font was just wrong.
The Fix
src/app/layout.tsx was updated to use next/font/google, which is built into Next.js and requires no additional package installation.
import { Geist, Geist_Mono } from "next/font/google";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
display: "swap",
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
display: "swap",
});
The variable option tells next/font to expose the loaded font as a CSS custom property. The class names returned by each font object are then applied to <body>:
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
When Next.js renders the page, it injects --font-geist-sans and --font-geist-mono into the DOM. The globals.css theme mapping now resolves correctly, and Tailwind's font-sans / font-mono utilities work as intended throughout the application.
Why next/font Instead of an Inline Style
| Concern | Inline style | next/font/google |
|---|---|---|
| CSS variable injection | ❌ Not provided | ✅ Automatic via class name |
| Font file hosting | ❌ External request at runtime | ✅ Self-hosted at build time |
font-display: swap | ❌ Manual | ✅ Automatic |
| Subsetting | ❌ Full font downloaded | ✅ latin subset only |
<link rel="preload"> | ❌ Manual | ✅ Injected by Next.js |
| Layout shift (CLS) | ❌ Not optimised | ✅ Metrics baked in at build time |
Health Check Route
As part of this release, GET /api/health was also simplified. Previously the endpoint performed a live database ping and validated the presence of environment variables, returning HTTP 503 on any failure. It now returns a static response:
{ "status": "ok" }
HTTP status: 200.
Impact
- Visual: Geist Sans and Geist Mono now render correctly across the entire application.
- Performance: Fonts are preloaded and self-hosted; no external DNS lookup or FOIT (flash of invisible text).
- Risk: Zero — the change is confined to a single layout file with no schema, router, or component modifications.