Skip to content

fix: self-host the type set — the build no longer depends on a font CDN - #86

Merged
ryandmonk merged 1 commit into
mainfrom
fix/pre1-self-host-fonts
Aug 12, 2026
Merged

fix: self-host the type set — the build no longer depends on a font CDN#86
ryandmonk merged 1 commit into
mainfrom
fix/pre1-self-host-fonts

Conversation

@ryandmonk

Copy link
Copy Markdown
Contributor

The defect. Both apps declared type through next/font/google, so every build downloaded Oswald, IBM Plex Sans/Mono and Jost from fonts.gstatic.com. When that CDN hiccups the build fails hard (An error occurred in next/fontBuild failed because of webpack errors). It failed roughly three of eight CI runs in one day, blocking merges and deploys. A product being prepared for public 1.0 should not need a third-party CDN to build.

The fix. One shared packages/fonts/ workspace package, next/font/local, 90.5 KB of fonts / 108 KB total across 12 files. Exported names, CSS variable names, weights, styles and display are unchanged, so no consumer CSS moved.

Why "no visual change" is true by construction: the vendored .woff2 files are the exact bytes next/font/google was already emitting for this repo, lifted from a build on main — corroborated by all eight IBM Plex faces being sha256-identical to af-site's OFL-pinned catalog. Subsetting to latin (which both apps always declared) was measured, not assumed: zero codepoints outside latin were found across apps/, packages/, e2e/, acceptance/ and the demo project.

Proof: 6 emitted faces byte-identical to main; preload set unchanged; all 137 text elements identical in size/position/weight; full-page screenshots byte-identical — 0 of 2,423,040 pixels differ. Reaching that zero required pinning the metric-matched fallbacks: next/font/local recomputes size-adjust from the file it is given, which moved IBM Plex Mono 134.59% → 131.49% and shifted 2,689 pixels of glyphs; packages/fonts/fallbacks.css pins all four to the values main emitted, with adjustFontFallback: false.

Proof the CDN is gone: builds run under a scoped guard making the font hosts unresolvable — main's code fails there exactly as reported, this branch's code exits 0 for both apps and emits the same six faces.

Licensing: all four families are OFL-1.1, confirmed from each binary's own licenseURL record. OFL.txt carries the full license and per-family copyright notices as clause 2 requires; catalog.json pins every file by sha256 and verify.mjs re-checks them offline as the package's test script — so pnpm test now covers the bytes.

pnpm test, typecheck, both static exports, composer smoke 32/32, composer-agent 49/49, exhibit 106 passed / 4 skipped.

🤖 Generated with Claude Code

…fail the build

apps/web and apps/composer declared their type with next/font/google. Next
self-hosts the result, so no visitor ever hit Google — but the BUILD fetched
every face from fonts.gstatic.com, which made a third-party CDN a hard
dependency of compiling this product. It failed roughly three of eight CI runs
in a day, blocking merges and deploys:

  [Error: Failed to fetch font file from `https://fonts.gstatic.com/...woff2`.]
  An error occurred in `next/font`.
  > Build failed because of webpack errors

Both apps now declare the same faces with next/font/local against committed
.woff2 files in packages/fonts. No build touches the network for type.

The vendored bytes are not a fresh download: they are the exact files
next/font/google was already emitting for this repo, lifted from a build on
main, so "no visual change" holds by construction rather than by inspection.
Proof, main vs this branch on apps/web: the six emitted faces are byte-identical
by sha256, the <link rel=preload> set is unchanged, all 137 text elements keep
their size, position and weight, and the full-page screenshots are byte-identical
PNGs — 0 of 2,423,040 pixels differ.

Getting that last 0 needed the fallbacks pinned. next/font/google reads its
metric-matched fallback from a per-family metrics database; next/font/local
recomputes it from the file you hand it, and ours are the latin subset, whose
OS/2 xAvgCharWidth differs. Left alone it moved IBM Plex Mono's size-adjust from
134.59% to 131.49% and visibly narrowed every character the subset does not
carry — 2,689 pixels' worth of shifted arrow glyphs. packages/fonts/fallbacks.css
pins all four faces at the values main emitted, with adjustFontFallback: false.

