Skip to content

fontdue/example-react-router

Repository files navigation

fontdue-js on React Router 7

A working example of using fontdue-js in a React Router 7 SSR app — no client-side query refetch on hydration, no theming flash, no per-island duplicate fetches.

It uses fontdue-js's framework-agnostic preload API, so the same pattern works in any React-rendering SSR framework (Astro, TanStack Start, Vike, Remix, etc.).

What it demonstrates

  • TypeTester islands server-rendered and hydrating without a refetch.
  • The unified TypeTester entry (fontdue-js/TypeTester) used directly from a route loader — same import path for both the loader (loadTypeTesterQuery) and the component (default export).
  • Backend URL configured once via VITE_FONTDUE_URL. No URL passed at the call site.
  • Direct GraphQL fetches from route loaders alongside the fontdue-js preload helpers (app/lib/graphql.ts + app/queries/*.graphql) — equivalent to the async function pattern in the Next.js App Router example and the frontmatter pattern in the Astro example.
  • CDN stale-while-revalidate caching on Netlify with on-demand purge via /api/revalidate.

Setup

cp .env.example .env
npm install
npm run dev

Open http://localhost:5173.

The default .env.example points at https://example.fontdue.xyz, which has CORS allow-listed http://localhost:5173. Point VITE_FONTDUE_URL at your own Fontdue site if you have one — the client origin will need to be in your site's allowed origins.

How the integration is wired

Three files do the work:

  • .envVITE_FONTDUE_URL is your Fontdue site's origin. The VITE_ prefix exposes it to client code (Vite convention); fontdue-js auto-reads PUBLIC_FONTDUE_URL / VITE_FONTDUE_URL from import.meta.env on both server and client. No explicit configure() call needed.

  • app/root.tsx — preloads aux UI data (theme custom properties, test-mode flag, server-side tracking config) and the cart count, then renders <FontdueProvider preloadedQuery={…}>. This is the layout-level Fontdue runtime; it commits the preloaded payload to the Relay env on hydration so theme/banner/tracking render without a flash. The loader here also runs the RootLayout GraphQL fetch so the header renders the dynamic logo, nav, and footer from the Fontdue admin.

  • app/routes/home.tsx — per-page component preloads:

    export async function loader() {
      const indexData = await fetchGraphql<IndexQuery>("Index", IndexDoc);
      const collections = /* …extract from indexData… */;
      const testerPreloads = await Promise.all(
        testerCollections.map((c) =>
          loadTypeTesterQuery({ familyName: c.featureStyle!.cssFamily!, styleName: c.featureStyle!.name! }),
        ),
      );
      return { collections, testerCollections, testerPreloads };
    }

    The component then renders <TypeTester preloadedQuery={preload} content="…" fontSize={48} />. loadTypeTesterQuery runs in the route loader (server). <TypeTester> server-renders as HTML, then hydrates with the preloaded payload — no network call on hydration. Multiple islands on the same page share one Relay env + one Redux store via module-level singletons.

Querying the Fontdue GraphQL API directly

