fix(build): self-host the app fonts so the build doesn't need Google

next/font/google downloads every font from fonts.googleapis.com during
`next build`, and the Coolify build kept failing on it (Nunito/Outfit on
2026-10-17, Lato and JetBrains Mono on 2026-10-20).

The ten families (Geist, Geist Mono, Inter, Space Grotesk, DM Sans,
Playfair Display, Lato, Nunito, Outfit, JetBrains Mono) are now Latin
.woff2 files in src/app/fonts (344 KB, downloaded from Google Fonts),
loaded with next/font/local under the same CSS variable names and
weights. Verified: `npm run build` succeeds with no network access, and
the login page renders in the local Inter with no font errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dave 2026-10-20 18:35:27 +02:00
parent 70ee567215
commit 42a20d7e72
12 changed files with 43 additions and 47 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,16 +1,5 @@
import type { Metadata, Viewport } from "next";
import {
Geist,
Geist_Mono,
Inter,
Space_Grotesk,
DM_Sans,
Playfair_Display,
Lato,
Nunito,
Outfit,
JetBrains_Mono,
} from "next/font/google";
import localFont from "next/font/local";
import "leaflet/dist/leaflet.css";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
@ -20,68 +9,75 @@ import { TooltipProvider } from "@/components/ui/tooltip";
import { ServiceWorkerRegistration } from "@/components/service-worker-registration";
import { ThemeBootstrapScript } from "@/components/theme-bootstrap";
// Geist (default app fonts)
const geistSans = Geist({
// Fonts are self-hosted from ./fonts (Latin subsets of the Google Fonts
// files, downloaded 2026-10-20). With next/font/google every build fetched
// them from fonts.googleapis.com, and the Coolify build kept failing on
// that download (Nunito/Outfit 17 Oct, Lato and JetBrains Mono 20 Oct).
// Same CSS variable names and weights as before.
// Geist (default app fonts) — variable fonts.
const geistSans = localFont({
src: "./fonts/geist-var.woff2",
variable: "--font-geist-sans",
subsets: ["latin"],
weight: "100 900",
});
const geistMono = Geist_Mono({
const geistMono = localFont({
src: "./fonts/geist-mono-var.woff2",
variable: "--font-geist-mono",
subsets: ["latin"],
weight: "100 900",
});
// Theme-bound fonts (registered as CSS vars for the theme picker to swap).
// Each font is restricted to the two weights actually used in the app
// (400 regular + 700 bold). Without these, next/font/google preloads
// every available weight and the browser logs warnings about unused
// preload tags (and the page wastes bandwidth fetching woff2 files
// that will never render).
const inter = Inter({
// Theme-bound fonts (registered as CSS vars for the theme picker to swap),
// 400 regular + 700 bold — the two weights the app uses. Most are variable
// files covering 400–700; Lato ships one file per weight.
const inter = localFont({
src: "./fonts/inter-var.woff2",
variable: "--font-inter",
subsets: ["latin"],
weight: ["400", "700"],
weight: "400 700",
display: "swap",
});
const spaceGrotesk = Space_Grotesk({
const spaceGrotesk = localFont({
src: "./fonts/space-grotesk-var.woff2",
variable: "--font-space-grotesk",
subsets: ["latin"],
weight: ["400", "700"],
weight: "400 700",
display: "swap",
});
const dmSans = DM_Sans({
const dmSans = localFont({
src: "./fonts/dm-sans-var.woff2",
variable: "--font-dm-sans",
subsets: ["latin"],
weight: ["400", "700"],
weight: "400 700",
display: "swap",
});
const playfair = Playfair_Display({
const playfair = localFont({
src: "./fonts/playfair-display-var.woff2",
variable: "--font-playfair",
subsets: ["latin"],
weight: ["400", "700"],
weight: "400 700",
display: "swap",
});
const lato = Lato({
const lato = localFont({
src: [
{ path: "./fonts/lato-400.woff2", weight: "400", style: "normal" },
{ path: "./fonts/lato-700.woff2", weight: "700", style: "normal" },
],
variable: "--font-lato",
subsets: ["latin"],
weight: ["400", "700"],
display: "swap",
});
const nunito = Nunito({
const nunito = localFont({
src: "./fonts/nunito-var.woff2",
variable: "--font-nunito",
subsets: ["latin"],
weight: ["400", "700"],
weight: "400 700",
display: "swap",
});
const outfit = Outfit({
const outfit = localFont({
src: "./fonts/outfit-var.woff2",
variable: "--font-outfit",
subsets: ["latin"],
weight: ["400", "700"],
weight: "400 700",
display: "swap",
});
const jetbrainsMono = JetBrains_Mono({
const jetbrainsMono = localFont({
src: "./fonts/jetbrains-mono-var.woff2",
variable: "--font-jetbrains-mono",
subsets: ["latin"],
weight: ["400", "700"],
weight: "400 700",
display: "swap",
});