fontdue-js 3.0.0-alpha12 → 3.0.0-alpha13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.playwright-mcp/console-2026-06-15T09-14-00-118Z.log +84 -0
  2. package/.playwright-mcp/console-2026-06-15T09-25-42-726Z.log +2 -0
  3. package/.playwright-mcp/console-2026-06-15T09-25-47-707Z.log +1 -0
  4. package/.playwright-mcp/page-2026-06-15T09-14-01-054Z.yml +13 -0
  5. package/CHANGELOG.md +2 -2
  6. package/README.md +25 -50
  7. package/dist/__tests__/createFontdueFetch.test.js +33 -0
  8. package/dist/__tests__/networkFetch.test.js +81 -2
  9. package/dist/__tests__/nextAdapter.test.js +195 -50
  10. package/dist/__tests__/serverConfig.test.js +62 -0
  11. package/dist/components/ConfigContext.d.ts +3 -0
  12. package/dist/components/ConfigContext.js +5 -2
  13. package/dist/components/FontdueAdminToolbar/index.js +72 -16
  14. package/dist/components/FontdueProvider/index.server.d.ts +1 -0
  15. package/dist/components/FontdueProvider/index.server.js +10 -0
  16. package/dist/fontdue.css +59 -0
  17. package/dist/next/index.d.ts +1 -2
  18. package/dist/next/index.js +16 -6
  19. package/dist/next/registerSingleTenantResolver.d.ts +1 -0
  20. package/dist/next/registerSingleTenantResolver.js +36 -0
  21. package/dist/next/revalidate.js +1 -1
  22. package/dist/next/tenant.d.ts +6 -4
  23. package/dist/next/tenant.js +106 -69
  24. package/dist/preview/constants.d.ts +2 -0
  25. package/dist/preview/constants.js +20 -1
  26. package/dist/relay/environment.d.ts +2 -0
  27. package/dist/relay/environment.js +67 -38
  28. package/dist/relay/serverConfig.d.ts +6 -4
  29. package/dist/relay/serverConfig.js +81 -19
  30. package/dist/server/index.d.ts +1 -1
  31. package/dist/server/index.js +27 -15
  32. package/package.json +1 -1
  33. package/types/next-navigation.d.ts +4 -0
  34. package/vitest.config.ts +5 -0
@@ -0,0 +1,84 @@
1
+ [ 597ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:25630
2
+ [ 747ms] Error: Switched to client rendering because the server rendering errored:
3
+
4
+ Relay: Missing @required value at path 'collection' in 'BuyButtonIDQuery'.
5
+ at handleFieldErrors (webpack-internal:///(ssr)/./node_modules/relay-runtime/lib/util/handlePotentialSnapshotErrors.js:36:19)
6
+ at handlePotentialSnapshotErrors (webpack-internal:///(ssr)/./node_modules/relay-runtime/lib/util/handlePotentialSnapshotErrors.js:70:5)
7
+ at handlePotentialSnapshotErrorsForState (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useFragmentInternal_CURRENT.js:120:5)
8
+ at useFragmentInternal (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useFragmentInternal_CURRENT.js:420:3)
9
+ at useFragmentInternal (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useFragmentInternal.js:11:54)
10
+ at useLazyLoadQueryNode (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useLazyLoadQueryNode.js:64:14)
11
+ at useLazyLoadQuery (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useLazyLoadQuery.js:13:14)
12
+ at BuyButtonIDQueryRenderer (webpack-internal:///(ssr)/./node_modules/fontdue-js/dist/components/BuyButton/index.js:104:79)
13
+ at Object.react_stack_bottom_frame (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:292649)
14
+ at renderWithHooks (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:114080)
15
+ at renderElement (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:128752)
16
+ at retryNode (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:194584)
17
+ at performWork (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:215436)
18
+ at Immediate._onImmediate (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:98974)
19
+ at process.processImmediate (node:internal/timers:491:21)
20
+ at process.callbackTrampoline (node:internal/async_hooks:130:17)
21
+ [ 1442ms] Error: Relay: Missing @required value at path 'collection' in 'BuyButtonIDQuery'.
22
+ at handleFieldErrors (webpack-internal:///(app-pages-browser)/./node_modules/relay-runtime/lib/util/handlePotentialSnapshotErrors.js:36:19)
23
+ at handlePotentialSnapshotErrors (webpack-internal:///(app-pages-browser)/./node_modules/relay-runtime/lib/util/handlePotentialSnapshotErrors.js:70:5)
24
+ at handlePotentialSnapshotErrorsForState (webpack-internal:///(app-pages-browser)/./node_modules/react-relay/lib/relay-hooks/useFragmentInternal_CURRENT.js:120:5)
25
+ at useFragmentInternal (webpack-internal:///(app-pages-browser)/./node_modules/react-relay/lib/relay-hooks/useFragmentInternal_CURRENT.js:420:3)
26
+ at useFragmentInternal (webpack-internal:///(app-pages-browser)/./node_modules/react-relay/lib/relay-hooks/useFragmentInternal.js:11:54)
27
+ at useLazyLoadQueryNode (webpack-internal:///(app-pages-browser)/./node_modules/react-relay/lib/relay-hooks/useLazyLoadQueryNode.js:64:14)
28
+ at useLazyLoadQuery (webpack-internal:///(app-pages-browser)/./node_modules/react-relay/lib/relay-hooks/useLazyLoadQuery.js:13:14)
29
+ at BuyButtonIDQueryRenderer (webpack-internal:///(app-pages-browser)/./node_modules/fontdue-js/dist/components/BuyButton/index.js:116:79)
30
+ at Object.react_stack_bottom_frame (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:23584:20)
31
+ at renderWithHooks (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:6793:22)
32
+ at updateFunctionComponent (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:9247:19)
33
+ at beginWork (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:10858:18)
34
+ at runWithFiberInDEV (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:872:30)
35
+ at performUnitOfWork (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:15727:22)
36
+ at workLoopSync (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:15547:41)
37
+ at renderRootSync (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:15527:11)
38
+ at performWorkOnRoot (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:15034:44)
39
+ at performWorkOnRootViaSchedulerTask (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:16816:7)
40
+ at MessagePort.performWorkUntilDeadline (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/scheduler/cjs/scheduler.development.js:45:48)
41
+ [ 217249ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js:196
42
+ [ 217808ms] [WARNING] [Fast Refresh] performing full reload because your application had an unrecoverable error @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js:113
43
+ [ 219163ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:25630
44
+ [ 730814ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:25630
45
+ [ 735697ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:25630
46
+ [ 735780ms] Error: Switched to client rendering because the server rendering errored:
47
+
48
+ Relay: Missing @required value at path 'collection' in 'BuyButtonIDQuery'.
49
+ at handleFieldErrors (webpack-internal:///(ssr)/./node_modules/relay-runtime/lib/util/handlePotentialSnapshotErrors.js:36:19)
50
+ at handlePotentialSnapshotErrors (webpack-internal:///(ssr)/./node_modules/relay-runtime/lib/util/handlePotentialSnapshotErrors.js:70:5)
51
+ at handlePotentialSnapshotErrorsForState (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useFragmentInternal_CURRENT.js:120:5)
52
+ at useFragmentInternal (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useFragmentInternal_CURRENT.js:420:3)
53
+ at useFragmentInternal (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useFragmentInternal.js:11:54)
54
+ at useLazyLoadQueryNode (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useLazyLoadQueryNode.js:64:14)
55
+ at useLazyLoadQuery (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useLazyLoadQuery.js:13:14)
56
+ at BuyButtonIDQueryRenderer (webpack-internal:///(ssr)/./node_modules/fontdue-js/dist/components/BuyButton/index.js:104:79)
57
+ at Object.react_stack_bottom_frame (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:292649)
58
+ at renderWithHooks (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:114080)
59
+ at renderElement (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:128752)
60
+ at retryNode (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:194584)
61
+ at performWork (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:215436)
62
+ at Immediate._onImmediate (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:98974)
63
+ at process.processImmediate (node:internal/timers:491:21)
64
+ at process.callbackTrampoline (node:internal/async_hooks:130:17)
65
+ [ 747981ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:25630
66
+ [ 748069ms] Error: Switched to client rendering because the server rendering errored:
67
+
68
+ Relay: Missing @required value at path 'collection' in 'BuyButtonIDQuery'.
69
+ at handleFieldErrors (webpack-internal:///(ssr)/./node_modules/relay-runtime/lib/util/handlePotentialSnapshotErrors.js:36:19)
70
+ at handlePotentialSnapshotErrors (webpack-internal:///(ssr)/./node_modules/relay-runtime/lib/util/handlePotentialSnapshotErrors.js:70:5)
71
+ at handlePotentialSnapshotErrorsForState (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useFragmentInternal_CURRENT.js:120:5)
72
+ at useFragmentInternal (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useFragmentInternal_CURRENT.js:420:3)
73
+ at useFragmentInternal (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useFragmentInternal.js:11:54)
74
+ at useLazyLoadQueryNode (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useLazyLoadQueryNode.js:64:14)
75
+ at useLazyLoadQuery (webpack-internal:///(ssr)/./node_modules/react-relay/lib/relay-hooks/useLazyLoadQuery.js:13:14)
76
+ at BuyButtonIDQueryRenderer (webpack-internal:///(ssr)/./node_modules/fontdue-js/dist/components/BuyButton/index.js:104:79)
77
+ at Object.react_stack_bottom_frame (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:292649)
78
+ at renderWithHooks (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:114080)
79
+ at renderElement (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:128752)
80
+ at retryNode (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:194584)
81
+ at performWork (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:215436)
82
+ at Immediate._onImmediate (/Users/tom/code/fontdue/generaltype/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:2:98974)
83
+ at process.processImmediate (node:internal/timers:491:21)
84
+ at process.callbackTrampoline (node:internal/async_hooks:130:17)
@@ -0,0 +1,2 @@
1
+ [ 4008ms] [ERROR] Refused to get unsafe header "X-Sg-Cs" @ https://www.google.com/search?q=generaltype.fontdue.xuyz&oq=generaltype.fontdue.xuyz&gs_lcrp=EgZjaHJvbWUyBggAEEUYOdIBCDMwODhqMGo3qAIAsAIA&sourceid=chrome&ie=UTF-8&sei=HMUvapfSAv7Ii-gP_8yN6A0:68
2
+ [ 4008ms] [ERROR] Refused to get unsafe header "X-Sorry-Redirect" @ https://www.google.com/search?q=generaltype.fontdue.xuyz&oq=generaltype.fontdue.xuyz&gs_lcrp=EgZjaHJvbWUyBggAEEUYOdIBCDMwODhqMGo3qAIAsAIA&sourceid=chrome&ie=UTF-8&sei=HMUvapfSAv7Ii-gP_8yN6A0:68
@@ -0,0 +1 @@
1
+ [ 5170ms] [VERBOSE] [DOM] Input elements should have autocomplete attributes (suggested: "current-password"): (More info: https://goo.gl/9p2vKq) %o @ https://www.fontdue.xyz/login?redirect_to=%2Fadmin:0
@@ -0,0 +1,13 @@
1
+ - generic [active] [ref=e1]:
2
+ - generic [ref=e6] [cursor=pointer]:
3
+ - button "Open Next.js Dev Tools" [ref=e7]:
4
+ - img [ref=e8]
5
+ - generic [ref=e11]:
6
+ - button "Open issues overlay" [ref=e12]:
7
+ - generic [ref=e13]:
8
+ - generic [ref=e14]: "0"
9
+ - generic [ref=e15]: "1"
10
+ - generic [ref=e16]: Issue
11
+ - button "Collapse issues badge" [ref=e17]:
12
+ - img [ref=e18]
13
+ - alert [ref=e20]
package/CHANGELOG.md CHANGED
@@ -7,8 +7,8 @@ In alpha on the `alpha` dist-tag — install with `npm install fontdue-js@alpha`
7
7
  - `fontdue-js/preview` — `handlePreviewRequest` (a Web-standard enter/exit route handler), `readPreviewToken`, `previewAuthHeaders`, and the cookie/endpoint constants.
8
8
  - `fontdue-js/preview/server` — `runWithPreview`, which holds the preview token in `AsyncLocalStorage` for the duration of a request so every server fetch and preload forwards it automatically, and forces preview responses out of shared/CDN caches so an admin's render is never served to the public.
9
9
 
10
- On Next.js (which has no app-installable ambient context) the adapter's `prepareFontdueRender` folds the draft-mode token into the per-render config insteadthe same effect, so the embedded components reveal hidden fonts server-side. See the README "Admin preview".
11
- - **One server fetch for every framework.** `fontdue-js/server` exports `createFontdueFetch({ url?, headers?, cacheTags? })` — a ready-made server-side GraphQL fetcher with URL resolution, error handling, `FontdueNotFoundError`, and automatic preview-token forwarding. Each input resolves per call from the explicit option, the per-render config (`runWithPreview` or the Next adapter), then the environment, and passing `cacheTags` opts the fetch into Next's data cache + `/api/revalidate` (inert in other runtimes) — so Next and the other frameworks now share the same fetcher rather than Next hand-rolling its own. See the README "Server-side GraphQL fetches".
10
+ Next.js uses draft mode rather than ambient context, and needs no per-render setup call: mounting `<FontdueProvider>` wires every server fetch and the embedded components' server preloads to forward the token, apply the cache tags for `/api/revalidate`, and serve a live render while previewingincluding embeds rendered inside a Server Component, which now preload server-side with the admin token instead of falling back to a client refetch. `configureFontduePreview()` (from `fontdue-js/next`) stays exported for when you want the resolved endpoint inside a render (e.g. a `metadataBase` fallback), but it's optional. See the README "Admin preview".
11
+ - **One server fetch for every framework.** `fontdue-js/server` exports `createFontdueFetch({ url?, headers?, cacheTags? })` — a ready-made server-side GraphQL fetcher with URL resolution, error handling, `FontdueNotFoundError`, and automatic preview-token forwarding. Each input resolves per call from the explicit option, the per-render config (`runWithPreview` elsewhere, or the config the Next adapter resolves per render once `<FontdueProvider>` is mounted), then the environment, and passing `cacheTags` opts the fetch into Next's data cache + `/api/revalidate` (inert in other runtimes) — so Next and the other frameworks now share the same fetcher rather than Next hand-rolling its own. See the README "Server-side GraphQL fetches".
12
12
  - **Server-rendered embeds.** Components now render their full HTML on the server where the framework supports it (v2 hydrated some empty and fetched on the client), via the `load{Component}Query()` preload helpers.
13
13
  - **Migrating from v2 (Next.js)** is a small, mechanical upgrade — see the README "Migrating a Next.js site from v2". `useFontStyle` is renamed to `useFont` (the old name still works as an alias).
14
14
 
package/README.md CHANGED
@@ -413,30 +413,32 @@ export { POST } from "fontdue-js/next/revalidate";
413
413
 
414
414
  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.
415
415
 
416
- 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; `currentFontdueEndpoint()` below shows how.
416
+ 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.
417
417
 
418
418
  ### Your own GraphQL fetches
419
419
 
420
- Use the same [`createFontdueFetch`](#server-side-graphql-fetches) as every other framework, and give it the current render's endpoint so it ties into Next's data cache and `/api/revalidate`. `currentFontdueEndpoint()` (from `fontdue-js/next`) describes that endpoint — the base origin, required headers, and the per-site cache tags:
420
+ Use the same [`createFontdueFetch`](#server-side-graphql-fetches) as every other framework. Mounting `<FontdueProvider>` in your layout is enough to wire it up there's no per-render setup call:
421
421
 
422
422
  ```ts
423
+ // src/lib/graphql.ts
423
424
  import { createFontdueFetch } from "fontdue-js/server";
424
- import { currentFontdueEndpoint } from "fontdue-js/next";
425
-
426
- export async function fetchGraphql<Q>(name: string, query: string, variables?: unknown) {
427
- const endpoint = currentFontdueEndpoint();
428
- const fetchFontdue = createFontdueFetch({
429
- url: endpoint.origin,
430
- headers: endpoint.headers,
431
- // Opts the fetch into Next's data cache (force-cache) and tags it so the
432
- // deploy hook purges it. Drop cacheTags (pass `[]`) on a preview render.
433
- cacheTags: endpoint.tags,
434
- });
435
- return fetchFontdue<Q>(name, query, variables);
425
+
426
+ export const fetchGraphql = createFontdueFetch();
427
+ ```
428
+
429
+ ```ts
430
+ // any page / layout / generateMetadata
431
+ import { fetchGraphql } from "@/lib/graphql";
432
+
433
+ export default async function Page() {
434
+ const data = await fetchGraphql<IndexQuery>("Index.graphql");
435
+ // …
436
436
  }
437
437
  ```
438
438
 
439
- `currentFontdueEndpoint()` resolves the per-render tenant in multi-tenant mode (after `prepareFontdueRender`) or the `NEXT_PUBLIC_FONTDUE_URL` site otherwise; the shape is exported as `type FontdueEndpoint`. Route handlers render outside React, so pass the endpoint they got from `prepareFontdueRender` explicitly. For preview, see [Admin preview → Next.js](#nextjs).
439
+ `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](#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.
440
+
441
+ Route handlers (robots/sitemap) aren't React renders, so they read the endpoint from `fontdueEndpoint()` (from `fontdue-js/next`; shape: `type FontdueEndpoint`) and pass it to `createFontdueFetch({ url, headers, cacheTags })` directly. `configureFontduePreview()` is still exported if you want the resolved endpoint inside a render (e.g. a `metadataBase` fallback), but it's optional now that mounting the provider wires the config up.
440
442
 
441
443
  ## Migrating a Next.js site from v2
442
444
 
@@ -468,7 +470,7 @@ For a site built on the [example repo](https://github.com/fontdue/example-next)
468
470
 
469
471
  Keep the Deploy hook URL in your Fontdue admin pointed at it.
470
472
 
471
- 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` and pass it `cacheTags: endpoint.tags` so caching and `/api/revalidate` are handled for you — see [Your own GraphQL fetches](#your-own-graphql-fetches).
473
+ 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](#your-own-graphql-fetches).
472
474
 
473
475
  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.
474
476
 
@@ -493,10 +495,10 @@ export const fetchGraphql = createFontdueFetch();
493
495
  const data = await fetchGraphql<IndexQuery>("Index", indexQuery, { slug });
494
496
  ```
495
497
 
496
- `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 from the explicit option, then the per-render config (set by `runWithPreview` or the Next adapter), then the environment:
498
+ `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:
497
499
 
498
- - **url** — option per-render tenant URL → `FONTDUE_URL` / `PUBLIC_FONTDUE_URL` / `VITE_FONTDUE_URL`.
499
- - **headers** — merged ambient + explicit (explicit wins); this is how the [admin preview](#admin-preview) token is forwarded automatically.
500
+ - **url** — the explicit option, else `FONTDUE_URL` / `PUBLIC_FONTDUE_URL` / `VITE_FONTDUE_URL` from the environment.
501
+ - **headers** — the explicit option merged over the ambient [admin preview](#admin-preview) context (`runWithPreview`), so the preview token is forwarded automatically.
500
502
  - **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. The Next hints are inert in other runtimes, where HTML is cached at the response/CDN layer instead.
501
503
 
502
504
  **Upgrading a hand-rolled fetch.** If you already have something like this:
@@ -525,7 +527,7 @@ export const fetchGraphql = createFontdueFetch();
525
527
 
526
528
  You get URL resolution, error handling, `FontdueNotFoundError`, and automatic preview-token forwarding for free.
527
529
 
528
- > **Next.js:** the same `createFontdueFetch` — pass it the endpoint's `cacheTags` so it ties into Next's data cache and the `/api/revalidate` deploy hook. See [Your own GraphQL fetches](#your-own-graphql-fetches) under the Next adapter.
530
+ > **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](#your-own-graphql-fetches) under the Next adapter.
529
531
 
530
532
  ## Admin preview
531
533
 
@@ -580,7 +582,7 @@ const preload = await loadTypeTesterQuery(vars, { headers });
580
582
 
581
583
  ### Next.js
582
584
 
583
- Next uses [draft mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode) rather than ambient context. The route layers `draftMode()` on top of `handlePreviewRequest`:
585
+ Next uses [draft mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode) rather than ambient context. The preview route layers `draftMode()` on top of `handlePreviewRequest`:
584
586
 
585
587
  ```ts
586
588
  // app/api/preview/route.ts
@@ -600,36 +602,9 @@ export async function DELETE(request: Request) {
600
602
  }
601
603
  ```
602
604
 
603
- Two things then forward the token on a Next render:
604
-
605
- - **`prepareFontdueRender`** — the call you already make at the top of every page folds the draft-mode token into the per-render config, so the embedded components (type testers, store) reveal hidden fonts server-side automatically. This is the Next counterpart to `runWithPreview`.
606
- - **Your own fetch** reads draft mode and hands the token to `createFontdueFetch`, dropping the cache tags so the render stays live:
607
-
608
- ```ts
609
- import { draftMode, cookies } from "next/headers";
610
- import { createFontdueFetch } from "fontdue-js/server";
611
- import { currentFontdueEndpoint } from "fontdue-js/next";
612
- import { PREVIEW_TOKEN_COOKIE, previewAuthHeaders } from "fontdue-js/preview";
613
-
614
- export async function fetchGraphql<Q>(name: string, query: string, variables?: unknown) {
615
- const endpoint = currentFontdueEndpoint();
616
- // draftMode()/cookies() are request-scoped — guard build-time static
617
- // generation, where they throw, as "not preview".
618
- const isPreview = (await draftMode()).isEnabled;
619
- const token = isPreview ? (await cookies()).get(PREVIEW_TOKEN_COOKIE)?.value : undefined;
620
-
621
- const fetchFontdue = createFontdueFetch({
622
- url: endpoint.origin,
623
- headers: { ...endpoint.headers, ...previewAuthHeaders(token) },
624
- // Public renders are tagged + cached; preview renders pass no tags so they
625
- // stay live and reveal hidden fonts.
626
- cacheTags: isPreview ? [] : endpoint.tags,
627
- });
628
- return fetchFontdue<Q>(name, query, variables);
629
- }
630
- ```
605
+ 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](#your-own-graphql-fetches)).
631
606
 
632
- The example repos wire this up end to end for each framework.
607
+ 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.
633
608
 
634
609
  ## UI config
635
610
 
@@ -219,4 +219,37 @@ describe('createFontdueFetch', () => {
219
219
  expect(init.next).toBeUndefined();
220
220
  });
221
221
  });
222
+ describe('fontdue-preview header', () => {
223
+ it('sends "false" by default so a public/session-only request never reveals hidden fonts', async () => {
224
+ const fetchMock = mockFetch(() => ({
225
+ status: 200,
226
+ json: async () => ({
227
+ data: {}
228
+ })
229
+ }));
230
+ const fetchGraphql = createFontdueFetch({
231
+ url: 'https://acme.fontdue.com'
232
+ });
233
+ await fetchGraphql('Q', 'query Q { __typename }');
234
+ const init = fetchMock.mock.calls[0][1];
235
+ expect(init.headers['fontdue-preview']).toBe('false');
236
+ });
237
+ it('sends "true" when bound with a preview Bearer token (reveal hidden fonts)', async () => {
238
+ const fetchMock = mockFetch(() => ({
239
+ status: 200,
240
+ json: async () => ({
241
+ data: {}
242
+ })
243
+ }));
244
+ const fetchGraphql = createFontdueFetch({
245
+ url: 'https://acme.fontdue.com',
246
+ headers: {
247
+ authorization: 'Bearer admin-tok'
248
+ }
249
+ });
250
+ await fetchGraphql('Q', 'query Q { __typename }');
251
+ const init = fetchMock.mock.calls[0][1];
252
+ expect(init.headers['fontdue-preview']).toBe('true');
253
+ });
254
+ });
222
255
  });
@@ -42,10 +42,11 @@ describe('createNetworkFetch (server)', () => {
42
42
  vi.stubGlobal('fetch', fetchMock);
43
43
 
44
44
  // React.cache doesn't memoize outside a React render, so the store is
45
- // mocked rather than set through setFontdueServerConfig.
45
+ // mocked rather than set through setFontdueServerConfig. The network layer
46
+ // reads it through resolveFontdueServerConfig (awaited per fetch).
46
47
  vi.doMock('../relay/serverConfig', async importActual => ({
47
48
  ...(await importActual()),
48
- getFontdueServerConfig: () => ({
49
+ resolveFontdueServerConfig: async () => ({
49
50
  url: 'http://app:4000',
50
51
  headers: {
51
52
  'x-forwarded-host': 'acme.fontdue.com'
@@ -85,4 +86,82 @@ describe('createNetworkFetch (server)', () => {
85
86
  const [, options] = fetchMock.mock.calls[0];
86
87
  expect(options.headers.authorization).toBe('Bearer preview-tok');
87
88
  });
89
+ });
90
+ function headersOf(fetchMock) {
91
+ return fetchMock.mock.calls[0][1].headers;
92
+ }
93
+ describe('createNetworkFetch (fontdue-preview header)', () => {
94
+ it('sends fontdue-preview: false on a public server fetch (no token)', async () => {
95
+ vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
96
+ const fetchMock = vi.fn(async () => ({
97
+ json: async () => ({
98
+ data: {}
99
+ })
100
+ }));
101
+ vi.stubGlobal('fetch', fetchMock);
102
+ const {
103
+ createNetworkFetch
104
+ } = await import("../relay/environment.js");
105
+ await createNetworkFetch()(request, {});
106
+ expect(headersOf(fetchMock)['fontdue-preview']).toBe('false');
107
+ });
108
+ it('sends fontdue-preview: true when a preview Bearer token is forwarded (server)', async () => {
109
+ vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
110
+ const fetchMock = vi.fn(async () => ({
111
+ json: async () => ({
112
+ data: {}
113
+ })
114
+ }));
115
+ vi.stubGlobal('fetch', fetchMock);
116
+ const {
117
+ createNetworkFetch
118
+ } = await import("../relay/environment.js");
119
+ await createNetworkFetch({
120
+ headers: {
121
+ authorization: 'Bearer tok'
122
+ }
123
+ })(request, {});
124
+ expect(headersOf(fetchMock)['fontdue-preview']).toBe('true');
125
+ });
126
+ it('on the client, sends true only when the preview marker cookie is set', async () => {
127
+ // typeof window must be defined at module load for IS_SERVER to be false.
128
+ vi.stubGlobal('window', {
129
+ addEventListener: () => {}
130
+ });
131
+ vi.stubGlobal('document', {
132
+ cookie: 'fontdue_preview=1'
133
+ });
134
+ vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', 'https://acme.fontdue.com');
135
+ const fetchMock = vi.fn(async () => ({
136
+ json: async () => ({
137
+ data: {}
138
+ })
139
+ }));
140
+ vi.stubGlobal('fetch', fetchMock);
141
+ const {
142
+ createNetworkFetch
143
+ } = await import("../relay/environment.js");
144
+ await createNetworkFetch()(request, {});
145
+ expect(headersOf(fetchMock)['fontdue-preview']).toBe('true');
146
+ });
147
+ it('on the client, sends false when the marker cookie is absent (logged-in admin browsing normally)', async () => {
148
+ vi.stubGlobal('window', {
149
+ addEventListener: () => {}
150
+ });
151
+ vi.stubGlobal('document', {
152
+ cookie: 'other=1'
153
+ });
154
+ vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', 'https://acme.fontdue.com');
155
+ const fetchMock = vi.fn(async () => ({
156
+ json: async () => ({
157
+ data: {}
158
+ })
159
+ }));
160
+ vi.stubGlobal('fetch', fetchMock);
161
+ const {
162
+ createNetworkFetch
163
+ } = await import("../relay/environment.js");
164
+ await createNetworkFetch()(request, {});
165
+ expect(headersOf(fetchMock)['fontdue-preview']).toBe('false');
166
+ });
88
167
  });