Files
cloudflare__vinext/examples/app-router-nitro
Nathan Nguyen 696948aca4 fix(next/font/google): use real axis range, validate options at build time (#885) (#893)
* shim: rewrite buildGoogleFontsUrl on top of the new build-url helper

Replace the inline URL builder in shims/font-google-base.ts with a thin
wrapper around the pure buildGoogleFontsUrl helper from PR 1
(packages/vinext/src/build/google-fonts/build-url.ts). The shim still
takes a FontOptions object and still returns a CSS URL, so external
callers and the existing 84 unit tests are unaffected.

Two visible behaviour changes:

1. Empty options no longer emit `:wght@100..900`. They emit no axis
   segment at all, which makes Google return the default static face
   (HTTP 200) for every font, including narrow-axis families like Sen
   that previously got HTTP 400. This is the dev fallback only; the
   build plugin always pre-resolves the real axis range from metadata
   in production.

2. Italic-only requests no longer drop the ital axis. Pre-fix the outer
   guard `weights.length > 0 || styles.length > 0` entered the block
   but the inner branch only handled the wght path, so a call like
   `Inter({ style: ['italic'] })` produced `family=Inter&display=swap`
   with no italic anywhere in the URL. The shim now derives an ital
   array from style and falls back to weight '400' when ital is set
   without an explicit weight, so Google has somewhere to attach the
   ital axis in the URL.

The shim deliberately does not import font-data.json or validate.ts: a
388 KB metadata file in the Worker bundle would dwarf the rest of the
shim, and the production path already has the metadata-aware variant.

Refs #885.

* plugin: validate options against metadata, surface HTTP errors as build errors

Replace the inline URL builder in plugins/fonts.ts with the pipeline added
in PR 1: validateGoogleFontOptions parses the call's options against the
bundled font metadata, getFontAxes resolves the actual `wght` range for
variable fonts, and buildGoogleFontsUrl emits the URL.

Three behaviour changes:

1. Variable fonts called with no `weight` now request the font's real
   axis range (Sen 400..800, Inter 100..900) instead of a hardcoded
   100..900. This is the user-visible fix for issue #885: Sen returns
   HTTP 200 with all five face files instead of HTTP 400.

2. Validation errors surface as build errors with the source file path
   attached. Unknown family, missing weight on a static font (Anton,
   Archivo Black, etc.), unknown weight, mixing 'variable' with explicit
   weights, axes on non-variable fonts, invalid display values, and
   missing/unknown subsets are now all caught at transform time. Pre-fix
   these silently produced URLs that Google rejected at request time.

3. HTTP non-2xx responses from Google are now distinguished from network
   errors. A 4xx/5xx is wrapped in GoogleFontsHttpError and surfaced
   with the failing URL plus the response body, because pre-fix the
   silent `catch { return; }` skipped self-hosting and let the runtime
   CDN path retry the same broken URL in the browser. Network errors
   (offline dev) still fall through silently to keep `vp dev` usable
   without a connection.

The fontCache key is the URL, so existing on-disk caches under
`.vinext/fonts/<hash>/` will be stale for affected fonts after this
change. One refetch per font on the next build.

Refs #885.

* tests: regression coverage for the #885 narrow-axis bug and validator throws

Five new tests in tests/font-google.test.ts plus subset additions to
existing fixtures whose Inter() / Roboto() calls were missing them.

Shim regressions:
- buildGoogleFontsUrl with empty options must not emit `:wght@100..900`
- buildGoogleFontsUrl with style-only options must keep the ital axis
  visible in the URL

Plugin regressions:
- Sen() with no weight must fetch `:wght@400..800`, not `:wght@100..900`
- unknown families throw a build error with the file path attached
- non-variable fonts called without an explicit weight throw at build
  time instead of silently emitting a broken URL
- HTTP 4xx/5xx from Google surfaces as a build error containing the URL
  and the response body
- network errors (DNS, ECONNREFUSED, AbortError) still fall through
  silently so offline dev keeps working

The existing plugin transform tests passed `Inter({ weight: '400' })`
without subsets; the validator now treats that as a missing-subsets
error (matches Next.js behaviour), so those fixtures gain
`subsets: ['latin']`. The two nested-brace regression tests still
exercise the parser's balanced-brace logic but use a benign
`_placeholder` field instead of `axes: { wght: 400 }`, which is
semantically invalid for Inter under the new validator.

Refs #885.

* address google font review feedback
2026-04-27 07:50:39 +01:00
..

App Router

A minimal example of a Next.js App Router application.

Demonstrates server components, client components, dynamic routes, and API route handlers.

Running Locally

  1. Install dependencies:
pnpm install
  1. Start the dev server:
pnpm dev
  1. Build for production:
pnpm build
  1. Preview the production build:
pnpm preview