@decocms/nextjs 8.5.2 → 8.7.0

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 (51) hide show
  1. package/README.md +82 -15
  2. package/dist/DraftPreviewIndicator.d.ts.map +1 -1
  3. package/dist/DraftPreviewIndicator.js +4 -3
  4. package/dist/DraftPreviewIndicator.js.map +1 -1
  5. package/dist/assignments.d.ts +95 -0
  6. package/dist/assignments.d.ts.map +1 -0
  7. package/dist/assignments.js +229 -0
  8. package/dist/assignments.js.map +1 -0
  9. package/dist/draft.d.ts +36 -10
  10. package/dist/draft.d.ts.map +1 -1
  11. package/dist/draft.js +38 -12
  12. package/dist/draft.js.map +1 -1
  13. package/dist/drafts.d.ts +39 -60
  14. package/dist/drafts.d.ts.map +1 -1
  15. package/dist/drafts.js +58 -117
  16. package/dist/drafts.js.map +1 -1
  17. package/dist/index.d.ts +5 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +5 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/instrumentation/index.d.ts +1 -1
  22. package/dist/instrumentation/index.js +3 -3
  23. package/dist/instrumentation/index.js.map +1 -1
  24. package/dist/proxy/index.d.ts +175 -35
  25. package/dist/proxy/index.d.ts.map +1 -1
  26. package/dist/proxy/index.js +242 -41
  27. package/dist/proxy/index.js.map +1 -1
  28. package/dist/server/index.d.ts +59 -20
  29. package/dist/server/index.d.ts.map +1 -1
  30. package/dist/server/index.js +143 -25
  31. package/dist/server/index.js.map +1 -1
  32. package/dist/server/slot.d.ts +25 -4
  33. package/dist/server/slot.d.ts.map +1 -1
  34. package/dist/server/slot.js +13 -9
  35. package/dist/server/slot.js.map +1 -1
  36. package/dist/visitor.d.ts +16 -0
  37. package/dist/visitor.d.ts.map +1 -0
  38. package/dist/visitor.js +16 -0
  39. package/dist/visitor.js.map +1 -0
  40. package/package.json +3 -3
  41. package/src/DraftPreviewIndicator.tsx +4 -3
  42. package/src/assignments.ts +278 -0
  43. package/src/config/index.cjs +1 -1
  44. package/src/draft.ts +43 -12
  45. package/src/drafts.ts +71 -126
  46. package/src/index.ts +5 -2
  47. package/src/instrumentation/index.ts +3 -3
  48. package/src/proxy/index.ts +371 -59
  49. package/src/server/index.ts +173 -33
  50. package/src/server/slot.ts +33 -5
  51. package/src/visitor.ts +16 -0
@@ -2,45 +2,84 @@
2
2
  * `@decocms/nextjs/server`: draft previews in Server Components (pages,
3
3
  * layouts, route handlers). Server only: it reads `next/headers`.
4
4
  *
5
- * A page (and, through the `x-deco-draft` header the proxy forwards, the root
6
- * layout) binds the request's draft before it reads content, then reads
7
- * through `contentClient(cms)`:
5
+ * The proxy decides what a request reads (`@decocms/nextjs/proxy`): the
6
+ * version, the visitor, and each test and campaign on or off, forwarded on
7
+ * `x-deco-version`, `x-deco-visitor` and `x-deco-assignments`. A page (and,
8
+ * through those headers, the root layout) binds them before it reads
9
+ * content, then reads through `contentClient(cms)`, which builds
10
+ * `cms.client({ version, experiments, campaigns })` and decides nothing
11
+ * again:
8
12
  *
9
13
  * ```tsx
10
14
  * export default async function Page({ searchParams }) {
11
- * await bindDraft(cms, { searchParams: await searchParams, site: "my-store" });
15
+ * await bindDraft(cms, { searchParams: await searchParams });
12
16
  * const client = contentClient(cms);
13
17
  * …
14
18
  * return <>{sections}<DraftPreviewIndicator /></>;
15
19
  * }
16
20
  * ```
17
21
  *
18
- * The host check is the proxy's (`decideDraft`), repeated here with the same
19
- * options, so a route the proxy doesn't match, or a forged `x-deco-draft`,
20
- * still gets the release on a host that may not preview.
22
+ * The host check is the proxy's (`decideDraft`), repeated here against the
23
+ * same CMS, so a route the proxy doesn't match, or a forged `x-deco-version`,
24
+ * still gets the release on a host that may not preview. On the release, a
25
+ * test at 0% stays off whatever `x-deco-assignments` says.
21
26
  */
