Migrate from v2

Upgrade an existing Next.js site from fontdue-js v2 to v3, file by file, including admin preview.

v3 is the framework-agnostic release of fontdue-js, adding support for Astro, React Router, and TanStack Start alongside Next.js. v2 only ever ran on Next.js, so if your site is on v2 it’s a Next.js App Router site, and the upgrade is mechanical. Component imports, the props each component takes, and NEXT_PUBLIC_FONTDUE_URL don’t change – what changes is the package format (v3 is ESM-only) and a handful of integration files.

This page walks those files as a diff. The example repo already ships in the v3 shape, so if you’re starting fresh, fork it and follow the walkthrough instead.11A few optional renames you can skip: a prerelease configure() API was removed before v3 shipped, but it never reached stable v2; useFontStyle is now useFont, with the old name kept as an alias; and a url prop on <FontdueProvider> can come out (v3 reads NEXT_PUBLIC_FONTDUE_URL everywhere) but still works as a client-side override.

Bump the package

Install v3 and update the dependency. v3 is ESM-only and needs React 18 or 19 and a current Node.js; if TypeScript stops resolving the imports, set moduleResolution to bundler (or node16 / nodenext) – see Requirements.

package.json
  "dependencies": {-    "fontdue-js": "^2.28.0",+    "fontdue-js": "^3.0.0",  }

Convert the Next config

withFontdue installs the image settings, the Cloudflare image loader, the rewrites the components rely on, and the htmlLimitedBots workaround that lets notFound() return a real 404 – so the hand-rolled versions come out. Because you now import it, the config has to be a module: rename next.config.js to next.config.mjs.

next.config.mjs
-const url = require("url");-const nextConfig = {-  images: { dangerouslyAllowSVG: true, remotePatterns: [ /* your Fontdue host */ ] },-  htmlLimitedBots: /.*/,-};-module.exports = nextConfig;+import { withFontdue } from "fontdue-js/next/config";+ +export default withFontdue({+  // any of your own Next config still goes here+});

Swap the revalidate route

v3 caches and tags its own server-side fetches, and the handler it ships purges every tagged response. Replace your hand-rolled revalidateTag body with the re-export, and leave the Deploy hook URL in your admin pointed at the same path – see Revalidate data.

src/app/api/revalidate/route.ts
-import { NextRequest, NextResponse } from "next/server";-import { revalidateTag } from "next/cache";- -export async function POST(_request: NextRequest) {-  revalidateTag("graphql");-  return NextResponse.json({ revalidated: true, now: Date.now() });-}+export { POST } from "fontdue-js/next/revalidate";

Fetch with createFontdueFetch

v3 ships one server-side GraphQL fetcher, createFontdueFetch from fontdue-js/server, that replaces the hand-written fetch you used in v2. Call it once to get a fetch function and hand it your query:

src/lib/graphql.ts
+import { notFound } from "next/navigation";+import { createFontdueFetch, FontdueNotFoundError } from "fontdue-js/server"; -const ENDPOINT = `${process.env.NEXT_PUBLIC_FONTDUE_URL}/graphql`;// …getStaticQuery, unchanged… const fetchGraphql = async <Q, V = void>(queryName: string, variables?: V): Promise<Q> => {  const query = await getStaticQuery(queryName);-  const response = await fetch(`${ENDPOINT}?query=${queryName}`, {-    method: "POST",-    body: JSON.stringify({ query, variables }),-    headers: { "content-type": "application/json" },-    next: { tags: ["graphql"] },-  });-  if (response.status !== 200) throw new Error("Fontdue request failed");-  const json = await response.json();-  if (json.errors?.[0]?.message) throw new Error(json.errors[0].message);-  return json.data;+  const fetchFontdue = createFontdueFetch();+  try {+    return await fetchFontdue<Q, V>(queryName, query, variables);+  } catch (error) {+    if (error instanceof FontdueNotFoundError) notFound();+    throw error;+  }};

createFontdueFetch() returns a function you call as fetchFontdue(queryName, query, variables?), and it folds in everything the v2 helper did by hand. It resolves the store from NEXT_PUBLIC_FONTDUE_URL, posts the query, and unwraps data for you. It opts the response into Next’s data cache and tags it, so the revalidate hook above clears it. It throws FontdueNotFoundError when the host doesn’t resolve to a store, which you map to a Next.js notFound(). And once preview is wired up below, it forwards the admin token on every request automatically. Query the GraphQL API covers the helper in full.

Add admin preview

Admin preview is new in v3: it lets a signed-in admin reveal hidden and unpublished fonts across the server-rendered site without exposing them to the public. Upgrading already auto-mounts the toolbar through your provider – what it needs is a route to broker the preview token. Add one new file:

// src/app/api/preview/route.ts (new)
import { draftMode } from "next/headers";
import { handlePreviewRequest } from "fontdue-js/preview";
const previewCookieOptions = { secure: process.env.NODE_ENV === "production" };
export async function POST(request: Request) {
const response = await handlePreviewRequest(request, previewCookieOptions);
if (response.ok) (await draftMode()).enable();
return response;
}
export async function DELETE(request: Request) {
const response = await handlePreviewRequest(request, previewCookieOptions);
(await draftMode()).disable();
return response;
}

That’s the whole wiring on Next.js. The route toggles draft mode, and the createFontdueFetch you switched to above reads it and forwards the token on every server fetch and preload – no per-page changes. Admin preview covers the contract in full.

1 A few optional renames you can skip: a prerelease configure() API was removed before v3 shipped, but it never reached stable v2; useFontStyle is now useFont, with the old name kept as an alias; and a url prop on <FontdueProvider> can come out (v3 reads NEXT_PUBLIC_FONTDUE_URL everywhere) but still works as a client-side override.