Skip to content

fontdue/example-astro

Repository files navigation

fontdue-js on Astro

A working example of using fontdue-js in an Astro site with full SSR — 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 (React Router 7, TanStack Start, Vike, Remix, etc.).

What it demonstrates

  • Multiple fontdue-js islands on one page (TypeTester / TypeTesters / CharacterViewer / BuyButton / TestFontsForm / NewsletterSignup / CartButton), all server-rendered, all hydrating without a refetch.
  • The unified component entry (fontdue-js/TypeTester etc.) used directly from .astro files — same import path yields both the loader (load*Query) and the component (default export).
  • Backend URL set once via the PUBLIC_FONTDUE_URL env var; fontdue-js auto-reads it from import.meta.env on both server and client. No configure() call.
  • Direct GraphQL fetches from Astro frontmatter alongside the fontdue-js preload helpers (src/lib/graphql.ts + src/queries/*.graphql) — equivalent to the async function pattern in the Next.js App Router example.
  • Stale-while-revalidate caching at Netlify's CDN with on-demand purgeCache via POST /api/revalidate wired to Fontdue's deploy hook.

Setup

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

Open http://localhost:4321.

The default .env.example points at https://example.fontdue.xyz, which has CORS allow-listed http://localhost:4321. Point PUBLIC_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:

  • .envPUBLIC_FONTDUE_URL is your Fontdue site's origin. The PUBLIC_ prefix exposes it to client code (Astro 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.

  • src/layouts/Layout.astro — preloads aux UI data (theme custom properties, test-mode flag, server-side tracking config, cart count) and renders the <FontdueProvider> and <CartButton> islands plus a closed <StoreModal>. 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.

    ---
    import 'fontdue-js/fontdue.css';
    import { loadFontdueProviderQuery } from 'fontdue-js';
    import FontdueProvider from 'fontdue-js/FontdueProvider';
    
    const fontduePreload = await loadFontdueProviderQuery();
    ---
    <html>
      <body>
        <FontdueProvider client:load preloadedQuery={fontduePreload} />
        <slot />
      </body>
    </html>
  • src/pages/index.astro — per-page component preloads:

    ---
    import TypeTester, { loadTypeTesterQuery } from 'fontdue-js/TypeTester';
    import Layout from '../layouts/Layout.astro';
    
    const preloaded = await loadTypeTesterQuery({ familyName: '', styleName: '' });
    ---
    
    <Layout>
      <TypeTester client:load preloadedQuery={preloaded} content="" fontSize={64} />
    </Layout>

    loadTypeTesterQuery runs in the Astro frontmatter (server). <TypeTester> renders server-side as HTML, then client:load 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. The same <TypeTester> import works inside an existing <FontdueProvider> tree (e.g. Next layouts) using the lazy {familyName, styleName} shape — it auto-detects the parent provider and skips its own self-wrap.

Querying the Fontdue GraphQL API directly

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

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

  • src/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';
  • src/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 astro 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 src/pages/index.astro:

---
import { fetchGraphql } from '../lib/graphql';
import IndexDoc from '../queries/Index.graphql?raw';
import type { IndexQuery } from '../queries/operations-types';

const data = await fetchGraphql<IndexQuery>('Index', IndexDoc);
const collections = data.viewer.fontCollections?.edges
  ?.flatMap((edge) => edge?.node ? [edge.node] : []) ?? [];
---

The frontmatter is the SSR data layer — equivalent to the Next.js App Router's async function Page(). 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 Astro routes use this pattern: src/layouts/Layout.astro (nav, settings, logo), src/pages/index.astro (collections list + preloaded testers for the first two), and src/pages/fonts/[slug].astro (font detail name/description/hero alongside <TypeTesters> / <CharacterViewer> / <BuyButton> islands).

Required Vite SSR config

astro.config.mjs includes one fontdue-js-specific line:

import fontdueJs from 'fontdue-js/vite';

export default defineConfig({
  // …
  vite: {
    plugins: [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 to rewrite named imports of those deps to default-import + destructure.
  • ssr.noExternal: ['fontdue-js'] so the cjs-interop transform runs over fontdue-js's own source (otherwise Vite externalizes it and Node tries to named-import from CJS directly).
  • optimizeDeps.include for the same set plus react-redux, so esbuild pre-bundles them for the browser.
  • define: { global: 'globalThis' } because fbjs (transitive via draft-js) references Node's global at module init.

Astro requirements

  • output: 'server' (or 'hybrid' with the page opted in) — the preload runs in frontmatter, which needs SSR.
  • An SSR adapter — this example uses @astrojs/netlify. The fontdue-js integration itself is adapter-agnostic; only the cache headers in src/middleware.ts and the purgeCache call in src/pages/api/revalidate.ts are Netlify-specific.
  • @astrojs/react for the React renderer.

Deploying to Netlify

This example is wired for Netlify SSR. To deploy a fork:

  1. Build settings (Netlify UI):
    • Build command: npm run build
    • Publish directory: dist
    • Base directory and Functions directory: leave blank — @astrojs/netlify writes the SSR function to the right place automatically.
  2. Environment variables: add PUBLIC_FONTDUE_URL under Site configuration → Environment variables. PUBLIC_* vars are inlined at build time by Vite, 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. src/middleware.ts 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), src/pages/api/revalidate.ts validates the token query param against REVALIDATE_TOKEN and calls Netlify's purgeCache({ tags: ['fontdue'] }).

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.

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