22
27
  import type { Client, CMS } from "@decocms/blocks";
23
- import { type PreviewPolicy } from "../drafts.ts";
24
28
  import { activeDraftPointer } from "./slot.ts";
25
- export { type PreviewPolicy, requestLike } from "../drafts.ts";
29
+ export { ASSIGNMENTS_ROUTE_PREFIX, assignmentsSegment, type PathAssignments, parseAssignmentsSegment, } from "../assignments.ts";
30
+ export { requestLike } from "../drafts.ts";
26
31
  export { activeDraftPointer };
27
32
  /** A page's `searchParams` prop, awaited. */
28
33
  export type SearchParams = Record<string, string | string[] | undefined>;
29
- export interface BindDraftOptions extends PreviewPolicy {
30
- /** The page's own `searchParams` (a layout has none: it reads the proxy's `x-deco-draft`). */
34
+ export interface BindDraftOptions {
35
+ /** The page's own `searchParams` (a layout has none: it reads the proxy's `x-deco-version`). */
31
36
  searchParams?: SearchParams;
32
37
  }
38
+ type BindCMS = Pick<CMS, "readVersion" | "forRelease" | "checkRelease">;
33
39
  /**
34
- * Binds the request's draft, if any: the page's own `?__draft=` first (`off`
35
- * leaves preview), then the pointer the proxy forwarded on `x-deco-draft`,
36
- * then the draft cookie, each on a host the options allow. Returns whether a
37
- * draft was bound. With previews off on every host it returns `false`
38
- * without reading the request, so a static page stays static.
40
+ * Binds what the request reads: the version, if any (the page's own
41
+ * `?__draft=` first, `off` leaving preview, then the version the proxy
42
+ * forwarded on `x-deco-version`, then the draft cookie, each on a host the
43
+ * CMS allows), and what the proxy decided (`x-deco-assignments`) and the
44
+ * visitor it read (`x-deco-visitor`). Returns whether a version was bound:
45
+ * a draft, or a published release's commit SHA (Studio's "Preview this
46
+ * version"). It reads the request's headers, so it belongs on the draft
47
+ * route; a cached (ISR) page reads what its path names (`assignedClient`).
39
48
  */
40
- export declare function bindDraft(cms: Pick<CMS, "draftPointer" | "settings" | "forRelease">, options?: BindDraftOptions): Promise<boolean>;
49
+ export declare function bindDraft(cms: BindCMS, options?: BindDraftOptions): Promise<boolean>;
41
50
  /**
42
- * The content client for this request: the draft `bindDraft` bound, or the
43
- * release.
51
+ * The content client for this request, from what `bindDraft` bound:
52
+ * `cms.client({ version, experiments, campaigns })` with the tests and
53
+ * campaigns the proxy decided, deciding nothing again. On a route the proxy
54
+ * didn't decide for, the visitor and the preview's moment and switched tests
55
+ * and campaigns decide instead. Create one per request.
44
56
  */
45
- export declare function contentClient(cms: Pick<CMS, "forDraft" | "forRelease">): Client;
57
+ export declare function contentClient(cms: Pick<CMS, "client">): Client;
58
+ /**
59
+ * The content client of a page on the assignments route
60
+ * (`app/%5Fassignments/[assignments]/[[...path]]`, which the proxy's
61
+ * `rewriteToAssignmentsRoute` rewrites the CMS's pages to): the current
62
+ * release, read as `cms.client({ experiments, campaigns })` with every test
63
+ * and campaign the path's segment names on or off as the proxy decided, and
64
+ * no visitor. The proxy decides, the page renders that, reading no request,
65
+ * so Next can cache it (ISR, `revalidate = 600`). A test at 0% stays off
66
+ * whatever the path names, and a test or a campaign the content doesn't have
67
+ * stays out, never an error: a 404 here would be cached under the path by
68
+ * Next. `null` only when the segment isn't one the proxy writes: the page
69
+ * answers with `notFound()`.
70
+ *
71
+ * ```tsx
72
+ * export const revalidate = 600;
73
+ * export async function generateStaticParams() {
74
+ * return [];
75
+ * }
76
+ * export default async function Page({ params }) {
77
+ * const { assignments, path } = await params;
78
+ * const client = await assignedClient(cms, assignments);
79
+ * if (!client) notFound();
80
+ * …
81
+ * }
82
+ * ```
83
+ */
84
+ export declare function assignedClient(cms: Pick<CMS, "client" | "forRelease">, segment: string): Promise<Client | null>;
46
85
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,EAAuC,KAAK,aAAa,EAAe,MAAM,cAAc,CAAC;AACpG,OAAO,EAAE,kBAAkB,EAAa,MAAM,WAAW,CAAC;AAE1D,OAAO,EAAE,KAAK,aAAa,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAE9B,6CAA6C;AAC7C,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;AAEzE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACrD,8FAA8F;IAC9F,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAOD;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,cAAc,GAAG,UAAU,GAAG,YAAY,CAAC,EAC1D,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAalB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,UAAU,GAAG,YAAY,CAAC,GAAG,MAAM,CAG/E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAiB,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAiBlE,OAAO,EAAE,kBAAkB,EAAa,MAAM,WAAW,CAAC;AAE1D,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,KAAK,eAAe,EACpB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAE9B,6CAA6C;AAC7C,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;AAEzE,MAAM,WAAW,gBAAgB;IAC/B,gGAAgG;IAChG,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,KAAK,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,aAAa,GAAG,YAAY,GAAG,cAAc,CAAC,CAAC;AAOxE;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ9F;AAkED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,MAAM,CAU9D;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,QAAQ,GAAG,YAAY,CAAC,EACvC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKxB"}
@@ -2,27 +2,36 @@
2
2
  * `@decocms/nextjs/server`: draft previews in Server Components (pages,
3
3
  * layouts, route handlers). Server only: it reads `next/headers`.
4
4
  *
5
- * A page (and, through the `x-deco-draft` header the proxy forwards, the root
6
- * layout) binds the request's draft before it reads content, then reads
7
- * through `contentClient(cms)`:
5
+ * The proxy decides what a request reads (`@decocms/nextjs/proxy`): the
6
+ * version, the visitor, and each test and campaign on or off, forwarded on
7
+ * `x-deco-version`, `x-deco-visitor` and `x-deco-assignments`. A page (and,
8
+ * through those headers, the root layout) binds them before it reads
9
+ * content, then reads through `contentClient(cms)`, which builds
10
+ * `cms.client({ version, experiments, campaigns })` and decides nothing
11
+ * again:
8
12
  *
9
13
  * ```tsx
10
14
  * export default async function Page({ searchParams }) {
11
- * await bindDraft(cms, { searchParams: await searchParams, site: "my-store" });
15
+ * await bindDraft(cms, { searchParams: await searchParams });
12
16
  * const client = contentClient(cms);
13
17
  * …
14
18
  * return <>{sections}<DraftPreviewIndicator /></>;
15
19
  * }
16
20
  * ```
17
21
  *
18
- * The host check is the proxy's (`decideDraft`), repeated here with the same
19
- * options, so a route the proxy doesn't match, or a forged `x-deco-draft`,
20
- * still gets the release on a host that may not preview.
22
+ * The host check is the proxy's (`decideDraft`), repeated here against the
23
+ * same CMS, so a route the proxy doesn't match, or a forged `x-deco-version`,
24
+ * still gets the release on a host that may not preview. On the release, a
25
+ * test at 0% stays off whatever `x-deco-assignments` says.
21
26
  */
27
+ import { parseVisitor } from "@decocms/blocks/experiments";
22
28
  import { headers } from "next/headers.js";
23
- import { DRAFT_HEADER, DRAFT_PARAM } from "../draft.js";
24
- import { allowedDraft, isDraftPreviewEnabled, requestLike } from "../drafts.js";
29
+ import { ASSIGNMENTS_HEADER, parseAssignmentsSegment, withoutDarkTests, } from "../assignments.js";
30
+ import { DRAFT_PARAM, DRAFT_UNAVAILABLE_HEADER, RELEASE_CHECK_HEADER, VERSION_HEADER, } from "../draft.js";
31
+ import { allowedDraft, draftReleaseCheck, requestLike } from "../drafts.js";
32
+ import { VISITOR_HEADER } from "../visitor.js";
25
33
  import { activeDraftPointer, draftSlot } from "./slot.js";
34
+ export { ASSIGNMENTS_ROUTE_PREFIX, assignmentsSegment, parseAssignmentsSegment, } from "../assignments.js";
26
35
  export { requestLike } from "../drafts.js";
27
36
  export { activeDraftPointer };
28
37
  function firstParam(searchParams, key) {
@@ -30,35 +39,144 @@ function firstParam(searchParams, key) {
30
39
  return (Array.isArray(raw) ? raw[0] : raw) ?? null;
31
40
  }
32
41
  /**
33
- * Binds the request's draft, if any: the page's own `?__draft=` first (`off`
34
- * leaves preview), then the pointer the proxy forwarded on `x-deco-draft`,
35
- * then the draft cookie, each on a host the options allow. Returns whether a
36
- * draft was bound. With previews off on every host it returns `false`
37
- * without reading the request, so a static page stays static.
42
+ * Binds what the request reads: the version, if any (the page's own
43
+ * `?__draft=` first, `off` leaving preview, then the version the proxy
44
+ * forwarded on `x-deco-version`, then the draft cookie, each on a host the
45
+ * CMS allows), and what the proxy decided (`x-deco-assignments`) and the
46
+ * visitor it read (`x-deco-visitor`). Returns whether a version was bound:
47
+ * a draft, or a published release's commit SHA (Studio's "Preview this
48
+ * version"). It reads the request's headers, so it belongs on the draft
49
+ * route; a cached (ISR) page reads what its path names (`assignedClient`).
38
50
  */
39
51
  export async function bindDraft(cms, options = {}) {
40
- const { searchParams, ...policy } = options;
41
- if (!(await isDraftPreviewEnabled(cms, policy)))
42
- return false;
52
+ if (await bindVersion(cms, options)) {
53
+ // A preview renders per request anyway, and may switch on a test at 0%.
54
+ await bindForwarded(cms, false);
55
+ return true;
56
+ }
57
+ if (await variesByVisitor(cms))
58
+ await bindForwarded(cms, true);
59
+ return false;
60
+ }
61
+ /** The visitor and the assignments the proxy forwarded, if any. */
62
+ async function bindForwarded(cms, release) {
63
+ const requestHeaders = await headers();
64
+ const slot = draftSlot();
65
+ slot.visitor = parseVisitor(requestHeaders.get(VISITOR_HEADER));
66
+ const decided = parseAssignmentsSegment(requestHeaders.get(ASSIGNMENTS_HEADER) ?? "");
67
+ slot.assignments = decided && release ? await withoutDarkTests(cms, decided) : decided;
68
+ }
69
+ /**
70
+ * Whether the release saves an `experiments` block (the built-in
71
+ * `split-traffic`) or a `campaigns` block (the built-in `calendar`) with a
72
+ * name in it: only then does a page vary by what the proxy decided. Read from
73
+ * memory, unresolved; `false` when both are missing.
74
+ */
75
+ async function variesByVisitor(cms) {
76
+ const saves = async (name, type) => {
77
+ try {
78
+ const [block] = await cms.forRelease().resolve(name, { run: false });
79
+ if (typeof block !== "object" || block === null)
80
+ return false;
81
+ const record = block;
82
+ return (record.__resolveType === type && Object.keys(record).some((key) => key !== "__resolveType"));
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ };
88
+ const [tests, campaigns] = await Promise.all([
89
+ saves("experiments", "split-traffic"),
90
+ saves("campaigns", "calendar"),
91
+ ]);
92
+ return tests || campaigns;
93
+ }
94
+ async function bindVersion(cms, options) {
95
+ const { searchParams } = options;
43
96
  const param = firstParam(searchParams, DRAFT_PARAM);
44
97
  if (param === "off")
45
98
  return false;
46
99
  const requestHeaders = await headers();
47
- const forwarded = param ?? requestHeaders.get(DRAFT_HEADER);
100
+ // The proxy read the link's draft and it's gone: the release, with the notice.
101
+ if (requestHeaders.get(DRAFT_UNAVAILABLE_HEADER) === "1") {
102
+ draftSlot().unavailable = true;
103
+ return false;
104
+ }
105
+ const forwarded = param ?? requestHeaders.get(VERSION_HEADER);
48
106
  const query = forwarded ? `?${DRAFT_PARAM}=${encodeURIComponent(forwarded)}` : "";
49
107
  const request = requestLike(requestHeaders, `/${query}`);
50
- const pointer = request ? await allowedDraft(cms, request, policy) : null;
51
- if (pointer === null)
108
+ if (request === null)
52
109
  return false;
53
- draftSlot().pointer = pointer;
110
+ // The proxy's release check for this request (`x-deco-release-check`), or one of its own:
111
+ // the host check here and the page's draft reads share it.
112
+ const releaseCheck = await draftReleaseCheck(cms, request, requestHeaders.get(RELEASE_CHECK_HEADER));
113
+ const version = await allowedDraft(cms, request, releaseCheck);
114
+ if (version === null)
115
+ return false;
116
+ const slot = draftSlot();
117
+ slot.version = version;
118
+ slot.releaseCheck = releaseCheck ?? null;
54
119
  return true;
55
120
  }
56
121
  /**
57
- * The content client for this request: the draft `bindDraft` bound, or the
58
- * release.
122
+ * The content client for this request, from what `bindDraft` bound:
123
+ * `cms.client({ version, experiments, campaigns })` with the tests and
124
+ * campaigns the proxy decided, deciding nothing again. On a route the proxy
125
+ * didn't decide for, the visitor and the preview's moment and switched tests
126
+ * and campaigns decide instead. Create one per request.
59
127
  */
60
128
  export function contentClient(cms) {
61
- const pointer = activeDraftPointer();
62
- return pointer ? cms.forDraft(pointer) : cms.forRelease();
129
+ const { version, visitor, assignments, releaseCheck } = draftSlot();
130
+ const options = { visitor, version: version?.version ?? null };
131
+ if (version !== null && releaseCheck !== null)
132
+ options.releaseCheck = releaseCheck;
133
+ if (version?.date !== undefined)
134
+ options.date = version.date;
135
+ const experiments = over(version?.experiments, assignments?.experiments);
136
+ const campaigns = over(version?.campaigns, assignments?.campaigns);
137
+ if (experiments !== undefined)
138
+ options.experiments = experiments;
139
+ if (campaigns !== undefined)
140
+ options.campaigns = campaigns;
141
+ return cms.client(options);
142
+ }
143
+ /** `decided` over `preview`, name by name; `undefined` when neither has any. */
144
+ function over(preview, decided) {
145
+ if (preview === undefined && decided === undefined)
146
+ return undefined;
147
+ return { ...preview, ...decided };
148
+ }
149
+ /**
150
+ * The content client of a page on the assignments route
151
+ * (`app/%5Fassignments/[assignments]/[[...path]]`, which the proxy's
152
+ * `rewriteToAssignmentsRoute` rewrites the CMS's pages to): the current
153
+ * release, read as `cms.client({ experiments, campaigns })` with every test
154
+ * and campaign the path's segment names on or off as the proxy decided, and
155
+ * no visitor. The proxy decides, the page renders that, reading no request,
156
+ * so Next can cache it (ISR, `revalidate = 600`). A test at 0% stays off
157
+ * whatever the path names, and a test or a campaign the content doesn't have
158
+ * stays out, never an error: a 404 here would be cached under the path by
159
+ * Next. `null` only when the segment isn't one the proxy writes: the page
160
+ * answers with `notFound()`.
161
+ *
162
+ * ```tsx
163
+ * export const revalidate = 600;
164
+ * export async function generateStaticParams() {
165
+ * return [];
166
+ * }
167
+ * export default async function Page({ params }) {
168
+ * const { assignments, path } = await params;
169
+ * const client = await assignedClient(cms, assignments);
170
+ * if (!client) notFound();
171
+ * …
172
+ * }
173
+ * ```
174
+ */
175
+ export async function assignedClient(cms, segment) {
176
+ const parsed = parseAssignmentsSegment(segment);
177
+ if (parsed === null)
178
+ return null;
179
+ const assignments = await withoutDarkTests(cms, parsed);
180
+ return cms.client({ experiments: assignments.experiments, campaigns: assignments.campaigns });
63
181
  }
64
182
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAsB,WAAW,EAAE,MAAM,cAAc,CAAC;AACpG,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE1D,OAAO,EAAsB,WAAW,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAU9B,SAAS,UAAU,CAAC,YAAsC,EAAE,GAAW;IACrE,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC,GAAG,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AACrD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAA0D,EAC1D,OAAO,GAAqB,EAAE;IAE9B,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC;IAC5C,IAAI,CAAC,CAAC,MAAM,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IACpD,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAClC,MAAM,cAAc,GAAG,MAAM,OAAO,EAAE,CAAC;IACvC,MAAM,SAAS,GAAG,KAAK,IAAI,cAAc,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,WAAW,IAAI,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAClF,MAAM,OAAO,GAAG,WAAW,CAAC,cAAyB,EAAE,IAAI,KAAK,EAAE,CAAC,CAAC;IACpE,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC1E,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACnC,SAAS,EAAE,CAAC,OAAO,GAAG,OAAO,CAAC;IAC9B,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,GAAyC;IACrE,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;IACrC,OAAO,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC;AAC5D,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EACL,kBAAkB,EAElB,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,WAAW,EACX,wBAAwB,EACxB,oBAAoB,EACpB,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE1D,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAElB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAY9B,SAAS,UAAU,CAAC,YAAsC,EAAE,GAAW;IACrE,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC,GAAG,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AACrD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAY,EAAE,OAAO,GAAqB,EAAE;IAC1E,IAAI,MAAM,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC;QACpC,wEAAwE;QACxE,MAAM,aAAa,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAChC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,MAAM,eAAe,CAAC,GAAG,CAAC;QAAE,MAAM,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC/D,OAAO,KAAK,CAAC;AACf,CAAC;AAED,mEAAmE;AACnE,KAAK,UAAU,aAAa,CAAC,GAAY,EAAE,OAAgB;IACzD,MAAM,cAAc,GAAG,MAAM,OAAO,EAAE,CAAC;IACvC,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC;IACzB,IAAI,CAAC,OAAO,GAAG,YAAY,CAAC,cAAc,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC;IAChE,MAAM,OAAO,GAAG,uBAAuB,CAAC,cAAc,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC,CAAC;IACtF,IAAI,CAAC,WAAW,GAAG,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,MAAM,gBAAgB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AACzF,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,eAAe,CAAC,GAA4B;IACzD,MAAM,KAAK,GAAG,KAAK,EAAE,IAAY,EAAE,IAAY,EAAoB,EAAE;QACnE,IAAI,CAAC;YACH,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC,OAAO,CAAU,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;YAC9E,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO,KAAK,CAAC;YAC9D,MAAM,MAAM,GAAG,KAAgC,CAAC;YAChD,OAAO,CACL,MAAM,CAAC,aAAa,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,eAAe,CAAC,CAC5F,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC;IACF,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC3C,KAAK,CAAC,aAAa,EAAE,eAAe,CAAC;QACrC,KAAK,CAAC,WAAW,EAAE,UAAU,CAAC;KAC/B,CAAC,CAAC;IACH,OAAO,KAAK,IAAI,SAAS,CAAC;AAC5B,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,GAAY,EAAE,OAAyB;IAChE,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC;IACjC,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IACpD,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAClC,MAAM,cAAc,GAAG,MAAM,OAAO,EAAE,CAAC;IACvC,+EAA+E;IAC/E,IAAI,cAAc,CAAC,GAAG,CAAC,wBAAwB,CAAC,KAAK,GAAG,EAAE,CAAC;QACzD,SAAS,EAAE,CAAC,WAAW,GAAG,IAAI,CAAC;QAC/B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,SAAS,GAAG,KAAK,IAAI,cAAc,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9D,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,WAAW,IAAI,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAClF,MAAM,OAAO,GAAG,WAAW,CAAC,cAAyB,EAAE,IAAI,KAAK,EAAE,CAAC,CAAC;IACpE,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACnC,0FAA0F;IAC1F,2DAA2D;IAC3D,MAAM,YAAY,GAAG,MAAM,iBAAiB,CAC1C,GAAG,EACH,OAAO,EACP,cAAc,CAAC,GAAG,CAAC,oBAAoB,CAAC,CACzC,CAAC;IACF,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;IAC/D,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACnC,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC;IACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACvB,IAAI,CAAC,YAAY,GAAG,YAAY,IAAI,IAAI,CAAC;IACzC,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,GAAwB;IACpD,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,SAAS,EAAE,CAAC;IACpE,MAAM,OAAO,GAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,CAAC;IAC9E,IAAI,OAAO,KAAK,IAAI,IAAI,YAAY,KAAK,IAAI;QAAE,OAAO,CAAC,YAAY,GAAG,YAAY,CAAC;IACnF,IAAI,OAAO,EAAE,IAAI,KAAK,SAAS;QAAE,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC7D,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IACzE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;IACnE,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,CAAC,WAAW,GAAG,WAAW,CAAC;IACjE,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAC3D,OAAO,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAC7B,CAAC;AAED,gFAAgF;AAChF,SAAS,IAAI,CACX,OAAsD,EACtD,OAAsD;IAEtD,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACrE,OAAO,EAAE,GAAG,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;AACpC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAuC,EACvC,OAAe;IAEf,MAAM,MAAM,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC;IAChD,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,MAAM,WAAW,GAAoB,MAAM,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACzE,OAAO,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,WAAW,CAAC,WAAW,EAAE,SAAS,EAAE,WAAW,CAAC,SAAS,EAAE,CAAC,CAAC;AAChG,CAAC"}
@@ -1,6 +1,27 @@
1
- export declare const draftSlot: () => {
2
- pointer: string | null;
3
- };
4
- /** The draft pointer bound to this request (`bindDraft`), or `null` when it renders the release. */
1
+ /**
2
+ * What a request renders, held for the request's Server Components: the
3
+ * version (a draft, or an old release, with its preview's moment and switched
4
+ * tests and campaigns), the visitor and the tests and campaigns the proxy
5
+ * decided. React's `cache()` gives one slot per request in the React Server
6
+ * Components graph, the scope a draft needs; no `next/headers` here, so the
7
+ * root components can read it without binding anything.
8
+ */
9
+ import type { RequestVersion } from "@decocms/blocks";
10
+ import type { Visitor } from "@decocms/blocks/experiments";
11
+ import type { PathAssignments } from "../assignments.ts";
12
+ interface DraftSlot {
13
+ version: RequestVersion | null;
14
+ visitor: Visitor | null;
15
+ assignments: PathAssignments | null;
16
+ /** The link named a draft that can't be read anymore: the page renders the release. */
17
+ unavailable: boolean;
18
+ /** The request's release check (`cms.checkRelease()`), which its draft reads wait for. */
19
+ releaseCheck: string | null;
20
+ }
21
+ export declare const draftSlot: () => DraftSlot;
22
+ /** The version bound to this request (`bindDraft`), or `null` when it renders the current release. */
5
23
  export declare function activeDraftPointer(): string | null;
24
+ /** Whether the request's link named a draft that can't be read anymore (`bindDraft`). */
25
+ export declare function draftUnavailable(): boolean;
26
+ export {};
6
27
  //# sourceMappingURL=slot.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slot.d.ts","sourceRoot":"","sources":["../../src/server/slot.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,SAAS,QAAa;IAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAA0B,CAAC;AAEtF,oGAAoG;AACpG,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,IAAI,CAElD"}
1
+ {"version":3,"file":"slot.d.ts","sourceRoot":"","sources":["../../src/server/slot.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAE3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,UAAU,SAAS;IACjB,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,uFAAuF;IACvF,WAAW,EAAE,OAAO,CAAC;IACrB,0FAA0F;IAC1F,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,eAAO,MAAM,SAAS,QAChB,SAOL,CAAC;AAEF,sGAAsG;AACtG,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,IAAI,CAElD;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C"}
@@ -1,13 +1,17 @@
1
- /**
2
- * The draft a request renders, held for the request's Server Components.
3
- * React's `cache()` gives one slot per request in the React Server
4
- * Components graph, the scope a draft needs; no `next/headers` here, so the
5
- * root components can read it without binding anything.
6
- */
7
1
  import { cache } from "react";
8
- export const draftSlot = cache(() => ({ pointer: null }));
9
- /** The draft pointer bound to this request (`bindDraft`), or `null` when it renders the release. */
2
+ export const draftSlot = cache(() => ({
3
+ version: null,
4
+ visitor: null,
5
+ assignments: null,
6
+ unavailable: false,
7
+ releaseCheck: null,
8
+ }));
9
+ /** The version bound to this request (`bindDraft`), or `null` when it renders the current release. */
10
10
  export function activeDraftPointer() {
11
- return draftSlot().pointer;
11
+ return draftSlot().version?.version ?? null;
12
+ }
13
+ /** Whether the request's link named a draft that can't be read anymore (`bindDraft`). */
14
+ export function draftUnavailable() {
15
+ return draftSlot().unavailable;
12
16
  }
13
17
  //# sourceMappingURL=slot.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"slot.js","sourceRoot":"","sources":["../../src/server/slot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAE9B,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,CAAC,GAA+B,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAEtF,oGAAoG;AACpG,MAAM,UAAU,kBAAkB;IAChC,OAAO,SAAS,EAAE,CAAC,OAAO,CAAC;AAC7B,CAAC"}
1
+ {"version":3,"file":"slot.js","sourceRoot":"","sources":["../../src/server/slot.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAa9B,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,CAC5B,GAAc,EAAE,CAAC,CAAC;IAChB,OAAO,EAAE,IAAI;IACb,OAAO,EAAE,IAAI;IACb,WAAW,EAAE,IAAI;IACjB,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,IAAI;CACnB,CAAC,CACH,CAAC;AAEF,sGAAsG;AACtG,MAAM,UAAU,kBAAkB;IAChC,OAAO,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,IAAI,IAAI,CAAC;AAC9C,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,gBAAgB;IAC9B,OAAO,SAAS,EAAE,CAAC,WAAW,CAAC;AACjC,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * How the proxy hands the pages the visitor: the request header it forwards
3
+ * once it read the visitor (`readVisitor` from `@decocms/blocks/experiments`,
4
+ * a new one on a first visit). The proxy writes it (`visitorRequestHeaders`
5
+ * from `@decocms/nextjs/proxy`); the pages read it (`bindDraft`, then
6
+ * `contentClient`, from `@decocms/nextjs/server`). The package's own copy: it
7
+ * never imports `@decocms/tanstack`. No imports, no state: safe in every
8
+ * bundle.
9
+ */
10
+ /**
11
+ * The request header carrying the visitor, in its stored form (the
12
+ * `deco_segment` cookie's value, `serializeVisitor`). The proxy removes any
13
+ * copy that arrives from outside (`draftRequestHeaders`).
14
+ */
15
+ export declare const VISITOR_HEADER = "x-deco-visitor";
16
+ //# sourceMappingURL=visitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visitor.d.ts","sourceRoot":"","sources":["../src/visitor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * How the proxy hands the pages the visitor: the request header it forwards
3
+ * once it read the visitor (`readVisitor` from `@decocms/blocks/experiments`,
4
+ * a new one on a first visit). The proxy writes it (`visitorRequestHeaders`
5
+ * from `@decocms/nextjs/proxy`); the pages read it (`bindDraft`, then
6
+ * `contentClient`, from `@decocms/nextjs/server`). The package's own copy: it
7
+ * never imports `@decocms/tanstack`. No imports, no state: safe in every
8
+ * bundle.
9
+ */
10
+ /**
11
+ * The request header carrying the visitor, in its stored form (the
12
+ * `deco_segment` cookie's value, `serializeVisitor`). The proxy removes any
13
+ * copy that arrives from outside (`draftRequestHeaders`).
14
+ */
15
+ export const VISITOR_HEADER = "x-deco-visitor";
16
+ //# sourceMappingURL=visitor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visitor.js","sourceRoot":"","sources":["../src/visitor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/nextjs",
3
- "version": "8.5.2",
3
+ "version": "8.7.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -56,7 +56,7 @@
56
56
  "typecheck": "node ../../scripts/tsc.mjs -p tsconfig.json"
57
57
  },
58
58
  "peerDependencies": {
59
- "@decocms/blocks": "^8.5.0",
59
+ "@decocms/blocks": "^8.6.0",
60
60
  "@opentelemetry/api": "^1.0.0",
61
61
  "next": "^16.0.0",
62
62
  "react": "^19.0.0"
@@ -67,7 +67,7 @@
67
67
  }
68
68
  },
69
69
  "devDependencies": {
70
- "@decocms/blocks": "8.5.2",
70
+ "@decocms/blocks": "8.7.0",
71
71
  "@opentelemetry/api": "^1.9.1",
72
72
  "@types/react": "^19.0.0",
73
73
  "@types/react-dom": "^19.0.0",
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * The "Preview mode" badge for the request's draft: a Server Component that
3
3
  * renders `DraftBadge` (`@decocms/blocks/preview`) only when `bindDraft`
4
- * bound a draft, so a published page emits nothing.
4
+ * bound a draft, so a published page emits nothing; when the link named a
5
+ * draft that can't be read anymore, the badge's calm notice instead.
5
6
  *
6
7
  * Render it inside the page, after `bindDraft` was awaited, never from a
7
8
  * layout: a layout renders alongside the page that fills it, so it may run
@@ -9,7 +10,7 @@
9
10
  */
10
11
  import { DraftBadge } from "@decocms/blocks/preview";
11
12
  import type { ReactNode } from "react";
12
- import { activeDraftPointer } from "./server/slot.ts";
13
+ import { activeDraftPointer, draftUnavailable } from "./server/slot.ts";
13
14
 
14
15
  export interface DraftPreviewIndicatorProps {
15
16
  /** Where "Exit preview" goes; default: the current URL with `?__draft=off`. */
@@ -18,6 +19,6 @@ export interface DraftPreviewIndicatorProps {
18
19
 
19
20
  export function DraftPreviewIndicator({ exitHref }: DraftPreviewIndicatorProps = {}): ReactNode {
20
21
  const pointer = activeDraftPointer();
21
- if (!pointer) return null;
22
+ if (!pointer) return draftUnavailable() ? <DraftBadge pointer={null} unavailable /> : null;
22
23
  return <DraftBadge pointer={pointer} exitHref={exitHref} />;
23
24
  }