fontdue-js
v3.7.0
Published
React components for [Fontdue](https://fontdue.com) sites. Framework-agnostic: works in Next.js, Astro, React Router 7, TanStack Start, Vike, Remix, and any other React SSR or client-only environment.
Readme
fontdue-js
React components for Fontdue sites. Framework-agnostic: works in Next.js, Astro, React Router 7, TanStack Start, Vike, Remix, and any other React SSR or client-only environment.
Requirements
react18 or 19node>= 18- TypeScript (if used) with
moduleResolutionset tonode16,nodenext, orbundlerso the package'sexportsmap resolves:
{
"compilerOptions": {
"moduleResolution": "nodenext"
}
}fontdue-js is published ESM-only.
Installation
npm install fontdue-js@latestConfiguration
Point fontdue-js at your Fontdue URL via an environment variable. Pick the one your framework already uses for public env vars:
| Framework | Variable |
| --- | --- |
| Astro | PUBLIC_FONTDUE_URL |
| React Router 7 / TanStack Start / Vike / Remix (Vite) | VITE_FONTDUE_URL |
| Next.js | NEXT_PUBLIC_FONTDUE_URL |
| Other / framework-less SSR | FONTDUE_URL |
A single variable covers both server and client in every supported framework — Vite exposes import.meta.env.PUBLIC_* / VITE_* on both sides, and Next inlines NEXT_PUBLIC_* on both sides.
Setup
Pick the section that matches your framework. All four examples below have a working repo linked at the bottom.
The general pattern in every framework:
- Mount
<FontdueProvider>once at the layout level — it sets up the Relay environment + Redux store and renders auxiliary UI (theme config, test-mode banner, consent banner, analytics tracking). - Mount
<StoreModal />once, alongside the provider — opens when a<BuyButton>or<CartButton>is clicked. - (SSR only.) Preload in the layout with
loadFontdueProviderQuery()and pass it as<FontdueProvider preloadedQuery={…}>. This ensures the page hydrates with preloaded data. - (SSR only.) Preload per-page components with their
load{Component}Query()helpers in route loaders / frontmatter / server components.
The shape of step 3 and 4 is the only thing that changes between frameworks.
No Vite plugin needed — wrap your Next config with withFontdue instead:
// next.config.mjs
import { withFontdue } from "fontdue-js/next/config";
export default withFontdue({
// your Next config
});The simplest setup omits the layout preload — with React Server Components, each fontdue-js component preloads its own query internally on the server and streams to the client.
// app/layout.tsx
import FontdueProvider from "fontdue-js/FontdueProvider";
import StoreModal from "fontdue-js/StoreModal";
import "fontdue-js/fontdue.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<FontdueProvider>
{children}
<StoreModal />
</FontdueProvider>
</body>
</html>
);
}// app/fonts/[slug]/page.tsx — no explicit preload needed.
import TypeTester from "fontdue-js/TypeTester";
export default function FontPage() {
return <TypeTester familyName="Example" styleName="Regular" />;
}Beyond the components, Next.js projects get a few extra entry points — config wrapping, cache revalidation, and helpers for your own GraphQL fetches. See Next.js adapter.
Example repo: fontdue/fontdue-example-next
Add the Vite plugin to astro.config.mjs:
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import fontdueJs from "fontdue-js/vite";
export default defineConfig({
integrations: [react()],
vite: { plugins: [fontdueJs()] },
});In Astro, every client:* component is its own React island — <FontdueProvider> doesn't form a React parent of your page components. Instead, it's a sibling island that sets up Fontdue's core on your site. Per-page components (<TypeTester>, etc.) self-wrap their own context when no parent provider is in scope.
Because of this, there's no global config in Astro: config set on <FontdueProvider> doesn't reach your page components. Pass config to each component instead — see UI config.
---
// src/layouts/Layout.astro
import FontdueProvider, { loadFontdueProviderQuery } from "fontdue-js/FontdueProvider";
import StoreModal from "fontdue-js/StoreModal";
import "fontdue-js/fontdue.css";
const fontduePreload = await loadFontdueProviderQuery();
---
<html lang="en">
<body>
<FontdueProvider client:load preloadedQuery={fontduePreload} />
<StoreModal client:load />
<slot />
</body>
</html>Per-page preload runs in frontmatter:
---
// src/pages/fonts/[slug].astro
import Layout from "../../layouts/Layout.astro";
import TypeTester, { loadTypeTesterQuery } from "fontdue-js/TypeTester";
const preloaded = await loadTypeTesterQuery({
familyName: "Example",
styleName: "Regular",
});
---
<Layout>
<TypeTester client:load preloadedQuery={preloaded} content="The quick brown fox" fontSize={64} />
</Layout>Example repo: fontdue/example-astro
Add the Vite plugin to vite.config.ts:
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";
import fontdueJs from "fontdue-js/vite";
export default defineConfig({
plugins: [reactRouter(), fontdueJs()],
});Preload in the root route's loader, pass the result through loaderData:
// app/root.tsx
import { Outlet } from "react-router";
import FontdueProvider, { loadFontdueProviderQuery } from "fontdue-js/FontdueProvider";
import StoreModal from "fontdue-js/StoreModal";
import "fontdue-js/fontdue.css";
import type { Route } from "./+types/root";
export async function loader() {
return { fontduePreload: await loadFontdueProviderQuery() };
}
export default function App({ loaderData }: Route.ComponentProps) {
return (
<FontdueProvider preloadedQuery={loaderData.fontduePreload}>
<Outlet />
<StoreModal />
</FontdueProvider>
);
}Per-page preload mirrors the same shape:
// app/routes/fonts.$slug.tsx
import TypeTester, { loadTypeTesterQuery } from "fontdue-js/TypeTester";
export async function loader() {
return {
preloadedQuery: await loadTypeTesterQuery({
familyName: "Example",
styleName: "Regular",
}),
};
}
export default function FontPage({ loaderData }) {
return <TypeTester preloadedQuery={loaderData.preloadedQuery} content="…" fontSize={64} />;
}Example repo: fontdue/example-react-router
Add the Vite plugin to vite.config.ts (alongside TanStack's plugin). Preload in the root route's loader:
// src/routes/__root.tsx
import { Outlet, createRootRoute } from "@tanstack/react-router";
import FontdueProvider, { loadFontdueProviderQuery } from "fontdue-js/FontdueProvider";
import StoreModal from "fontdue-js/StoreModal";
import "fontdue-js/fontdue.css";
export const Route = createRootRoute({
loader: async () => ({ fontduePreload: await loadFontdueProviderQuery() }),
component: RootComponent,
});
function RootComponent() {
const { fontduePreload } = Route.useLoaderData();
return (
<FontdueProvider preloadedQuery={fontduePreload}>
<Outlet />
<StoreModal />
</FontdueProvider>
);
}Per-page preload uses each route's loader:
// src/routes/fonts.$slug.tsx
import { createFileRoute } from "@tanstack/react-router";
import TypeTester, { loadTypeTesterQuery } from "fontdue-js/TypeTester";
export const Route = createFileRoute("/fonts/$slug")({
loader: async () => ({
preloadedQuery: await loadTypeTesterQuery({
familyName: "Example",
styleName: "Regular",
}),
}),
component: FontPage,
});
function FontPage() {
const { preloadedQuery } = Route.useLoaderData();
return <TypeTester preloadedQuery={preloadedQuery} content="…" fontSize={64} />;
}Example repo: fontdue/example-tanstack
Add fontdueJs() to your Vite plugins. Run loadFontdueProviderQuery() wherever your framework loads layout-level data (Vike's +data.ts, Remix's root loader, etc.) and pass the result to <FontdueProvider preloadedQuery>. The component-level load*Query() helpers work the same way for per-page data.
Vike example — +data.ts for the layout, plus a per-page data loader. Re-export Data = Awaited<ReturnType<typeof data>> from each +data.ts so useData<Data>() gets a proper type without restating the shape:
// pages/+data.ts
import { loadFontdueProviderQuery } from "fontdue-js/FontdueProvider";
export const data = async () => ({
fontduePreload: await loadFontdueProviderQuery(),
});
export type Data = Awaited<ReturnType<typeof data>>;// pages/+Layout.tsx
import { useData } from "vike-react/useData";
import FontdueProvider from "fontdue-js/FontdueProvider";
import StoreModal from "fontdue-js/StoreModal";
import "fontdue-js/fontdue.css";
import type { Data } from "./+data";
export default function Layout({ children }: { children: React.ReactNode }) {
const { fontduePreload } = useData<Data>();
return (
<FontdueProvider preloadedQuery={fontduePreload}>
{children}
<StoreModal />
</FontdueProvider>
);
}// pages/fonts/@slug/+data.ts
import { loadTypeTesterQuery } from "fontdue-js/TypeTester";
export const data = async () => ({
preloadedQuery: await loadTypeTesterQuery({
familyName: "Example",
styleName: "Regular",
}),
});
export type Data = Awaited<ReturnType<typeof data>>;// pages/fonts/@slug/+Page.tsx
import { useData } from "vike-react/useData";
import TypeTester from "fontdue-js/TypeTester";
import type { Data } from "./+data";
export default function FontPage() {
const { preloadedQuery } = useData<Data>();
return <TypeTester preloadedQuery={preloadedQuery} content="…" fontSize={64} />;
}Remix follows the same shape with root loader / route loader + useLoaderData(). We don't ship an example repo for these yet — open an issue if you'd like one.
Mount <FontdueProvider> (no preloadedQuery) and render components with their lazy props ({collectionId}, {familyName, styleName}, etc.). They fetch on mount.
import FontdueProvider from "fontdue-js/FontdueProvider";
import StoreModal from "fontdue-js/StoreModal";
import TypeTester from "fontdue-js/TypeTester";
import "fontdue-js/fontdue.css";
export default function App() {
return (
<FontdueProvider>
<TypeTester familyName="Example" styleName="Regular" />
<StoreModal />
</FontdueProvider>
);
}Next.js adapter
Next.js App Router projects get a few extra entry points beyond the components. The example repo wires up all of them.
withFontdue — next.config wrapper
// next.config.mjs
import { withFontdue } from "fontdue-js/next/config";
export default withFontdue({
// your Next config
});What it installs:
- Image settings —
images.remotePatternsentries for Fontdue's image hosts (plusdangerouslyAllowSVG, since font specimens are often SVGs), merged with your ownimagesconfig. - Blocking metadata — Next's streamed metadata locks in a
200response before anotFound()thrown duringgenerateMetadatacan take effect (vercel/next.js#82041).withFontduesetshtmlLimitedBotsto match every user agent so metadata rendering blocks the response. This only coversgenerateMetadata: anotFound(),redirect()or error in the page itself gets its status from whether the page shell rendered, so keep the page out of any Suspense boundary you want a real status for — a route's ownloading.tsxstreams the shell as200before the page runs, which is the same upstream issue.
The rest of your config — rewrites included — passes through unchanged.
Optional: image optimization on Cloudflare
If you have a Cloudflare zone with image transformations enabled, set:
NEXT_PUBLIC_FONTDUE_IMAGE_HOST=img.your-domain.com
NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS=cdn.fontdue.comnext/image optimization then moves to the Cloudflare edge, and your deployment needs neither the /_next/image endpoint nor sharp. NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS (comma-separated hostnames) should mirror the transformation host's allowed source origins — sources on other hosts are served as originals rather than as transform URLs Cloudflare would refuse. Both variables must be present when next build runs (the loader is inlined into the client bundle), not just at serve time.
Updating content: /api/revalidate
fontdue-js's server-side fetches are cached by Next and tagged graphql. Re-export the deploy-hook route handler:
// app/api/revalidate/route.ts
export { POST } from "fontdue-js/next/revalidate";and set the Deploy hook URL in your Fontdue admin (Settings → Website settings) to https://your-site.example/api/revalidate. Fontdue calls it whenever your site's content changes, purging everything tagged graphql so the next request renders fresh.
fontdue-js's own server-side fetches opt into Next's data cache (and the graphql tag) automatically — static pages revalidated by the deploy hook is the intended way to run a Fontdue site, not dynamic rendering. Give your own fetches the same treatment; the setup below shows how.
Local development. The data cache and the deploy hook are production-only. In
next dev, fontdue-js skips the data cache entirely (keyed offNODE_ENV), so every render fetches fresh and admin content changes show on the next reload — no/api/revalidatecall needed. This is deliberate: Next's on-demandrevalidateTagdoesn't reliably purge the dev data cache, so caching it locally would just serve stale content. Caching and revalidation switch back on automatically in production builds.
Your own GraphQL fetches
Use the same createFontdueFetch as every other framework. Mounting <FontdueProvider> in your layout is enough to wire it up — there's no per-render setup call:
// src/lib/graphql.ts
import { createFontdueFetch } from "fontdue-js/server";
export const fetchGraphql = createFontdueFetch();// any page / layout / generateMetadata
import { fetchGraphql } from "@/lib/graphql";
export default async function Page() {
const data = await fetchGraphql<IndexQuery>("Index.graphql");
// …
}createFontdueFetch() resolves its config per fetch from Next's request context: it points your fetches at your site (NEXT_PUBLIC_FONTDUE_URL), applies the cache tags that tie them into /api/revalidate, and — when a logged-in admin is previewing — forwards the admin token and serves the render live (see Admin preview). Resolving per fetch means soft navigations that re-render only the page segment are covered too, with nothing to repeat per entry point.
Route handlers (robots/sitemap) run outside a React render, but the same createFontdueFetch() still resolves your site from NEXT_PUBLIC_FONTDUE_URL, so they need no extra setup. Their fetches aren't added to Next's data cache (there's no per-render config to tag them), which is what you want for robots/sitemap — they're cheap and rarely change.
Migrating a Next.js site from v2
For a site built on the example repo (App Router), v3 is a small, mechanical upgrade — component imports, props, and NEXT_PUBLIC_FONTDUE_URL all stay the same. Work through this checklist:
Install the v3 line:
npm install fontdue-js@alphav3 is ESM-only and needs
react18/19 andnode>= 18. If TypeScript can't resolve the imports, setmoduleResolutionto"bundler"(ornode16/nodenext) intsconfig.json— see Requirements.Wrap your Next config with
withFontdue. It installs the Fontdue image settings (remotePatterns,dangerouslyAllowSVG) and thehtmlLimitedBotsworkaround that lets anotFound()ingenerateMetadatareturn a real 404, so you can delete those from your own config — for many sites the whole file shrinks to:// next.config.mjs (replaces next.config.js — the package is ESM) import { withFontdue } from "fontdue-js/next/config"; export default withFontdue({});Replace your
/api/revalidatehandler with the re-export. v3 caches and tags its own server-side fetches, and the shared handler purges everything carrying thegraphqltag:// app/api/revalidate/route.ts export { POST } from "fontdue-js/next/revalidate";Keep the Deploy hook URL in your Fontdue admin pointed at it.
Delete caching workarounds you no longer need.
export const fetchCache = "default-cache"in the layout (if you added it) is obsolete — fontdue-js opts its own fetches into the data cache now. For your app's own GraphQL fetches, move the transport tocreateFontdueFetch; mounting<FontdueProvider>then handles caching,/api/revalidate, and admin preview for you — see Your own GraphQL fetches.Remove the
urlprop from<FontdueProvider>if you passed one. It never configured server-side fetches (server components have no context); v3 resolves everything fromNEXT_PUBLIC_FONTDUE_URL. The prop still works as a client-side runtime override, but with the env var set you don't need it.Rename
useFontStyletouseFontif you use it (the old name still works as an alias).
What you get for it: server components now render the embeds' full HTML on the server (v2 hydrated some of them empty and fetched client-side), every fontdue-js fetch is cached and purged per-site by the deploy hook, and the optional Cloudflare image loader can replace the in-process optimizer entirely.
If you're starting fresh instead of migrating, fork the example repo — it ships in this shape already.
Server-side GraphQL fetches
Beyond the preload helpers, you'll often run your own GraphQL queries server-side — page chrome, metadata, custom sections. fontdue-js/server exports a ready-made fetcher so you don't hand-roll the transport:
import { createFontdueFetch } from "fontdue-js/server";
// One fetcher for the whole app. Resolves the Fontdue URL from the environment
// (FONTDUE_URL / PUBLIC_FONTDUE_URL / VITE_FONTDUE_URL).
export const fetchGraphql = createFontdueFetch();
// In a loader / frontmatter / server component:
const data = await fetchGraphql<IndexQuery>("Index", indexQuery, { slug });createFontdueFetch({ url?, headers?, cacheTags? }) returns fetchGraphql(operationName, query, variables?). It POSTs to /graphql, unwraps data, throws on GraphQL errors, and throws FontdueNotFoundError when the host doesn't resolve to a site — catch it to render your framework's 404. It's the same fetcher in every framework; each input resolves per call:
- url — the explicit option, else
FONTDUE_URL/PUBLIC_FONTDUE_URL/VITE_FONTDUE_URLfrom the environment. - headers — the explicit option merged over the ambient admin preview context (
runWithPreview), so the preview token is forwarded automatically. - cacheTags — when present, the fetch opts into Next's data cache (
force-cache+ tags) so/api/revalidatecan purge it; absent/empty leaves it uncached. Caching only kicks in for production builds — innext devthe fetch stays uncached so local content is always fresh (see Updating content). The Next hints are inert in other runtimes, where HTML is cached at the response/CDN layer instead.
Upgrading a hand-rolled fetch. If you already have something like this:
// Before — hand-rolled.
async function fetchGraphql(name, query, variables) {
const res = await fetch(`${import.meta.env.PUBLIC_FONTDUE_URL}/graphql`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ query, variables }),
});
const json = await res.json();
if (json.errors) throw new Error(json.errors[0].message);
return json.data;
}replace it with:
// After.
import { createFontdueFetch } from "fontdue-js/server";
export const fetchGraphql = createFontdueFetch();You get URL resolution, error handling, FontdueNotFoundError, and automatic preview-token forwarding for free.
Next.js: the same
createFontdueFetch— mounting<FontdueProvider>ties it into Next's data cache, the/api/revalidatedeploy hook, and admin preview, with no per-render setup call. See Your own GraphQL fetches under the Next adapter.
Admin preview
Logged-in Fontdue admins get a preview toolbar — rendered automatically by <FontdueProvider>, hidden for everyone else — that reveals hidden (unpublished) fonts across the whole site. The toolbar brokers a short-lived admin token and POSTs it to a small preview route on your own origin; from then on, server renders forward the token so GraphQL returns the unpublished content. The public never has the cookie, so their renders stay sessionless and cacheable.
Two entry points cover this: fontdue-js/preview (the portable cookie contract) and fontdue-js/preview/server (the ambient wiring).
1. Add the preview route at /api/preview — the toolbar POSTs to enter preview and DELETEs to exit. handlePreviewRequest is a Web-standard Request → Response handler, so it drops into any Fetch-API framework:
// Astro — src/pages/api/preview.ts
import { handlePreviewRequest } from "fontdue-js/preview";
export const ALL = ({ request }) => handlePreviewRequest(request);
export const prerender = false;// React Router 7 — app/routes/api.preview.ts
import { handlePreviewRequest } from "fontdue-js/preview";
export const action = ({ request }) => handlePreviewRequest(request);The path is configurable via config.preview.endpoint on <FontdueProvider> (default /api/preview) — mount the route to match.
2. Forward the token on server renders. The recommended way is ambient: wrap each request in runWithPreview (from fontdue-js/preview/server) in your framework's middleware. While it's active, every createFontdueFetch call and every load*Query() preload forwards the token automatically — no per-call plumbing — and preview responses are forced out of any shared/CDN cache so an admin's render is never served to the public.
// Astro — src/middleware.ts
import { runWithPreview } from "fontdue-js/preview/server";
export const onRequest = (ctx, next) => runWithPreview(ctx.request, next);// React Router 7 — root route (with future.v8_middleware enabled)
import { runWithPreview } from "fontdue-js/preview/server";
export const middleware = [({ request }, next) => runWithPreview(request, next)];runWithPreview uses AsyncLocalStorage, so it works wherever middleware shares a runtime with the render — Node (the default SSR target on Netlify/Vercel), Deno, Bun.
Explicit alternative. Where the ambient context can't propagate (e.g. middleware running in a separate runtime from the render, such as Astro's edgeMiddleware: true), read the token yourself and pass it as the { headers } option, which always overrides the ambient context:
import { readPreviewToken, previewAuthHeaders } from "fontdue-js/preview";
const headers = previewAuthHeaders(readPreviewToken(request.headers.get("cookie")));
const fetchGraphql = createFontdueFetch({ headers });
const preload = await loadTypeTesterQuery(vars, { headers });previewAuthHeaders returns {} when there's no token, so it's always safe to pass.
Next.js
Next uses draft mode rather than ambient context. The preview route layers draftMode() on top of handlePreviewRequest:
// app/api/preview/route.ts
import { draftMode } from "next/headers";
import { handlePreviewRequest } from "fontdue-js/preview";
export async function POST(request: Request) {
const response = await handlePreviewRequest(request);
if (response.ok) (await draftMode()).enable();
return response;
}
export async function DELETE(request: Request) {
const response = await handlePreviewRequest(request);
(await draftMode()).disable();
return response;
}That's the only preview-specific code. Mounting <FontdueProvider> registers a resolver that reads draft mode and the token cookie per server fetch, so the whole render forwards the token — your own fetches and the embedded components' server preloads alike — and hidden fonts show up everywhere, served live. No per-render call is needed (see Your own GraphQL fetches).
This includes the Fontdue React components. Rendered in a Server Component (the App Router default), <TypeTester>, <CharacterViewer> and friends preload their data on the server through that same resolver — even though you never call a load*Query helper yourself — so they reveal their hidden fonts too. The only embeds that don't depend on it are ones that fetch in the browser: a Fontdue component under a "use client" boundary, or the <fontdue-*> web components. Those reveal hidden fonts directly via the logged-in admin's session, so they need nothing either way. The example repos wire this up end to end for each framework.
UI config
Most components accept a config object that controls UI behavior — type-tester options (selectable, priceBar, size ranges, OpenType-feature UI…), store-modal layout, form styling, analytics tracking, and more. See the full config reference.
There are two places to set it.
On the provider. <FontdueProvider config={…}> configures every Fontdue component rendered inside it. This is the normal way wherever your components are React descendants of the provider — Next.js, React Router 7, TanStack Start, Vike, Remix, and client-only setups.
<FontdueProvider config={{ typeTester: { selectable: true } }}>
<TypeTester familyName="Example" styleName="Regular" />
</FontdueProvider>Per component. Every component below also accepts an optional config prop with the same shape. It's the way to configure a component used as a standalone island (see Astro, below).
<TypeTester preloadedQuery={preloaded} config={{ typeTester: { selectable: true } }} />The config type is exported from the package root, so you can type a shared object: import type { Config } from "fontdue-js".
How the two compose:
- Standalone — a component with no
<FontdueProvider>ancestor uses its ownconfigprop in full. - Inside a provider — the component's
configdeep-merges onto the provider's: the component's keys win, everything else is inherited. - Next.js App Router is the exception. There, components render through React Server Components and read config from the
<FontdueProvider>only — a per-componentconfigprop is ignored. Set config on the provider.
Astro and other per-island frameworks: there is no global config
In Astro, Vike, and any framework that hydrates each client:* component as its own React island, <FontdueProvider> is a sibling island, not a React ancestor of your page components. Config set on the provider does not reach them, and there is no global config to set.
Pass config to each island instead. Define it once and reuse it so every island agrees:
---
import type { Config } from "fontdue-js";
import TypeTester, { loadTypeTesterQuery } from "fontdue-js/TypeTester";
// One source of truth for this page's testers.
const fontdueConfig = { typeTester: { selectable: true } } satisfies Config;
const regular = await loadTypeTesterQuery({ familyName: "Example", styleName: "Regular" });
const italic = await loadTypeTesterQuery({ familyName: "Example", styleName: "Italic" });
---
<TypeTester client:load preloadedQuery={regular} config={fontdueConfig} />
<TypeTester client:load preloadedQuery={italic} config={fontdueConfig} />config affects the server-rendered HTML, so keeping a single source of truth is also what keeps SSR and client hydration in agreement. To share config across many pages, export it from a module (e.g. src/lib/fontdue.ts) and import it wherever you mount a component.
IDs
Some components accept a
collectionId. This is theidreturned from the GraphQL API. You can alternatively passcollectionSlug, which is useful when you aren't consuming the GraphQL API directly. PrefercollectionIdwhen possible.
Components
Every component below has a default export and (where applicable) a load{Component}Query named export for the SSR preload path. Both share a single entry point per component. Every component also accepts an optional config prop — see UI config.
Lazy vs. preloaded props
Most components accept their data in one of two shapes:
- Lazy props — the identifying inputs the component needs to look up its own data:
collectionId/collectionSlugfor collection-bound components,familyName+styleNamefor the standalone<TypeTester>, nothing at all for forms like<NewsletterSignup>or<TestFontsForm>. The component fetches on mount, on the client, and shows nothing in the meantime. preloadedQuery— the result of calling the correspondingload{Component}Query(…)helper on the server. The component skips its own fetch, renders synchronously from the payload, and hydrates on the client without a re-fetch.
Both shapes are mutually exclusive on each component. You can mix them across the page — e.g. preload a <TypeTester> on a font-detail route while using a lazy <BuyButton> on the same page — and you can mix them across pages, since the choice is per-render.
// Lazy — fetches on the client.
<TypeTester familyName="Example" styleName="Regular" />
// Preloaded — server-rendered, hydrates without a fetch.
const preloadedQuery = await loadTypeTesterQuery({
familyName: "Example",
styleName: "Regular",
});
<TypeTester preloadedQuery={preloadedQuery} />Which to use?
- If you're in Next.js's App Router, use the lazy props — React Server Components takes care of the preload internally for you.
- If you're in any other framework with SSR (Astro, RR7, TanStack, Vike, Remix), prefer
preloadedQuery. Otherwise the page hydrates with a flash of empty content while each component fetches. - If your site is client-only (no SSR), lazy props are the only option.
A few components don't accept preloadedQuery:
StoreModalandCartButtonstill render server-side, but without a preloaded query — their fetch always happens on the client after hydration, and the SSR output is the empty Suspense fallback. The reason: their data is per-customer-session (cart contents, modal-open state) and a build-time / CDN-cached SSR call would just cache an empty cart and delay the real one.<CartButton>reflects the right count as soon as the post-hydration fetch resolves;<StoreModal>renders nothing visible until opened, so there's nothing to flash.CustomerLoginFormhas no preload helper today — it's a thin form with no upfront data needs.
FontdueProvider
Provides the Fontdue context (Relay environment, Redux store, config, components map) and renders auxiliary UI (theme, test-mode banner, consent banner, tracking). Render once at the layout level.
import FontdueProvider, { loadFontdueProviderQuery } from "fontdue-js/FontdueProvider";| Prop | Description |
| --- | --- |
| preloadedQuery | (Recommended) Result of loadFontdueProviderQuery(). Warms aux UI synchronously. |
| config | object UI config applied to everything in the provider's tree. See UI config. |
StoreModal
The cart and checkout UI, rendered as a modal. Mount once at the layout level. Opens when a BuyButton is clicked or when navigated to from another component.
import StoreModal from "fontdue-js/StoreModal";StoreModal doesn't accept preloadedQuery — its content is per-customer-session (cart contents, modal-open state), which isn't safe to resolve at SSR time. The component still renders server-side, but its data fetch always runs on the client after hydration. The modal is closed by default, so there's nothing visible to flash.
BuyButton
A button that opens StoreModal to the relevant collection.
import BuyButton, { loadBuyButtonQuery } from "fontdue-js/BuyButton";| Prop | Description |
| --- | --- |
| collectionId or collectionSlug | (Required, lazy) string Collection identifier. Omit if passing preloadedQuery. |
| preloadedQuery | (Required, SSR) Result of loadBuyButtonQuery({ collectionId }) or loadBuyButtonQuery({ collectionSlug }). |
| collectionName | (Optional) string Name to render in the default label: Buy {collectionName}. |
| label | (Optional) string Override the button label entirely. |
CartButton
Opens StoreModal, jumping straight to the cart screen if there are items in it.
import CartButton from "fontdue-js/CartButton";No preloadedQuery (same reason as StoreModal). Renders server-side with an empty Suspense fallback, then fetches the cart on the client after hydration and updates with the live count. Render anywhere; safe with or without an explicit <FontdueProvider> ancestor.
| Prop | Description |
| --- | --- |
| buttonStyle | (Optional) string Pass 'icon' to render the cart icon instead of a text label. The value is also surfaced as data-button-style on the rendered <button> so you can target other styles via CSS. |
| label | (Optional) string Text content. Defaults to "Cart". Ignored when buttonStyle="icon". |
| suffix | (Optional) string Template appended to the label. Substitutions: {count}, {subtotal}. Hidden when the cart is empty. |
| children | (Optional) ReactNode Custom button contents. Replaces the default label / icon entirely. |
CharacterViewer
An interactive character / glyph explorer.
import CharacterViewer, { loadCharacterViewerQuery } from "fontdue-js/CharacterViewer";| Prop | Description |
| --- | --- |
| collectionId or collectionSlug | (Required, lazy) string Collection identifier. |
| preloadedQuery | (Required, SSR) Result of loadCharacterViewerQuery({ collectionId }) or loadCharacterViewerQuery({ collectionSlug }). |
CustomerLoginForm
A form for customers to look up their order history. Submitting an email address sends a link to a Fontdue-hosted orders page.
import CustomerLoginForm from "fontdue-js/CustomerLoginForm";| Prop | Description |
| --- | --- |
| submitLabel | (Optional) string Submit button label. Defaults to "Submit". |
TypeTesters
Group of type testers configured through the Fontdue dashboard.
import TypeTesters, { loadTypeTestersQuery } from "fontdue-js/TypeTesters";| Prop | Description |
| --- | --- |
| collectionId or collectionSlug | (Required, lazy) string Collection identifier. |
| preloadedQuery | (Required, SSR) Result of loadTypeTestersQuery({ collectionId, tags?, excludeTags? }) or loadTypeTestersQuery({ collectionSlug, tags?, excludeTags? }). |
| defaultMode | (Optional) 'group' \| 'local' Whether the "Affect all styles" toggle starts on (group) or off (local). |
| autofit | (Optional) boolean Make sentences fit on one line, adjusting size as the container resizes. Disables when the user changes font size or content. |
| truncate | (Optional) boolean \| number \| { lines?: number, expandOnFocus?: boolean } Cap each paragraph at a number of lines, keeping the cap while a visitor edits the text. true is the original one-line preview, which opens on click. Overrides the truncate UI config. |
| tags | (Optional) string[] Render only testers tagged with any of these. |
| excludeTags | (Optional) string[] Exclude testers tagged with any of these. |
| features | (Optional) string[] OpenType feature codes to expose to users across all testers in the group (e.g. ['ss01', 'ss02']). |
| onFocus | (Optional) () => void Fired when any tester gains focus. |
| onBlur | (Optional) () => void Fired when any tester loses focus. |
| onToolbarOpenClose | (Optional) (open: boolean) => void Fired when the toolbar opens or closes. |
TypeTester (standalone)
Standalone tester driven by props rather than dashboard content. Doesn't support the "Affect all styles" toggle.
import TypeTester, { loadTypeTesterQuery } from "fontdue-js/TypeTester";| Prop | Description |
| --- | --- |
| familyName and styleName | (Required, lazy) string Identify the font style to render. The family/style must already be uploaded to your Fontdue admin. |
| preloadedQuery | (Required, SSR) Result of loadTypeTesterQuery({ familyName, styleName }). |
| fontSize | (Optional) number Initial font size in pixels. |
| lineHeight | (Optional) number Proportional line height (1 == fontSize). |
| letterSpacing | (Optional) number Letter spacing. |
| truncate | (Optional) boolean \| number \| { lines?: number, expandOnFocus?: boolean } Cap the paragraph at a number of lines, keeping the cap while a visitor edits the text. true is the original one-line preview, which opens on click. Overrides the truncate UI config. |
| content | (Optional) string Initial content. |
| direction | (Optional) 'ltr' \| 'rtl' Writing direction. |
| alignment | (Optional) 'left' \| 'center' \| 'right' Text alignment. |
| features | (Optional) string[] OpenType feature codes to expose to users (e.g. ['ss01', 'ss02']). |
| featuresSelected | (Optional) string[] Subset of features to mark as initially selected. |
| axes | (Optional) string[] Variable axes to expose (e.g. ['wdth', 'ital']). Pair with variableSettings. |
| featureSettings | (Optional) { feature: string, value: string }[] Pre-selected features. Shape matches TypeTester.featureSettings in the GraphQL API. |
| variableSettings | (Optional) { axis: string, value: number }[] Pre-selected axis values. Shape matches TypeTester.variableSettings in the GraphQL API. |
| autofit | (Optional) See TypeTesters.autofit above. |
| onFocus / onBlur | (Optional) () => void |
TestFontsForm
A form that lets visitors download test fonts after entering their details. Requires Test Fonts to be configured.
import TestFontsForm, { loadTestFontsFormQuery } from "fontdue-js/TestFontsForm";| Prop | Description |
| --- | --- |
| preloadedQuery | (Optional, SSR) Result of loadTestFontsFormQuery(). Lazy if omitted. |
| agreementLabel | (Optional) string Label for the required agreement checkbox. Defaults to the "EULA agreement text" field in your Fontdue Labels settings. |
| downloadLabel | (Optional) string Submit button label. Defaults to "Download test fonts". |
| newsletterCheckboxChecked | (Optional) boolean Pre-check the newsletter opt-in. |
NewsletterSignup
A signup form that adds the visitor as a Customer in Fontdue.
import NewsletterSignup, { loadNewsletterSignupQuery } from "fontdue-js/NewsletterSignup";| Prop | Description |
| --- | --- |
| preloadedQuery | (Optional, SSR) Result of loadNewsletterSignupQuery(). Lazy if omitted. |
| title | (Optional) string Heading rendered above the form. |
| intro | (Optional) string Paragraph rendered between the title and the form. |
| optInLabel | (Optional) string Label rendered next to the opt-in checkbox. Defaults to the "Newsletter opt-in label" field in your Fontdue Labels settings. |
| buttonLabel | (Optional) string Submit button label. Defaults to "Subscribe". |
| successLabel | (Optional) string Message shown after a successful submission. Defaults to the "Newsletter success label" field in your Fontdue Labels settings. |
| optInCheckboxChecked | (Optional) boolean Pre-check the opt-in box. |
Hooks
useFont
Loads and renders a webfont. Pass webfontSources (available as FontStyle.webfontSources in the GraphQL API) to load via the FontFace API directly — no CSS @font-face needed.
import useFont from "fontdue-js/useFont";
const FontStyle = ({ familyName, styleName, webfontSources }) => {
const { style, loaded } = useFont({
fontFamily: `${familyName} ${styleName}`,
webfontSources,
});
return <span style={style}>The quick brown fox</span>;
};If webfontSources is omitted, the hook falls back to detecting fonts loaded by your CSS @font-face rules.
Also available as fontdue-js/useFontStyle for backwards compatibility.
useConsent
Returns true when the visitor has granted consent for a given category. Re-renders when the consent state changes (e.g. the visitor accepts the consent banner).
import { useConsent } from "fontdue-js/useConsent";
const analyticsConsent = useConsent("analytics");useAutofit
Measures a string against a container and returns a font size that fits on one line. Used internally by TypeTester's autofit prop; exported for custom layouts.
import useAutofit from "fontdue-js/useAutofit";
const { ref, fontSize, ready } = useAutofit({
text: "The quick brown fox",
fontFamily: "Tonka Regular",
fontSize: 200,
});Examples
Working repos for each supported framework, all hitting the same example.fontdue.xyz store and exercising every preloadable component:
- Next.js —
fontdue/fontdue-example-next - Astro —
fontdue/example-astro - React Router 7 —
fontdue/example-react-router - TanStack Start —
fontdue/example-tanstack