Aside from the preload helpers, you can query viewer { … } from a route loader the same way the Astro example queries it from frontmatter or the Next.js example queries it from a server component. Three pieces:

  • app/lib/graphql.ts — a 25-line fetchGraphql<Q, V>(name, query, variables) helper. import.meta.env.VITE_FONTDUE_URL provides the endpoint, so there's no hard-coded URL.

  • app/queries/*.graphql — query documents (RootLayout, Index, Font). Imported with Vite's ?raw suffix so the string is inlined at build time:

    import IndexDoc from "../queries/Index.graphql?raw";
  • app/queries/operations-types.ts — generated by @graphql-codegen/cli from the .graphql documents and the live schema (config in codegen.ts). npm run dev runs codegen in --watch alongside react-router dev (via npm-run-all), so editing a .graphql file regenerates types automatically. npm run codegen is the one-shot equivalent. The file is committed so contributors don't need a live Fontdue URL just to type-check. The fetchGraphql helper takes the response and variables types as generics:

    fetchGraphql<FontQuery, FontQueryVariables>("Font", FontDoc, { slug });

Calling it from a route loader:

export async function loader() {
  const data = await fetchGraphql<IndexQuery>("Index", IndexDoc);
  const collections = data.viewer.fontCollections?.edges
    ?.flatMap((edge) => edge?.node ? [edge.node] : []) ?? [];
  return { collections };
}

Run the GraphQL fetch and the fontdue-js preload helpers in the same Promise.all so the entire page costs one network round-trip:

const [layoutData, fontduePreload, cartPreload] = await Promise.all([
  fetchGraphql<RootLayoutQuery>("RootLayout", RootLayoutDoc),
  loadFontdueProviderQuery(),
  loadCartButtonQuery(),
]);

Three routes use this pattern: app/root.tsx (logo, nav, settings), app/routes/home.tsx (collections list + preloaded testers for the first two), and app/routes/fonts.$slug.tsx (font detail name/description/hero alongside <TypeTesters> / <CharacterViewer> / <BuyButton> islands).

Required Vite SSR config

vite.config.ts includes one fontdue-js-specific line:

import fontdueJs from "fontdue-js/vite";

export default defineConfig({
  plugins: [tailwindcss(), reactRouter(), fontdueJs()],
});

Why: fontdue-js publishes per-file ESM (it isn't bundled at publish time). Some of its transitive deps (react-relay, relay-runtime, draft-js, fbjs) are CJS and use module.exports = require('./lib') re-export shapes that defeat strict ESM named-import resolution in both Node SSR and browser contexts. The fontdueJs() plugin wires up vite-plugin-cjs-interop, ssr.noExternal: ['fontdue-js'], optimizeDeps.include, and define: { global: 'globalThis' } to make those work.

Deploying to Netlify

This example is wired for Netlify SSR via the official adapter @netlify/vite-plugin-react-router, which generates .netlify/v1/functions/react-router-server.mjs at build time. Netlify deploys that as the SSR request handler. (Netlify's framework auto-detection identifies RR7 but does not wire up the function on its own — the plugin is required.)

To deploy a fork:

  1. Build settings (Netlify UI):
    • Build command: npm run build
    • Publish directory: build/client
    • Functions directory: leave blank — the plugin writes to .netlify/v1/functions/ automatically.
  2. Environment variables: add VITE_FONTDUE_URL under Site configuration → Environment variables. VITE_* vars are inlined at build time, so it must be set before the first build runs. Also set REVALIDATE_TOKEN to a long random string — required by /api/revalidate (see below).
  3. CORS allow-list: once Netlify gives you the deploy URL (e.g. https://your-site.netlify.app), add it to your Fontdue site's allowed origins. If you want PR previews to work, allow-list the https://deploy-preview-*--your-site.netlify.app pattern too.

Caching and revalidation

Pages are SSR but cached on Netlify's CDN with stale-while-revalidate, so requests after the first one are served from the edge in milliseconds. The headers export in app/root.tsx sets:

Netlify-CDN-Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400
Netlify-Cache-Tag: fontdue

After the SWR window the edge serves the stale copy and regenerates in the background — the user never waits for the upstream GraphQL call. To force a refresh on demand (e.g. when fonts change in your Fontdue admin), app/routes/api.revalidate.ts validates the token query param against REVALIDATE_TOKEN and calls Netlify's purgeCache({ tags: ['fontdue'] }). That route exports its own headers returning Cache-Control: no-store so the API endpoint itself isn't cached — RR7's leaf-route headers override the root's.

Wire it up in Fontdue: Website settings → Deploy hook URL. Paste:

https://your-site.netlify.app/api/revalidate?token=YOUR_REVALIDATE_TOKEN

Fontdue will POST to this URL whenever you publish changes; cached HTML on Netlify's edge is invalidated and the next request regenerates against fresh data.

Today the Fontdue API doesn't include a collection id/slug in the deploy-hook payload, so every page is purged together. If/when it does, switch to per-collection tags (fontdue:${slug}) and purge selectively.

React Router requirements

  • ssr: true in react-router.config.ts — the preload runs in route loaders, which need SSR.
  • React Router 7.x with the Vite plugin (@react-router/dev/vite).

Status

This example targets fontdue-js v3, the release that introduced the framework-agnostic preload API.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors