fix: self-host the type set — the build no longer depends on a font CDN - #86
Merged
Conversation
…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>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
dspack-studio-composer | 0393682 | Aug 12 2026, 06:11 PM |
There was a problem hiding this comment.
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/fontsworkspace containing.woff2assets, pinned metadata (catalog.json), pinned fallback metrics (fallbacks.css), and an offline integrity check (verify.mjs). - Updates
apps/webandapps/composerto load the same font families/variables vianext/font/localand 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 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 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" }], |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect. Both apps declared type through
next/font/google, so every build downloaded Oswald, IBM Plex Sans/Mono and Jost fromfonts.gstatic.com. When that CDN hiccups the build fails hard (An error occurred in next/font→Build 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 anddisplayare unchanged, so no consumer CSS moved.Why "no visual change" is true by construction: the vendored
.woff2files are the exact bytesnext/font/googlewas already emitting for this repo, lifted from a build onmain— corroborated by all eight IBM Plex faces being sha256-identical to af-site's OFL-pinned catalog. Subsetting tolatin(which both apps always declared) was measured, not assumed: zero codepoints outside latin were found acrossapps/,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/localrecomputessize-adjustfrom the file it is given, which moved IBM Plex Mono 134.59% → 131.49% and shifted 2,689 pixels of glyphs;packages/fonts/fallbacks.csspins all four to the valuesmainemitted, withadjustFontFallback: 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
licenseURLrecord.OFL.txtcarries the full license and per-family copyright notices as clause 2 requires;catalog.jsonpins every file by sha256 andverify.mjsre-checks them offline as the package'stestscript — sopnpm testnow 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