Admin preview

Reveal hidden and unpublished fonts on a server-rendered Fontdue site while you're signed in as an admin, without exposing them to the public.

Admin preview lets a logged-in admin reveal hidden and unpublished collections across a server-rendered site, while the public keeps seeing only published fonts on cacheable, sessionless pages. On a script-tag site this needs no wiring: the browser does the fetching, so entering preview from the toolbar reloads the page with a preview header attached and the Fontdue script handles the rest. A server-rendered site needs a bit more wiring when fetching data on its own server, covered on this page. The FontdueProvider you already mounted adds an admin toolbar that appears only when you’re logged in to your admin.11The toolbar handles passing your admin session through to the server’s GraphQL queries via short-lived tokens stored in a cookie.

The preview route

You provide a route at the default path /api/preview and hand each request to handlePreviewRequest from fontdue-js/preview, which reads the token off the request and writes or clears the two cookies. The toolbar POSTs to it on enter and sends a DELETE on exit. What differs per framework is the file path and how each spells a route handler.22A public visitor carries no preview cookie, so their requests send no token and stay on the cacheable published-only path – preview is something an admin opts into, not a mode the site can fall into on its own.

Next.js also toggles draft mode here – setting the cookies is what lets createFontdueFetch find the token on later server fetches.33previewCookieOptions marks the cookies secure in production so they only travel over HTTPS, while staying readable on a plain-HTTP local dev server.

Astro opts the route out of prerendering with export const prerender = false, so it runs per request and can set cookies.

In React Router 7 the route is an action, registered in app/routes.ts as route("api/preview", "routes/api.preview.ts").

In TanStack Start it’s a server route with explicit POST and DELETE handlers, each handing the request straight to handlePreviewRequest.

// src/app/api/preview/route.ts
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;
}
// src/pages/api/preview.ts
import type { APIRoute } from "astro";
import { handlePreviewRequest } from "fontdue-js/preview";
export const ALL: APIRoute = ({ request }) => handlePreviewRequest(request);
export const prerender = false;
// app/routes/api.preview.ts
import { handlePreviewRequest } from "fontdue-js/preview";
import type { Route } from "./+types/api.preview";
export const action = ({ request }: Route.ActionArgs) =>
handlePreviewRequest(request);
// src/routes/api.preview.ts
import { createFileRoute } from "@tanstack/react-router";
import { handlePreviewRequest } from "fontdue-js/preview";
export const Route = createFileRoute("/api/preview")({
server: {
handlers: {
POST: ({ request }) => handlePreviewRequest(request),
DELETE: ({ request }) => handlePreviewRequest(request),
},
},
});

Forwarding the token

Once the cookies are set, the token still has to reach the GraphQL fetches and preloads that render your pages. Next.js leans on its own draft mode for this; every other framework wraps each request in runWithPreview from fontdue-js/preview/server.

In Next.js there’s nothing more to wire. The route enables draft mode, and createFontdueFetch reads it and forwards the token automatically on every server fetch and preload – no middleware, no per-call plumbing.

You wrap each request in runWithPreview in the framework’s middleware. It reads the token and puts it in an AsyncLocalStorage context for the whole render, so createFontdueFetch and every load{Component}Query preload pick it up and forward it – and it forces preview responses out of the shared cache so an admin’s view is never served to the public.44The same middleware also applies the CDN cache headers from Revalidate data; the snippets below trim that part to keep the preview wiring in focus.

runWithPreview goes in src/middleware.ts, which runs for every request before the page renders:

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
import { runWithPreview } from "fontdue-js/preview/server";
export const onRequest = defineMiddleware((context, next) =>
runWithPreview(context.request, next)
);

In React Router 7 it’s route middleware on the root route, which needs future.v8_middleware turned on:

// app/root.tsx
import { runWithPreview } from "fontdue-js/preview/server";
import type { Route } from "./+types/root";
export const middleware: Route.MiddlewareFunction[] = [
({ request }, next) => runWithPreview(request, next),
];
// react-router.config.ts
export default {
ssr: true,
future: { v8_middleware: true },
} satisfies Config;

In TanStack Start it’s request middleware registered globally in src/start.ts, so it wraps every request the server handles:

// src/lib/preview.ts
import { createMiddleware } from "@tanstack/react-start";
import { runWithPreview } from "fontdue-js/preview/server";
export const previewMiddleware = createMiddleware({ type: "request" }).server(
({ request, next }) =>
runWithPreview(request, async () => (await next()).response)
);
// src/start.ts
import { createStart } from "@tanstack/react-start";
import { previewMiddleware } from "./lib/preview";
export const startInstance = createStart(() => ({
requestMiddleware: [previewMiddleware],
}));

This ambient middleware forwards the token to every fetch and preload on the page with no per-call changes.55The ambient context relies on the middleware running in the same runtime as the render, which holds for the default Node SSR target. If you split middleware into a separate edge runtime – for example Astro’s edgeMiddleware: true – the AsyncLocalStorage context can’t reach the render. Fall back to reading the token with readPreviewToken and threading previewAuthHeaders(token) into your fetches and preloads explicitly.

When the token expires

The admin token is stateless and short-lived – it lasts one hour. If it’s expired, the toolbar shows a warning and a Re-enter preview button that generates a fresh token and reloads the page.

1 The toolbar handles passing your admin session through to the server’s GraphQL queries via short-lived tokens stored in a cookie. 
2 A public visitor carries no preview cookie, so their requests send no token and stay on the cacheable published-only path – preview is something an admin opts into, not a mode the site can fall into on its own. 
3 previewCookieOptions marks the cookies secure in production so they only travel over HTTPS, while staying readable on a plain-HTTP local dev server. 
4 The same middleware also applies the CDN cache headers from Revalidate data; the snippets below trim that part to keep the preview wiring in focus. 
5 The ambient context relies on the middleware running in the same runtime as the render, which holds for the default Node SSR target. If you split middleware into a separate edge runtime – for example Astro’s edgeMiddleware: true – the AsyncLocalStorage context can’t reach the render. Fall back to reading the token with readPreviewToken and threading previewAuthHeaders(token) into your fetches and preloads explicitly.