This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A demo/integration POC showing how to consume fontdue-js@3.x (alpha) — the
framework-agnostic preload + preview API — from a TanStack Start SSR app.
The same preload pattern applies to any React-rendering SSR framework. Pin a
specific fontdue-js alpha version exactly (no caret/tilde — alpha versions
without a dot break npm range matching); the 3.x surface is unstable until
3.0.0 ships.
Sibling POCs validating the same API on other frameworks (useful for cross-checking patterns):
~/code/fontdue/fontdue-rr7-example— React Router 7 (root-route middleware).~/code/fontdue/fontdue-astro-example— Astro (src/middleware.ts).~/code/fontdue/fontdue-example-next— Next.js 15 App Router (RSC magic path).
cp .env.example .env(required before first run; setsVITE_FONTDUE_URL)npm installnpm run dev— runsvite dev --port 3000andgraphql-codegen --watchin parallel vianpm-run-all. Editing a.graphqlfile regeneratessrc/queries/operations-types.tsautomatically. App at http://localhost:3000.npm run codegen— one-shot codegen.npm run build— production build; writes the Netlify SSR function to.netlify/v1/functions/server.mjs.npm run typecheck—tsc --noEmit. The TanStack Router plugin regeneratessrc/routeTree.gen.tswhen the dev server runs (or on build); a fresh checkout that hasn't run dev yet may show a missing-route type error until the tree is generated.
The whole point of this repo is the SSR data layer wiring. Read these files together, not separately:
-
vite.config.ts— registersfontdue-js/vite(sets up CJS interop forreact-relay/relay-runtimeand friends,ssr.noExternal, dep pre-bundling, anddefine: { global: 'globalThis' }) and@netlify/vite-plugin-tanstack-start(emits the Netlify SSR function). If imports of fontdue-js subpaths break in dev or build, the fontdue-js plugin or its included-dep list is the first place to look. -
src/lib/graphql.ts—export const fetchGraphql = createFontdueFetch(), a single module-scoped server fetcher fromfontdue-js/server. It resolves the Fontdue URL fromVITE_FONTDUE_URL/PUBLIC_FONTDUE_URL/FONTDUE_URL, POSTs the query, unwrapsdata, and throwsFontdueNotFoundErroron a 404. There is no per-request binding: the global request middleware (below) wraps every request inrunWithPreview, so this fetcher — and every fontdue-js preload helper — automatically forwards the admin preview token when an admin is previewing, and sends a plain request otherwise. Call them with just their variables; preview rides the ambient context. -
src/lib/preview.ts+src/start.ts— the preview + caching layer. TanStack Start has no root-route middleware like RR7; instead a global request middleware (createMiddleware({ type: 'request' })) is registered on the Start instance viacreateStart({ requestMiddleware: [...] })insrc/start.ts. The Start plugin auto-discoverssrc/start.ts(it resolves astartentry from the src dir, exportingstartInstance) — no import or registration elsewhere is needed. The middleware'sserverfn gets{ request, next };next()returns the renderedResponse. It:- Preview. Wraps the render in
runWithPreview(request, …)fromfontdue-js/preview/server, which puts the admin token (from the preview cookie set by/api/preview) into an AsyncLocalStorage context for the whole render so every fetch/preload reveals unpublished fonts, and forces preview responses out of the shared CDN cache (Cache-Control: no-store). This relies on the middleware running in the same runtime as the render, which holds for the Netlify Functions (Node) SSR target. - CDN caching. Applies
Netlify-CDN-Cache-ControlSWR +Netlify-Cache-Tag: fontdueto public, non-preview HTML responses only.
- Preview. Wraps the render in
-
src/routes/__root.tsx— the layout-level data layer. ItsloaderrunsloadFontdueProviderQuery()andfetchGraphql<RootLayoutQuery>(…)in parallel — one round-trip for the whole layout.<FontdueProvider preloadedQuery={…}>commits the aux payload into the shared client Relay env on hydration so theme/banner/tracking render with no flash. It also mounts the admin preview toolbar automatically (shown only to logged-in admins).<StoreModal>is mounted with nopreloadedQuery— it's a lazy-only component (closed at SSR, fetches on open); don't add a preload there. Cache headers are NOT set here — the global middleware owns them for every page. -
Page route loaders (
src/routes/index.tsx,fonts.$slug.tsx,test-fonts.tsx) — eachloadercalls the matchingload*Queryfor the components it renders, in parallel viaPromise.all, alongside anyfetchGraphqlchrome fetch. Each component is rendered with its ownpreloadedQueryprop. The 6 components with a preload helper (BuyButton, CharacterViewer, NewsletterSignup, TestFontsForm, TypeTester, TypeTesters) are preloaded; the 3 lazy-only ones (StoreModal, CartButton, CustomerLoginForm) are just rendered. -
src/routes/api.preview.ts— server-route handlers (POST enter / DELETE exit) delegating tohandlePreviewRequestfromfontdue-js/preview. The toolbar POSTs a short-lived admin token here to set the preview cookies. The default path is/api/preview; to change it, setconfig.preview.endpointon<FontdueProvider>and rename the route. -
src/routes/api.revalidate.ts— POST-only server route. Validates?token=againstprocess.env.REVALIDATE_TOKENand calls Netlify'spurgeCache({ tags: ['fontdue'] }). Wired into Fontdue at Website settings → Deploy hook URL. -
src/queries/*.graphql— query documents imported with Vite's?rawsuffix so the string inlines at build time. Types live insrc/queries/operations-types.ts(committed), generated by@graphql-codegen/cli(config incodegen.ts).
Backend URL: VITE_FONTDUE_URL in .env. fontdue-js auto-reads it from
import.meta.env on both server and client — no explicit configure() call.
The default tenant https://example.fontdue.xyz has CORS allow-listed
http://localhost:3000; pointing at another tenant requires that tenant to
allow-list the dev origin.
- Always preload in parallel with
Promise.allwhen a route has multipleload*Query/fetchGraphqlcalls — that's the established pattern. - The same component import (e.g.
fontdue-js/TypeTester) yields both the loader (load*Querynamed export) and the component (default export). - Mount exactly one
<FontdueProvider>per page (in__root.tsx). It owns the aux UI (ThemeConfig, TestModeBanner, ConsentBanner, Tracking, the admin toolbar); per-component islands self-wrap internally without claiming that slot. - Routes are file-system based (
src/routes/). The TanStack Router plugin regeneratessrc/routeTree.gen.ts. - Preview: never thread the token by hand.
runWithPreview(wired once insrc/start.ts) makes every server fetch/preload pick it up from ambient context. If you ever move to a split edge runtime where AsyncLocalStorage can't cross to the render, fall back to reading the cookie and passingpreviewAuthHeaders(token)via each call'sheadersoption.
CDN SWR caching is set in src/lib/preview.ts (the global request middleware),
not per route. Default policy: s-maxage=300 (5 min fresh on edge) +
stale-while-revalidate=86400. Browser Cache-Control: max-age=0, must-revalidate so users always hit the edge. Preview responses are forced to
private, no-store by runWithPreview, and /api/* + non-HTML responses are
left untouched.