Subset is latin, matching the subsets: ["latin"] both apps always declared.
next/font/google emits every subset Google returns and preloads only the declared
one, so main shipped 29 files where 6 were ever preloaded. Dropping the other 23
was checked, not assumed: a scan of apps/, packages/, e2e/, acceptance/ and the
demo project found zero codepoints in the latin-ext, Greek, Cyrillic or
Vietnamese ranges.

All four families are OFL-1.1, and each vendored binary says so itself in its
licenseURL name record. OFL.txt carries the license and every family's copyright
notice as clause 2 requires. catalog.json pins each file by sha256 and
verify.mjs re-checks them offline as the package's test script, so pnpm test
covers the bytes.

Exported names and CSS variable names are unchanged, so no consumer CSS moves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 12, 2026 18:09
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
dspack-studio-composer 0393682 Aug 12 2026, 06:11 PM

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes the build-time dependency on Google Fonts by vendoring the product type set in-repo and switching both Next.js apps from next/font/google to next/font/local, with a verification script to pin the exact font bytes.

Changes:

  • Adds a new packages/fonts workspace containing .woff2 assets, pinned metadata (catalog.json), pinned fallback metrics (fallbacks.css), and an offline integrity check (verify.mjs).
  • Updates apps/web and apps/composer to load the same font families/variables via next/font/local and to use the pinned fallback faces/metrics.
  • Registers the new workspace in pnpm-lock.yaml.

Reviewed changes

Copilot reviewed 8 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds the new packages/fonts workspace to the lockfile importers.
packages/fonts/verify.mjs Offline SHA-256 + size verification of vendored faces against catalog.json.
packages/fonts/README.md Documents rationale, provenance, subset, and licensing details for the vendored type set.
packages/fonts/package.json Defines the internal workspace package and its test script (node verify.mjs).
packages/fonts/OFL.txt Bundles OFL-1.1 text and required copyright notices for redistributed fonts.
packages/fonts/fallbacks.css Pins metric-matched fallback @font-face declarations to preserve layout consistency.
packages/fonts/catalog.json Catalogs/pins each face’s filename, size, and SHA-256 digest.
apps/web/app/fonts.ts Switches to next/font/local and points at vendored font files + pinned fallbacks.
apps/composer/app/fonts.ts Switches to next/font/local and points at vendored font files + pinned fallbacks.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/web/app/fonts.ts
Comment on lines +24 to +29
import localFont from "next/font/local";

export const oswald = Oswald({
subsets: ["latin"],
weight: ["600"],
import "../../../packages/fonts/fallbacks.css";

export const oswald = localFont({
src: [{ path: "../../../packages/fonts/files/oswald-600-latin.woff2", weight: "600", style: "normal" }],
Comment thread packages/fonts/verify.mjs
Comment on lines +19 to +34
const failures = [];
let bytes = 0;

for (const face of catalog.faces) {
const file = path.join(HERE, "files", face.file);
if (!fs.existsSync(file)) {
failures.push(`${face.file}: missing`);
continue;
}
const buf = fs.readFileSync(file);
const sha256 = crypto.createHash("sha256").update(buf).digest("hex");
if (buf.length !== face.bytes) failures.push(`${face.file}: ${buf.length} bytes, catalog says ${face.bytes}`);
else if (sha256 !== face.sha256) failures.push(`${face.file}: sha256 ${sha256}, catalog says ${face.sha256}`);
bytes += buf.length;
}

Comment on lines +24 to +29
import localFont from "next/font/local";

export const oswald = Oswald({
subsets: ["latin"],
weight: ["600"],
import "../../../packages/fonts/fallbacks.css";

export const oswald = localFont({
src: [{ path: "../../../packages/fonts/files/oswald-600-latin.woff2", weight: "600", style: "normal" }],
@ryandmonk
ryandmonk merged commit b1b83fd into main Aug 12, 2026
3 checks passed
@ryandmonk
ryandmonk deleted the fix/pre1-self-host-fonts branch August 12, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants