npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

  • react 18 or 19
  • node >= 18
  • TypeScript (if used) with moduleResolution set to node16, nodenext, or bundler so the package's exports map resolves:
{
  "compilerOptions": {
    "moduleResolution": "nodenext"
  }
}

fontdue-js is published ESM-only.

Installation

npm install fontdue-js@latest

Configuration

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:

  1. 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).
  2. Mount <StoreModal /> once, alongside the provider — opens when a <BuyButton> or <CartButton> is clicked.
  3. (SSR only.) Preload in the layout with loadFontdueProviderQuery() and pass it as <FontdueProvider preloadedQuery={…}>. This ensures the page hydrates with preloaded data.
  4. (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 settingsimages.remotePatterns entries for Fontdue's image hosts (plus dangerouslyAllowSVG, since font specimens are often SVGs), merged with your own images config.
  • Blocking metadata — Next's streamed metadata locks in a 200 response before a notFound() thrown during generateMetadata can take effect (vercel/next.js#82041). withFontdue sets htmlLimitedBots to match every user agent so metadata rendering blocks the response. This only covers generateMetadata: a notFound(), 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 own loading.tsx streams the shell as 200 before 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.com

next/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 off NODE_ENV), so every render fetches fresh and admin content changes show on the next reload — no /api/revalidate call needed. This is deliberate: Next's on-demand revalidateTag doesn'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:

  1. Install the v3 line:

    npm install fontdue-js@alpha

    v3 is ESM-only and needs react 18/19 and node >= 18. If TypeScript can't resolve the imports, set moduleResolution to "bundler" (or node16/nodenext) in tsconfig.json — see Requirements.

  2. Wrap your Next config with withFontdue. It installs the Fontdue image settings (remotePatterns, dangerouslyAllowSVG) and the htmlLimitedBots workaround that lets a notFound() in generateMetadata return 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({});
  3. Replace your /api/revalidate handler with the re-export. v3 caches and tags its own server-side fetches, and the shared handler purges everything carrying the graphql tag:

    // app/api/revalidate/route.ts
    export { POST } from "fontdue-js/next/revalidate";

    Keep the Deploy hook URL in your Fontdue admin pointed at it.

  4. 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 to createFontdueFetch; mounting <FontdueProvider> then handles caching, /api/revalidate, and admin preview for you — see Your own GraphQL fetches.

  5. Remove the url prop from <FontdueProvider> if you passed one. It never configured server-side fetches (server components have no context); v3 resolves everything from NEXT_PUBLIC_FONTDUE_URL. The prop still works as a client-side runtime override, but with the env var set you don't need it.

  6. Rename useFontStyle to useFont if 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_URL from 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/revalidate can purge it; absent/empty leaves it uncached. Caching only kicks in for production builds — in next dev the 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/revalidate deploy 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 own config prop in full.
  • Inside a provider — the component's config deep-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-component config prop 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 the id returned from the GraphQL API. You can alternatively pass collectionSlug, which is useful when you aren't consuming the GraphQL API directly. Prefer collectionId when 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 / collectionSlug for collection-bound components, familyName + styleName for 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 corresponding load{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:

  • StoreModal and CartButton still 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.
  • CustomerLoginForm has 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: