Summary
When a caller uses next/font/google without passing an explicit weight option, the vinext shim builds the Google Fonts URL with :wght@100..900. Many Google Fonts don't support that full range (e.g. Sen is 400–800), so Google Fonts CDN responds with HTTP 400 and the stylesheet never loads. The rendered page computes font-family: 'Sen', sans-serif correctly, but because no @font-face registers, the browser paints the generic sans-serif fallback — so the bug looks like "font-family cascade issue" even though the real fault is the URL.
Reproduction
- In
app/layout.tsx:
import { Sen } from 'next/font/google';
const appFont = Sen({ subsets: ['latin'] });
- Load the page and check the head — vinext emits:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sen:wght@100..900&display=swap" />
curl -I that URL:
HTTP/2 400
content-type: text/html; charset=utf-8
content-length: 0
- Compare with Sen's actual supported range:
$ curl -I 'https://fonts.googleapis.com/css2?family=Sen:wght@400..800&display=swap'
HTTP/2 200
content-type: text/css; charset=utf-8
$ curl -I 'https://fonts.googleapis.com/css2?family=Sen&display=swap'
HTTP/2 200
content-type: text/css; charset=utf-8
Root cause
dist/shims/font-google-base.js:80-95:
function buildGoogleFontsUrl(family, options) {
const params = new URLSearchParams();
let spec = family;
const weights = options.weight ? …;
const styles = options.style ? …;
if (weights.length > 0 || styles.length > 0) {
…
} else spec += `:wght@100..900`; // ← always-wrong default
params.set("family", spec);
params.set("display", options.display ?? "swap");
return `https://fonts.googleapis.com/css2?${params.toString()}`;
}
The hardcoded :wght@100..900 assumes every Google font is a 100–900 variable axis. Many aren't:
- Sen → 400–800
- Archivo Black, Anton, Fjalla One, Russo One, and many display fonts → single static weight (400)
- Many classical fonts → 300–700 or 400–900 only
Next.js real implementation looks up each family's axis metadata from a bundled fonts database and either fetches with no :wght@ (which makes Google serve the default variable font) or with the exact valid range. vinext's shim can't do the former because it always appends a :wght@ segment.
Suggested fix
The minimal, safe fix is to omit :wght@… when no weight is provided. Google Fonts CDN handles the no-weight case correctly and returns the variable font (or the default weight for static fonts):
- } else spec += `:wght@100..900`;
+ }
A more Next.js-faithful fix would be to bundle a slimmed-down axis database (hexere already has a similar google-fonts-meta.json for its own use, ~500 KB at most) and either (a) validate the requested weight range against the font's real axis, or (b) auto-select a full range when the caller passes no weight.
Impact
Every call site that uses next/font/google without an explicit weight option and happens to pick a font whose axis range isn't exactly 100–900 silently loads nothing. The failure mode is invisible in document.fonts (entries are registered by <link>-sourced @font-face, and a 400-response stylesheet registers no faces), which makes this bug very hard to diagnose — devs typically spend hours staring at Tailwind preflight cascade rules before checking the CDN response.
Related: this is one of three shim-level issues I hit while porting a Next.js app to vinext — see also:
- HMR state accumulation in the same shim (separate issue)
- Metadata file URL extension stripping (separate issue)
Workaround
Pass explicit weights that match the font's real axis range, e.g.:
const appFont = Sen({
subsets: ['latin'],
weight: ['400', '500', '600', '700', '800'],
});
Summary
When a caller uses
next/font/googlewithout passing an explicitweightoption, the vinext shim builds the Google Fonts URL with:wght@100..900. Many Google Fonts don't support that full range (e.g. Sen is 400–800), so Google Fonts CDN responds with HTTP 400 and the stylesheet never loads. The rendered page computesfont-family: 'Sen', sans-serifcorrectly, but because no@font-faceregisters, the browser paints the genericsans-seriffallback — so the bug looks like "font-family cascade issue" even though the real fault is the URL.Reproduction
app/layout.tsx:curl -Ithat URL:Root cause
dist/shims/font-google-base.js:80-95:The hardcoded
:wght@100..900assumes every Google font is a 100–900 variable axis. Many aren't:Next.js real implementation looks up each family's axis metadata from a bundled fonts database and either fetches with no
:wght@(which makes Google serve the default variable font) or with the exact valid range. vinext's shim can't do the former because it always appends a:wght@segment.Suggested fix
The minimal, safe fix is to omit
:wght@…when noweightis provided. Google Fonts CDN handles the no-weight case correctly and returns the variable font (or the default weight for static fonts):A more Next.js-faithful fix would be to bundle a slimmed-down axis database (hexere already has a similar
google-fonts-meta.jsonfor its own use, ~500 KB at most) and either (a) validate the requested weight range against the font's real axis, or (b) auto-select a full range when the caller passes no weight.Impact
Every call site that uses
next/font/googlewithout an explicitweightoption and happens to pick a font whose axis range isn't exactly 100–900 silently loads nothing. The failure mode is invisible indocument.fonts(entries are registered by<link>-sourced@font-face, and a 400-response stylesheet registers no faces), which makes this bug very hard to diagnose — devs typically spend hours staring at Tailwind preflight cascade rules before checking the CDN response.Related: this is one of three shim-level issues I hit while porting a Next.js app to vinext — see also:
Workaround
Pass explicit weights that match the font's real axis range, e.g.: