fontdue-js 3.0.0-alpha11 → 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.
- package/.playwright-mcp/console-2026-06-15T09-14-00-118Z.log +84 -0
- package/.playwright-mcp/console-2026-06-15T09-25-42-726Z.log +2 -0
- package/.playwright-mcp/console-2026-06-15T09-25-47-707Z.log +1 -0
- package/.playwright-mcp/page-2026-06-15T09-14-01-054Z.yml +13 -0
- package/CHANGELOG.md +14 -0
- package/README.md +144 -17
- package/dist/__tests__/createFontdueFetch.test.js +154 -3
- package/dist/__tests__/networkFetch.test.js +81 -2
- package/dist/__tests__/nextAdapter.test.js +249 -40
- package/dist/__tests__/serverConfig.test.js +62 -0
- package/dist/components/ConfigContext.d.ts +3 -0
- package/dist/components/ConfigContext.js +5 -2
- package/dist/components/FontdueAdminToolbar/index.js +72 -16
- package/dist/components/FontdueProvider/index.server.d.ts +1 -0
- package/dist/components/FontdueProvider/index.server.js +10 -0
- package/dist/fontdue.css +59 -0
- package/dist/next/index.d.ts +1 -2
- package/dist/next/index.js +16 -6
- package/dist/next/registerSingleTenantResolver.d.ts +1 -0
- package/dist/next/registerSingleTenantResolver.js +36 -0
- package/dist/next/revalidate.js +1 -1
- package/dist/next/tenant.d.ts +6 -4
- package/dist/next/tenant.js +122 -49
- package/dist/preview/constants.d.ts +2 -0
- package/dist/preview/constants.js +20 -1
- package/dist/relay/environment.d.ts +2 -0
- package/dist/relay/environment.js +67 -38
- package/dist/relay/serverConfig.d.ts +6 -4
- package/dist/relay/serverConfig.js +81 -19
- package/dist/server/index.d.ts +15 -3
- package/dist/server/index.js +77 -31
- package/package.json +1 -1
- package/types/next-headers.d.ts +9 -0
- package/types/next-navigation.d.ts +4 -0
- 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
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 3.0.0
|
|
2
|
+
|
|
3
|
+
In alpha on the `alpha` dist-tag — install with `npm install fontdue-js@alpha`.
|
|
4
|
+
|
|
5
|
+
- **Framework-agnostic.** fontdue-js now works in any React SSR or client-only environment — Astro, React Router 7, TanStack Start, Vike, Remix, and the existing Next.js App Router — not just Next.js. Each supported framework has a worked example repo (see the README "Examples"). The package is now ESM-only and ships an `exports` map, so TypeScript consumers need `moduleResolution` set to `bundler`, `node16`, or `nodenext`.
|
|
6
|
+
- **Admin preview for every framework.** A preview toolbar — rendered by `<FontdueProvider>` and shown only to logged-in admins — reveals hidden (unpublished) fonts site-wide. New entry points implement a portable contract:
|
|
7
|
+
- `fontdue-js/preview` — `handlePreviewRequest` (a Web-standard enter/exit route handler), `readPreviewToken`, `previewAuthHeaders`, and the cookie/endpoint constants.
|
|
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
|
+
|
|
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 previewing — including 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
|
+
- **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
|
+
- **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
|
+
|
|
1
15
|
## 2.28.0
|
|
2
16
|
|
|
3
17
|
- Checkout now captures the buyer's **analytics consent and ad attribution** on the order. When the cart or store-modal checkout opens, fontdue-js sends the consent-banner state, anonymous ID, Meta browser IDs (the `_fbp`/`_fbc` cookies), and the page URL to the server, where they're stored on the order. This lets Fontdue emit a server-side *purchase* conversion event (Facebook Conversions API, Google Ads) when the order completes — completion happens in a Stripe webhook, outside the browser — while respecting the buyer's cookie-consent choice: if consent was declined, no identifiers are sent and nothing is forwarded to ad platforms. The call is fire-and-forget and never affects checkout.
|
package/README.md
CHANGED
|
@@ -413,31 +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;
|
|
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
|
-
|
|
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
|
-
|
|
423
|
+
// src/lib/graphql.ts
|
|
424
|
+
import { createFontdueFetch } from "fontdue-js/server";
|
|
424
425
|
|
|
425
|
-
export
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
});
|
|
436
|
-
return (await response.json()).data;
|
|
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
|
+
// …
|
|
437
436
|
}
|
|
438
437
|
```
|
|
439
438
|
|
|
440
|
-
|
|
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.
|
|
441
442
|
|
|
442
443
|
## Migrating a Next.js site from v2
|
|
443
444
|
|
|
@@ -469,7 +470,7 @@ For a site built on the [example repo](https://github.com/fontdue/example-next)
|
|
|
469
470
|
|
|
470
471
|
Keep the Deploy hook URL in your Fontdue admin pointed at it.
|
|
471
472
|
|
|
472
|
-
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.
|
|
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).
|
|
473
474
|
|
|
474
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.
|
|
475
476
|
|
|
@@ -479,6 +480,132 @@ What you get for it: server components now render the embeds' full HTML on the s
|
|
|
479
480
|
|
|
480
481
|
If you're starting fresh instead of migrating, fork the example repo — it ships in this shape already.
|
|
481
482
|
|
|
483
|
+
## Server-side GraphQL fetches
|
|
484
|
+
|
|
485
|
+
Beyond the preload helpers, you'll often run your own [GraphQL](https://docs.fontdue.com/graphql-api) queries server-side — page chrome, metadata, custom sections. `fontdue-js/server` exports a ready-made fetcher so you don't hand-roll the transport:
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
import { createFontdueFetch } from "fontdue-js/server";
|
|
489
|
+
|
|
490
|
+
// One fetcher for the whole app. Resolves the Fontdue URL from the environment
|
|
491
|
+
// (FONTDUE_URL / PUBLIC_FONTDUE_URL / VITE_FONTDUE_URL).
|
|
492
|
+
export const fetchGraphql = createFontdueFetch();
|
|
493
|
+
|
|
494
|
+
// In a loader / frontmatter / server component:
|
|
495
|
+
const data = await fetchGraphql<IndexQuery>("Index", indexQuery, { slug });
|
|
496
|
+
```
|
|
497
|
+
|
|
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:
|
|
499
|
+
|
|
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.
|
|
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.
|
|
503
|
+
|
|
504
|
+
**Upgrading a hand-rolled fetch.** If you already have something like this:
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
// Before — hand-rolled.
|
|
508
|
+
async function fetchGraphql(name, query, variables) {
|
|
509
|
+
const res = await fetch(`${import.meta.env.PUBLIC_FONTDUE_URL}/graphql`, {
|
|
510
|
+
method: "POST",
|
|
511
|
+
headers: { "content-type": "application/json" },
|
|
512
|
+
body: JSON.stringify({ query, variables }),
|
|
513
|
+
});
|
|
514
|
+
const json = await res.json();
|
|
515
|
+
if (json.errors) throw new Error(json.errors[0].message);
|
|
516
|
+
return json.data;
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
replace it with:
|
|
521
|
+
|
|
522
|
+
```ts
|
|
523
|
+
// After.
|
|
524
|
+
import { createFontdueFetch } from "fontdue-js/server";
|
|
525
|
+
export const fetchGraphql = createFontdueFetch();
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
You get URL resolution, error handling, `FontdueNotFoundError`, and automatic preview-token forwarding for free.
|
|
529
|
+
|
|
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.
|
|
531
|
+
|
|
532
|
+
## Admin preview
|
|
533
|
+
|
|
534
|
+
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.
|
|
535
|
+
|
|
536
|
+
Two entry points cover this: `fontdue-js/preview` (the portable cookie contract) and `fontdue-js/preview/server` (the ambient wiring).
|
|
537
|
+
|
|
538
|
+
**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:
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
// Astro — src/pages/api/preview.ts
|
|
542
|
+
import { handlePreviewRequest } from "fontdue-js/preview";
|
|
543
|
+
export const ALL = ({ request }) => handlePreviewRequest(request);
|
|
544
|
+
export const prerender = false;
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
// React Router 7 — app/routes/api.preview.ts
|
|
549
|
+
import { handlePreviewRequest } from "fontdue-js/preview";
|
|
550
|
+
export const action = ({ request }) => handlePreviewRequest(request);
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
The path is configurable via `config.preview.endpoint` on `<FontdueProvider>` (default `/api/preview`) — mount the route to match.
|
|
554
|
+
|
|
555
|
+
**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.
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
// Astro — src/middleware.ts
|
|
559
|
+
import { runWithPreview } from "fontdue-js/preview/server";
|
|
560
|
+
export const onRequest = (ctx, next) => runWithPreview(ctx.request, next);
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
```ts
|
|
564
|
+
// React Router 7 — root route (with future.v8_middleware enabled)
|
|
565
|
+
import { runWithPreview } from "fontdue-js/preview/server";
|
|
566
|
+
export const middleware = [({ request }, next) => runWithPreview(request, next)];
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
`runWithPreview` uses `AsyncLocalStorage`, so it works wherever middleware shares a runtime with the render — Node (the default SSR target on Netlify/Vercel), Deno, Bun.
|
|
570
|
+
|
|
571
|
+
**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:
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
import { readPreviewToken, previewAuthHeaders } from "fontdue-js/preview";
|
|
575
|
+
|
|
576
|
+
const headers = previewAuthHeaders(readPreviewToken(request.headers.get("cookie")));
|
|
577
|
+
const fetchGraphql = createFontdueFetch({ headers });
|
|
578
|
+
const preload = await loadTypeTesterQuery(vars, { headers });
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
`previewAuthHeaders` returns `{}` when there's no token, so it's always safe to pass.
|
|
582
|
+
|
|
583
|
+
### Next.js
|
|
584
|
+
|
|
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`:
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
// app/api/preview/route.ts
|
|
589
|
+
import { draftMode } from "next/headers";
|
|
590
|
+
import { handlePreviewRequest } from "fontdue-js/preview";
|
|
591
|
+
|
|
592
|
+
export async function POST(request: Request) {
|
|
593
|
+
const response = await handlePreviewRequest(request);
|
|
594
|
+
if (response.ok) (await draftMode()).enable();
|
|
595
|
+
return response;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export async function DELETE(request: Request) {
|
|
599
|
+
const response = await handlePreviewRequest(request);
|
|
600
|
+
(await draftMode()).disable();
|
|
601
|
+
return response;
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
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)).
|
|
606
|
+
|
|
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.
|
|
608
|
+
|
|
482
609
|
## UI config
|
|
483
610
|
|
|
484
611
|
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](https://docs.fontdue.com/fontduejs#b3dec49aa08240bba2b4c71a67c08333).
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import { createFontdueFetch, FontdueNotFoundError } from '../server/index.js';
|
|
3
|
+
import { registerAmbientConfigResolver } from '../relay/serverConfig.js';
|
|
3
4
|
beforeEach(() => {
|
|
4
5
|
vi.unstubAllEnvs();
|
|
5
6
|
vi.unstubAllGlobals();
|
|
6
7
|
});
|
|
8
|
+
|
|
9
|
+
// Several tests inject a per-render config through the ambient resolver seam
|
|
10
|
+
// (the same seam runWithPreview and the Next slot use). Always clear it.
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
registerAmbientConfigResolver(() => undefined);
|
|
13
|
+
});
|
|
14
|
+
function withConfig(config) {
|
|
15
|
+
registerAmbientConfigResolver(() => config);
|
|
16
|
+
}
|
|
7
17
|
function mockFetch(impl) {
|
|
8
18
|
const fetchMock = vi.fn(async (url, init) => impl(url, init));
|
|
9
19
|
vi.stubGlobal('fetch', fetchMock);
|
|
@@ -98,7 +108,148 @@ describe('createFontdueFetch', () => {
|
|
|
98
108
|
await fetchGraphql('Q', 'query Q { __typename }');
|
|
99
109
|
expect(fetchMock.mock.calls[0][0]).toBe('https://env.fontdue.com/graphql?query=Q');
|
|
100
110
|
});
|
|
101
|
-
it('throws a helpful error when no URL is configured', () => {
|
|
102
|
-
|
|
111
|
+
it('throws a helpful error when no URL is configured (resolved per call)', async () => {
|
|
112
|
+
mockFetch(() => ({
|
|
113
|
+
status: 200,
|
|
114
|
+
json: async () => ({
|
|
115
|
+
data: {}
|
|
116
|
+
})
|
|
117
|
+
}));
|
|
118
|
+
const fetchGraphql = createFontdueFetch();
|
|
119
|
+
await expect(fetchGraphql('Q', 'query Q { __typename }')).rejects.toThrow(/no Fontdue URL configured/);
|
|
120
|
+
});
|
|
121
|
+
describe('Next data cache tags', () => {
|
|
122
|
+
it('opts into force-cache + tags when cacheTags are given', async () => {
|
|
123
|
+
var _init$next;
|
|
124
|
+
const fetchMock = mockFetch(() => ({
|
|
125
|
+
status: 200,
|
|
126
|
+
json: async () => ({
|
|
127
|
+
data: {}
|
|
128
|
+
})
|
|
129
|
+
}));
|
|
130
|
+
const fetchGraphql = createFontdueFetch({
|
|
131
|
+
url: 'https://acme.fontdue.com',
|
|
132
|
+
cacheTags: ['graphql:acme.fontdue.com']
|
|
133
|
+
});
|
|
134
|
+
await fetchGraphql('Q', 'query Q { __typename }');
|
|
135
|
+
const init = fetchMock.mock.calls[0][1];
|
|
136
|
+
expect(init.cache).toBe('force-cache');
|
|
137
|
+
// The global `graphql` tag is prepended automatically.
|
|
138
|
+
expect((_init$next = init.next) === null || _init$next === void 0 ? void 0 : _init$next.tags).toEqual(['graphql', 'graphql:acme.fontdue.com']);
|
|
139
|
+
});
|
|
140
|
+
it('leaves the fetch uncached when no cacheTags are given', async () => {
|
|
141
|
+
const fetchMock = mockFetch(() => ({
|
|
142
|
+
status: 200,
|
|
143
|
+
json: async () => ({
|
|
144
|
+
data: {}
|
|
145
|
+
})
|
|
146
|
+
}));
|
|
147
|
+
const fetchGraphql = createFontdueFetch({
|
|
148
|
+
url: 'https://acme.fontdue.com'
|
|
149
|
+
});
|
|
150
|
+
await fetchGraphql('Q', 'query Q { __typename }');
|
|
151
|
+
const init = fetchMock.mock.calls[0][1];
|
|
152
|
+
expect(init.cache).toBeUndefined();
|
|
153
|
+
expect(init.next).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
it('treats an empty cacheTags list as uncached (preview renders)', async () => {
|
|
156
|
+
const fetchMock = mockFetch(() => ({
|
|
157
|
+
status: 200,
|
|
158
|
+
json: async () => ({
|
|
159
|
+
data: {}
|
|
160
|
+
})
|
|
161
|
+
}));
|
|
162
|
+
const fetchGraphql = createFontdueFetch({
|
|
163
|
+
url: 'https://acme.fontdue.com',
|
|
164
|
+
cacheTags: []
|
|
165
|
+
});
|
|
166
|
+
await fetchGraphql('Q', 'query Q { __typename }');
|
|
167
|
+
const init = fetchMock.mock.calls[0][1];
|
|
168
|
+
expect(init.cache).toBeUndefined();
|
|
169
|
+
expect(init.next).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
describe('per-render config (the Next slot / ambient resolver)', () => {
|
|
173
|
+
it('resolves url, headers and cacheTags from the config', async () => {
|
|
174
|
+
var _init$next2;
|
|
175
|
+
const fetchMock = mockFetch(() => ({
|
|
176
|
+
status: 200,
|
|
177
|
+
json: async () => ({
|
|
178
|
+
data: {}
|
|
179
|
+
})
|
|
180
|
+
}));
|
|
181
|
+
withConfig({
|
|
182
|
+
url: 'https://tenant.fontdue.com',
|
|
183
|
+
headers: {
|
|
184
|
+
'x-forwarded-host': 'tenant.fontdue.com'
|
|
185
|
+
},
|
|
186
|
+
cacheTags: ['graphql:tenant.fontdue.com']
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// No options: a module-level fetcher picks up the current render's config.
|
|
190
|
+
const fetchGraphql = createFontdueFetch();
|
|
191
|
+
await fetchGraphql('Q', 'query Q { __typename }');
|
|
192
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
193
|
+
expect(url).toBe('https://tenant.fontdue.com/graphql?query=Q');
|
|
194
|
+
expect(init.headers['x-forwarded-host']).toBe('tenant.fontdue.com');
|
|
195
|
+
expect(init.cache).toBe('force-cache');
|
|
196
|
+
expect((_init$next2 = init.next) === null || _init$next2 === void 0 ? void 0 : _init$next2.tags).toEqual(['graphql', 'graphql:tenant.fontdue.com']);
|
|
197
|
+
});
|
|
198
|
+
it('explicit options override the config (url and cacheTags)', async () => {
|
|
199
|
+
const fetchMock = mockFetch(() => ({
|
|
200
|
+
status: 200,
|
|
201
|
+
json: async () => ({
|
|
202
|
+
data: {}
|
|
203
|
+
})
|
|
204
|
+
}));
|
|
205
|
+
withConfig({
|
|
206
|
+
url: 'https://tenant.fontdue.com',
|
|
207
|
+
cacheTags: ['graphql:tenant.fontdue.com']
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Explicit url + empty cacheTags (e.g. a preview render) win.
|
|
211
|
+
const fetchGraphql = createFontdueFetch({
|
|
212
|
+
url: 'https://explicit.fontdue.com',
|
|
213
|
+
cacheTags: []
|
|
214
|
+
});
|
|
215
|
+
await fetchGraphql('Q', 'query Q { __typename }');
|
|
216
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
217
|
+
expect(url).toBe('https://explicit.fontdue.com/graphql?query=Q');
|
|
218
|
+
expect(init.cache).toBeUndefined();
|
|
219
|
+
expect(init.next).toBeUndefined();
|
|
220
|
+
});
|
|
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
|
+
});
|
|
103
254
|
});
|
|
104
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
|
-
|
|
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
|
});
|