@decocms/nextjs 8.6.0 → 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.
package/README.md CHANGED
@@ -52,7 +52,7 @@ export default async function DraftPage({ params, searchParams }) {
52
52
  }
53
53
  ```
54
54
 
55
- Published pages stay cached (ISR, below): the proxy sends only drafted requests to the draft route, sets the draft cookie, forwards the accepted version on `x-deco-version` (removing any copy sent from outside), and marks the response `Cache-Control: no-store, private` and `X-Robots-Tag: noindex, nofollow`. A layout that reads content goes with its route, so the cached pages read no request: under the draft route (`app/%5Fdraft/layout.tsx`) it calls `bindDraft(cms)` and `contentClient(cms)`, which read the forwarded headers; under the assignments route (`app/%5Fassignments/[assignments]/layout.tsx`) it reads through `await assignedClient(cms, (await params).assignments)`. `bindDraft` reads `headers()`, and a cached page that reads a request header renders on every request instead, so never call it in the root layout or on the assignments route.
55
+ Published pages stay cached (ISR, below): the proxy sends only preview requests to the draft route (any `?__draft=`, `off` included, or the draft cookie; never the assignments route), sets the draft cookie, forwards the accepted version on `x-deco-version` (removing any copy sent from outside), and marks the response exactly `Cache-Control: max-age=0, no-cache, must-revalidate`, `Vary: Cookie` and `X-Robots-Tag: noindex, nofollow`. A draft request checks for the latest release once (`cms.checkRelease()`) and forwards the check's id on `x-deco-release-check` (removing any copy sent from outside): the proxy's host check and read of the draft, `bindDraft`'s host check and the page's `contentClient` all wait for that one check, so one preview request fetches `latest.json` once (a page in another process than the proxy makes one check of its own). It reads a link's draft before forwarding it: a draft that can't be read anymore renders the release, expires the cookie and tells the pages on `x-deco-draft-unavailable`, so `DraftPreviewIndicator` shows a calm notice instead of an error page. A layout that reads content goes with its route, so the cached pages read no request: under the draft route (`app/%5Fdraft/layout.tsx`) it calls `bindDraft(cms)` and `contentClient(cms)`, which read the forwarded headers; under the assignments route (`app/%5Fassignments/[assignments]/layout.tsx`) it reads through `await assignedClient(cms, (await params).assignments)`. `bindDraft` reads `headers()`, and a cached page that reads a request header renders on every request instead, so never call it in the root layout or on the assignments route.
56
56
 
57
57
  A draft link can carry a published release's commit SHA instead of a draft (Studio's "Preview this version"), and either one can carry a preview time (`__at`) and tests and campaigns switched on or off (`__test`, `__campaign`), which `cms.readVersion` reads as the client's `date`, `experiments` and `campaigns`. All of it passes the same host check and renders on the draft route: a visitor on the live site never picks a version. A preview may switch on a test at 0%.
58
58
 
@@ -155,7 +155,7 @@ const { withDeco } = require("@decocms/nextjs/config");
155
155
  module.exports = withDeco({ images: { loader: "custom", loaderFile: "./src/image-loader.ts" } });
156
156
  ```
157
157
 
158
- `withDeco` puts v7's two draft header rules before your own (a `?__draft=` query, or the draft cookie: no-store, `Vary: Cookie`, noindex) and adds Studio's origins to `allowedDevOrigins`. Each can be turned off: `withDeco(config, { draftHeaders: false, studioOrigins: false })`. Next sets `Cache-Control` and `Vary` itself on dynamic pages, so only `X-Robots-Tag` is sure to arrive from these rules; the proxy's `applyDraft` sets all three on the responses it makes.
158
+ `withDeco` puts v7's two draft header rules before your own (a `?__draft=` query, or the draft cookie: `max-age=0, no-cache, must-revalidate`, `Vary: Cookie`, noindex) and adds Studio's origins to `allowedDevOrigins`. Each can be turned off: `withDeco(config, { draftHeaders: false, studioOrigins: false })`. Next sets `Cache-Control` and `Vary` itself on dynamic pages, so only `X-Robots-Tag` is sure to arrive from these rules; the proxy's `applyDraft` sets all three on the responses it makes.
159
159
 
160
160
  ## Request metric
161
161
 
@@ -1 +1 @@
1
- {"version":3,"file":"DraftPreviewIndicator.d.ts","sourceRoot":"","sources":["../src/DraftPreviewIndicator.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,MAAM,WAAW,0BAA0B;IACzC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,GAAE,0BAA+B,GAAG,SAAS,CAI9F"}
1
+ {"version":3,"file":"DraftPreviewIndicator.d.ts","sourceRoot":"","sources":["../src/DraftPreviewIndicator.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,MAAM,WAAW,0BAA0B;IACzC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,GAAE,0BAA+B,GAAG,SAAS,CAI9F"}
@@ -2,18 +2,19 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
3
  * The "Preview mode" badge for the request's draft: a Server Component that
4
4
  * renders `DraftBadge` (`@decocms/blocks/preview`) only when `bindDraft`
5
- * bound a draft, so a published page emits nothing.
5
+ * bound a draft, so a published page emits nothing; when the link named a
6
+ * draft that can't be read anymore, the badge's calm notice instead.
6
7
  *
7
8
  * Render it inside the page, after `bindDraft` was awaited, never from a
8
9
  * layout: a layout renders alongside the page that fills it, so it may run
9
10
  * before the page bound its draft.
10
11
  */
11
12
  import { DraftBadge } from "@decocms/blocks/preview";
12
- import { activeDraftPointer } from "./server/slot.js";
13
+ import { activeDraftPointer, draftUnavailable } from "./server/slot.js";
13
14
  export function DraftPreviewIndicator({ exitHref } = {}) {
14
15
  const pointer = activeDraftPointer();
15
16
  if (!pointer)
16
- return null;
17
+ return draftUnavailable() ? _jsx(DraftBadge, { pointer: null, unavailable: true }) : null;
17
18
  return _jsx(DraftBadge, { pointer: pointer, exitHref: exitHref });
18
19
  }
19
20
  //# sourceMappingURL=DraftPreviewIndicator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DraftPreviewIndicator.js","sourceRoot":"","sources":["../src/DraftPreviewIndicator.tsx"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAOtD,MAAM,UAAU,qBAAqB,CAAC,EAAE,QAAQ,EAAE,GAA+B,EAAE;IACjF,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;IACrC,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,OAAO,KAAC,UAAU,IAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAI,CAAC;AAC9D,CAAC"}
1
+ {"version":3,"file":"DraftPreviewIndicator.js","sourceRoot":"","sources":["../src/DraftPreviewIndicator.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAOxE,MAAM,UAAU,qBAAqB,CAAC,EAAE,QAAQ,EAAE,GAA+B,EAAE;IACjF,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;IACrC,IAAI,CAAC,OAAO;QAAE,OAAO,gBAAgB,EAAE,CAAC,CAAC,CAAC,KAAC,UAAU,IAAC,OAAO,EAAE,IAAI,EAAE,WAAW,SAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3F,OAAO,KAAC,UAAU,IAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAI,CAAC;AAC9D,CAAC"}
package/dist/draft.d.ts CHANGED
@@ -27,13 +27,37 @@ export declare const DRAFT_COOKIE = "__deco_draft";
27
27
  * so a client can't send one itself.
28
28
  */
29
29
  export declare const VERSION_HEADER = "x-deco-version";
30
+ /**
31
+ * The request header the proxy sets on a draft request it hands the pages:
32
+ * the id of the release check it made for it (`cms.checkRelease()`), so
33
+ * `bindDraft`'s host check and the page's draft reads wait for that check
34
+ * instead of making their own (one `latest.json` read per preview request).
35
+ * The proxy removes any copy that arrives from outside; an id the CMS didn't
36
+ * make (a proxy in another process) is a check of its own anyway.
37
+ */
38
+ export declare const RELEASE_CHECK_HEADER = "x-deco-release-check";
30
39
  /** The URL prefix a drafted request is rewritten onto (`app/%5Fdraft/[[...path]]`). */
31
40
  export declare const DRAFT_ROUTE_PREFIX = "/_draft";
32
41
  /**
33
- * The response headers of a page rendered from a draft: never stored by a
34
- * browser or a CDN (`Cache-Control: no-store, private`, `Vary: Cookie`) and
35
- * never indexed (`X-Robots-Tag: noindex, nofollow`). `{}` when `version` is
36
- * `null`, so the result of the draft decision applies as is.
42
+ * The request header the proxy sets when the link named a draft that can't be
43
+ * read anymore (deleted, say): the pages render the release and
44
+ * `DraftPreviewIndicator` says so calmly. The proxy removes any copy that
45
+ * arrives from outside.
46
+ */
47
+ export declare const DRAFT_UNAVAILABLE_HEADER = "x-deco-draft-unavailable";
48
+ /**
49
+ * The `Cache-Control` of every response to a preview request (a request with
50
+ * `?__draft=`, whatever its value, or the draft cookie): always revalidated,
51
+ * never answered from a cache (PO 2026-10-23).
52
+ */
53
+ export declare const DRAFT_CACHE_CONTROL = "max-age=0, no-cache, must-revalidate";
54
+ /**
55
+ * The response headers of a preview: never answered from a browser's or a
56
+ * CDN's cache (`Cache-Control: max-age=0, no-cache, must-revalidate`, `Vary:
57
+ * Cookie`) and never indexed (`X-Robots-Tag: noindex, nofollow`). `{}` when
58
+ * `preview` is `null`, `false` or empty, so the result of the draft decision
59
+ * applies as is: pass the version, or whether the request carries a draft
60
+ * signal at all (`?__draft=off` and a refused or unavailable draft too).
37
61
  */
38
- export declare function draftResponseHeaders(version: string | null | undefined): Record<string, string>;
62
+ export declare function draftResponseHeaders(preview: string | boolean | null | undefined): Record<string, string>;
39
63
  //# sourceMappingURL=draft.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO/F"}
1
+ {"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AAEnE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,yCAAyC,CAAC;AAE1E;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,GAC3C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB"}
package/dist/draft.js CHANGED
@@ -27,19 +27,43 @@ export const DRAFT_COOKIE = "__deco_draft";
27
27
  * so a client can't send one itself.
28
28
  */
29
29
  export const VERSION_HEADER = "x-deco-version";
30
+ /**
31
+ * The request header the proxy sets on a draft request it hands the pages:
32
+ * the id of the release check it made for it (`cms.checkRelease()`), so
33
+ * `bindDraft`'s host check and the page's draft reads wait for that check
34
+ * instead of making their own (one `latest.json` read per preview request).
35
+ * The proxy removes any copy that arrives from outside; an id the CMS didn't
36
+ * make (a proxy in another process) is a check of its own anyway.
37
+ */
38
+ export const RELEASE_CHECK_HEADER = "x-deco-release-check";
30
39
  /** The URL prefix a drafted request is rewritten onto (`app/%5Fdraft/[[...path]]`). */
31
40
  export const DRAFT_ROUTE_PREFIX = "/_draft";
32
41
  /**
33
- * The response headers of a page rendered from a draft: never stored by a
34
- * browser or a CDN (`Cache-Control: no-store, private`, `Vary: Cookie`) and
35
- * never indexed (`X-Robots-Tag: noindex, nofollow`). `{}` when `version` is
36
- * `null`, so the result of the draft decision applies as is.
42
+ * The request header the proxy sets when the link named a draft that can't be
43
+ * read anymore (deleted, say): the pages render the release and
44
+ * `DraftPreviewIndicator` says so calmly. The proxy removes any copy that
45
+ * arrives from outside.
46
+ */
47
+ export const DRAFT_UNAVAILABLE_HEADER = "x-deco-draft-unavailable";
48
+ /**
49
+ * The `Cache-Control` of every response to a preview request (a request with
50
+ * `?__draft=`, whatever its value, or the draft cookie): always revalidated,
51
+ * never answered from a cache (PO 2026-10-23).
52
+ */
53
+ export const DRAFT_CACHE_CONTROL = "max-age=0, no-cache, must-revalidate";
54
+ /**
55
+ * The response headers of a preview: never answered from a browser's or a
56
+ * CDN's cache (`Cache-Control: max-age=0, no-cache, must-revalidate`, `Vary:
57
+ * Cookie`) and never indexed (`X-Robots-Tag: noindex, nofollow`). `{}` when
58
+ * `preview` is `null`, `false` or empty, so the result of the draft decision
59
+ * applies as is: pass the version, or whether the request carries a draft
60
+ * signal at all (`?__draft=off` and a refused or unavailable draft too).
37
61
  */
38
- export function draftResponseHeaders(version) {
39
- if (!version)
62
+ export function draftResponseHeaders(preview) {
63
+ if (!preview)
40
64
  return {};
41
65
  return {
42
- "Cache-Control": "no-store, private",
66
+ "Cache-Control": DRAFT_CACHE_CONTROL,
43
67
  Vary: "Cookie",
44
68
  "X-Robots-Tag": "noindex, nofollow",
45
69
  };
package/dist/draft.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"draft.js","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;AAErC;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAE/C,uFAAuF;AACvF,MAAM,CAAC,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAkC;IACrE,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,OAAO;QACL,eAAe,EAAE,mBAAmB;QACpC,IAAI,EAAE,QAAQ;QACd,cAAc,EAAE,mBAAmB;KACpC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"draft.js","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;AAErC;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAE/C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAE3D,uFAAuF;AACvF,MAAM,CAAC,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,0BAA0B,CAAC;AAEnE;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,sCAAsC,CAAC;AAE1E;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAA4C;IAE5C,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,OAAO;QACL,eAAe,EAAE,mBAAmB;QACpC,IAAI,EAAE,QAAQ;QACd,cAAc,EAAE,mBAAmB;KACpC,CAAC;AACJ,CAAC"}
package/dist/drafts.d.ts CHANGED
@@ -46,7 +46,17 @@ export declare function requestLike(headers: Headers, pathAndQuery: string, fall
46
46
  * the preview's moment and switched tests and campaigns), or `null` for the
47
47
  * current release: what its Studio link carries, on a host the CMS allows.
48
48
  */
49
- export declare function allowedDraft(cms: Pick<CMS, "readVersion">, request: RequestLike): Promise<RequestVersion | null>;
49
+ export declare function allowedDraft(cms: Pick<CMS, "readVersion">, request: RequestLike, releaseCheck?: string | null): Promise<RequestVersion | null>;
50
+ /** Whether a version names a saved draft (a pointer, not a release's SHA and not `local`). */
51
+ export declare function namesDraft(version: string): boolean;
52
+ /**
53
+ * The release check of a request whose link names a draft
54
+ * (`cms.checkRelease()`), so its host check, its draft reads and its render
55
+ * share one read of `latest.json`: the one `forwarded` names when the CMS
56
+ * made it (the proxy's, for the same request), else a new one. `undefined`
57
+ * for any other request: the release, a release's SHA, no link.
58
+ */
59
+ export declare function draftReleaseCheck(cms: Pick<CMS, "checkRelease">, request: RequestLike, forwarded?: string | null): Promise<string | undefined>;
50
60
  /**
51
61
  * The draft cookie: the same `Set-Cookie` `cms.draftCookie` writes (a test
52
62
  * holds the two together). Studio frames the site cross-site, hence
@@ -1 +1 @@
1
- {"version":3,"file":"drafts.d.ts","sourceRoot":"","sources":["../src/drafts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,KAAK,GAAG,EAAgB,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AA2BhG;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc,GAAG,IAAI,CAM5E;AAED,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAI5D;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,MAAM,GAAG,IAAI,CAIxF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,GACpB,WAAW,GAAG,IAAI,CAQpB;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAC7B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAIhC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAM,EAAO,GAAG,MAAM,CAUvE;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAEzD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAGlE"}
1
+ {"version":3,"file":"drafts.d.ts","sourceRoot":"","sources":["../src/drafts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EACL,KAAK,GAAG,EAGR,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AA2BzB;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc,GAAG,IAAI,CAM5E;AAED,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAI5D;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,MAAM,GAAG,IAAI,CAIxF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,GACpB,WAAW,GAAG,IAAI,CAQpB;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAC7B,OAAO,EAAE,WAAW,EACpB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAC3B,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAIhC;AAKD,8FAA8F;AAC9F,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGnD;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,EAC9B,OAAO,EAAE,WAAW,EACpB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI7B;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAM,EAAO,GAAG,MAAM,CAUvE;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAEzD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAGlE"}
package/dist/drafts.js CHANGED
@@ -21,7 +21,7 @@
21
21
  *
22
22
  * No React, no Next.js and no module state: the proxy and the pages share it.
23
23
  */
24
- import { parseVersion } from "@decocms/blocks";
24
+ import { parseDraftPointer, parseVersion, } from "@decocms/blocks";
25
25
  import { DRAFT_COOKIE, DRAFT_PARAM } from "./draft.js";
26
26
  const DRAFT_OFF = "off";
27
27
  function draftParam(request) {
@@ -94,11 +94,31 @@ export function requestLike(headers, pathAndQuery, fallbackHost) {
94
94
  * the preview's moment and switched tests and campaigns), or `null` for the
95
95
  * current release: what its Studio link carries, on a host the CMS allows.
96
96
  */
97
- export async function allowedDraft(cms, request) {
97
+ export async function allowedDraft(cms, request, releaseCheck) {
98
98
  const read = readDraftVersion(request);
99
99
  if (read === null)
100
100
  return null;
101
- return (await cms.readVersion(request)) === null ? null : read;
101
+ return (await cms.readVersion(request, { releaseCheck })) === null ? null : read;
102
+ }
103
+ /** A draft pointer's version that names no draft: the release, nothing fetched. */
104
+ const LOCAL_VERSION = "local";
105
+ /** Whether a version names a saved draft (a pointer, not a release's SHA and not `local`). */
106
+ export function namesDraft(version) {
107
+ const parsed = parseDraftPointer(version);
108
+ return parsed !== null && parsed.version !== LOCAL_VERSION;
109
+ }
110
+ /**
111
+ * The release check of a request whose link names a draft
112
+ * (`cms.checkRelease()`), so its host check, its draft reads and its render
113
+ * share one read of `latest.json`: the one `forwarded` names when the CMS
114
+ * made it (the proxy's, for the same request), else a new one. `undefined`
115
+ * for any other request: the release, a release's SHA, no link.
116
+ */
117
+ export async function draftReleaseCheck(cms, request, forwarded) {
118
+ const read = readDraftVersion(request);
119
+ if (read === null || !namesDraft(read.version))
120
+ return undefined;
121
+ return cms.checkRelease(forwarded);
102
122
  }
103
123
  /**
104
124
  * The draft cookie: the same `Set-Cookie` `cms.draftCookie` writes (a test
@@ -1 +1 @@
1
- {"version":3,"file":"drafts.js","sourceRoot":"","sources":["../src/drafts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAY,YAAY,EAAyC,MAAM,iBAAiB,CAAC;AAChG,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,SAAS,GAAG,KAAK,CAAC;AAExB,SAAS,UAAU,CAAC,OAAoB;IACtC,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,MAAqB,EAAE,IAAY;IACrD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,EAAE,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,IAAI;YAAE,SAAS;QAC7D,IAAI,CAAC;YACH,OAAO,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAoB;IACnD,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAClC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACpD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IACvC,OAAO,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,cAAc,CAAC,OAAoB;IACjD,OAAO,CACL,UAAU,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC,KAAK,IAAI,CACjG,CAAC;AACJ,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,WAAW,CAAC,OAA6C;IACvE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IACzE,MAAM,IAAI,GAAG,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;IACtD,OAAO,IAAI,IAAI,IAAI,CAAC;AACtB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,OAAgB,EAChB,YAAoB,EACpB,YAAqB;IAErB,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,CAAC;IAC5D,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,CAAC;QACH,OAAO,EAAE,GAAG,EAAE,IAAI,GAAG,CAAC,YAAY,EAAE,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAA6B,EAC7B,OAAoB;IAEpB,MAAM,IAAI,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/B,OAAO,CAAC,MAAM,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACjE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,KAAK,GAAa,EAAE;IAC7D,OAAO;QACL,GAAG,YAAY,IAAI,KAAK,EAAE;QAC1B,QAAQ;QACR,GAAG,KAAK;QACR,UAAU;QACV,QAAQ;QACR,eAAe;QACf,aAAa;KACd,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,WAAW,CAAC,OAAoB;IAC9C,OAAO,UAAU,CAAC,OAAO,CAAC,KAAK,SAAS,CAAC;AAC3C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,OAAoB;IACjD,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO,KAAK,IAAI,KAAK,KAAK,SAAS,IAAI,YAAY,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACrF,CAAC"}
1
+ {"version":3,"file":"drafts.js","sourceRoot":"","sources":["../src/drafts.ts"],"names":[],"mappings":"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"}
@@ -30,8 +30,11 @@
30
30
  * (`contentClient`), deciding nothing again. It sends the visitor's cookie on
31
31
  * a first visit only, never from a preview.
32
32
  *
33
- * Cached (ISR) pages can't read a request, so a drafted request to one is
34
- * rewritten onto a dynamic draft route (`/_draft/<path>`, mounted as
33
+ * Cached (ISR) pages can't read a request, so a preview request (any draft
34
+ * signal: `?__draft=` with any value, `off` included, or the draft cookie) is
35
+ * rewritten onto a dynamic draft route, never cached, and answers
36
+ * `Cache-Control: max-age=0, no-cache, must-revalidate`; a drafted request
37
+ * is rewritten onto it (`/_draft/<path>`, mounted as
35
38
  * `app/%5Fdraft/...`: a literal `_draft` folder is private in Next and never
36
39
  * routes), and a published one onto the assignments route
37
40
  * (`/_assignments/<assignments>/<path>`, `rewriteToAssignmentsRoute`), where
@@ -52,8 +55,11 @@ import { type Visitor } from "@decocms/blocks/experiments";
52
55
  import { type NextRequest, NextResponse } from "next/server.js";
53
56
  import { type RequestAssignments } from "../assignments.ts";
54
57
  export { requestLike } from "../drafts.ts";
55
- /** What the draft decision needs from the CMS: its preview host check, and the release. */
56
- export type CMSHandle = Pick<CMS, "readVersion" | "forRelease">;
58
+ /**
59
+ * What the draft decision needs from the CMS: its preview host check, the
60
+ * release, and the draft a link names (read once before the page renders).
61
+ */
62
+ export type CMSHandle = Pick<CMS, "readVersion" | "forRelease" | "forDraft" | "checkRelease">;
57
63
  /** What the proxy does with a request's draft state. */
58
64
  export interface DraftDecision {
59
65
  /**
@@ -65,13 +71,36 @@ export interface DraftDecision {
65
71
  version: RequestVersion | null;
66
72
  /** The `Set-Cookie` the response carries: storing a link's version, or ending a preview. */
67
73
  setCookie: string | null;
74
+ /**
75
+ * The request carries a draft signal at all (`?__draft=` with any value, or
76
+ * the draft cookie): it renders on the draft route, never from a cache,
77
+ * and answers `draftResponseHeaders`, whatever `version` is.
78
+ */
79
+ preview?: boolean;
80
+ /**
81
+ * The link named a draft that can't be read (deleted, or its host
82
+ * failing): the release renders, the cookie is expired when the draft is
83
+ * gone (a failure that may pass leaves it), and the pages show
84
+ * the calm notice (`x-deco-draft-unavailable`), never an error page.
85
+ */
86
+ unavailable?: true;
87
+ /**
88
+ * The release check made for this draft request (`cms.checkRelease()`),
89
+ * forwarded on `x-deco-release-check`: the host check, the draft read here
90
+ * and the page's all wait for it, so the request reads `latest.json` once.
91
+ */
92
+ releaseCheck?: string;
68
93
  }
69
94
  export { ASSIGNMENTS_HEADER, ASSIGNMENTS_ROUTE_PREFIX, assignmentsKey, assignmentsRoutePath, assignmentsSegment, isAssignmentsRoutePath, type PathAssignments, parseAssignmentsSegment, type RequestAssignments, readAssignments, } from "../assignments.ts";
70
95
  export { DRAFT_ROUTE_PREFIX, VERSION_HEADER } from "../draft.ts";
71
96
  /**
72
97
  * The version a request renders and the cookie its response sets, on a host
73
98
  * the CMS allows (as the pages' `bindDraft` checks again). A request with
74
- * no `?__draft=` and no draft cookie never reaches the CMS.
99
+ * no `?__draft=` and no draft cookie never reaches the CMS. A draft request
100
+ * checks for the latest release once (`cms.checkRelease()`), shared by the
101
+ * host check, the read below and the page. A draft pointer
102
+ * is read once here (`forDraft(...).revision()`, the read the page makes
103
+ * again): a draft that can't be read gives the release and `unavailable`.
75
104
  */
76
105
  export declare function decideDraft(cms: CMSHandle, request: NextRequest | Request): Promise<DraftDecision>;
77
106
  /**
@@ -107,8 +136,9 @@ export declare function visitorRequestHeaders(headers: Headers, decision: Visito
107
136
  export declare function applyVisitor<R extends Response>(response: R, decision: VisitorDecision): R;
108
137
  /**
109
138
  * Each test and campaign on or off for this request, decided once:
110
- * `cms.client({ visitor, ...version })` (the visitor, and the Studio link's
111
- * version, moment and switched tests and campaigns) resolving the saved
139
+ * `cms.client({ visitor, ...version, releaseCheck })` (the visitor, the Studio
140
+ * link's version, moment and switched tests and campaigns, and the request's
141
+ * release check) resolving the saved
112
142
  * `experiments` and `campaigns` blocks. Missing blocks, or content that
113
143
  * doesn't load, give none, never an error.
114
144
  */
@@ -122,10 +152,12 @@ export declare function assignmentsRequestHeaders(headers: Headers, assignments:
122
152
  /** Whether `pathname` is on the draft route: a site answers a direct hit there with its 404. */
123
153
  export declare function isDraftRoutePath(pathname: string): boolean;
124
154
  /**
125
- * The rewrite of a drafted request onto the draft route, keeping its path and
126
- * forwarding the version and, when given, the visitor (`decideVisitor`) and
127
- * the assignments (`decideAssignments`); `null` when the request isn't
128
- * drafted or is already on the draft route.
155
+ * The rewrite of a preview request (a drafted one, or any other with a draft
156
+ * signal: `off`, a refused host, a draft that can't be read) onto the draft
157
+ * route, which is dynamic and never cached, keeping its path and forwarding
158
+ * the version and, when given, the visitor (`decideVisitor`) and the
159
+ * assignments (`decideAssignments`); `null` when the request isn't a preview
160
+ * or is already on the draft route.
129
161
  */
130
162
  export declare function rewriteToDraftRoute(request: NextRequest, decision: DraftDecision, visitor?: VisitorDecision, assignments?: RequestAssignments): NextResponse | null;
131
163
  /**
@@ -143,7 +175,7 @@ export declare function rewriteToDraftRoute(request: NextRequest, decision: Draf
143
175
  * minutes stale (the first request after that still gets it while Next
144
176
  * renders it again), a banner switching on late, which is accepted.
145
177
  *
146
- * `null` for a drafted request (it renders on the draft route, never cached)
178
+ * `null` for a preview request (it renders on the draft route, never cached)
147
179
  * and for a path already on the draft or assignments route. When the content
148
180
  * saves tests or campaigns, the response is marked `Cache-Control: private,
149
181
  * no-cache`: Next keeps one cached page per set of assignments, but the URL
@@ -154,11 +186,15 @@ export declare function rewriteToDraftRoute(request: NextRequest, decision: Draf
154
186
  */
155
187
  export declare function rewriteToAssignmentsRoute(cms: Pick<CMS, "client">, request: NextRequest, draft: DraftDecision, visitor: VisitorDecision): Promise<NextResponse | null>;
156
188
  /**
157
- * The cookie and the response headers of a draft: `Set-Cookie`, and for a
158
- * drafted response `Cache-Control: no-store, private`, `Vary: Cookie` and
189
+ * The cookie and the response headers of a preview: `Set-Cookie`, and for
190
+ * any request with a draft signal (`preview`, or a version)
191
+ * `Cache-Control: max-age=0, no-cache, must-revalidate`, `Vary: Cookie` and
159
192
  * `X-Robots-Tag: noindex, nofollow` (`draftResponseHeaders`). With the
160
193
  * version in a cookie a draft shares its URL with the published page, so a
161
- * cache keyed on the URL alone would serve it to visitors.
194
+ * cache keyed on the URL alone would serve it to visitors. The proxy answers
195
+ * before the page renders, so it can't hold the cookie back from a failed
196
+ * render; `decideDraft` reads the draft first instead, and a draft that
197
+ * can't be read expires the cookie.
162
198
  */
163
199
  export declare function applyDraft<R extends Response>(response: R, decision: DraftDecision): R;
164
200
  export interface DraftProxyOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"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"}
@@ -1,17 +1,27 @@
1
1
  import { readVisitor, serializeVisitor } from "@decocms/blocks/experiments";
2
2
  import { NextResponse } from "next/server.js";
3
3
  import { ASSIGNMENTS_HEADER, assignmentsRoutePath, assignmentsSegment, isAssignmentsRoutePath, readAssignmentsAndScope, } from "../assignments.js";
4
- import { DRAFT_ROUTE_PREFIX, draftResponseHeaders, VERSION_HEADER } from "../draft.js";
5
- import { allowedDraft, draftCookie, draftLinkValue, endsPreview, hasDraftSignal, requestLike, } from "../drafts.js";
4
+ import { DRAFT_ROUTE_PREFIX, DRAFT_UNAVAILABLE_HEADER, draftResponseHeaders, RELEASE_CHECK_HEADER, VERSION_HEADER, } from "../draft.js";
5
+ import { allowedDraft, draftCookie, draftLinkValue, draftReleaseCheck, endsPreview, hasDraftSignal, requestLike, } from "../drafts.js";
6
6
  import { VISITOR_HEADER } from "../visitor.js";
7
7
  export { requestLike } from "../drafts.js";
8
8
  export { ASSIGNMENTS_HEADER, ASSIGNMENTS_ROUTE_PREFIX, assignmentsKey, assignmentsRoutePath, assignmentsSegment, isAssignmentsRoutePath, parseAssignmentsSegment, readAssignments, } from "../assignments.js";
9
9
  export { DRAFT_ROUTE_PREFIX, VERSION_HEADER } from "../draft.js";
10
10
  const INERT = Object.freeze({ version: null, setCookie: null });
11
+ /** A preview that renders the release (`off`, a refused host, a value that doesn't parse). */
12
+ const RELEASE_PREVIEW = Object.freeze({
13
+ version: null,
14
+ setCookie: null,
15
+ preview: true,
16
+ });
11
17
  /**
12
18
  * The version a request renders and the cookie its response sets, on a host
13
19
  * the CMS allows (as the pages' `bindDraft` checks again). A request with
14
- * no `?__draft=` and no draft cookie never reaches the CMS.
20
+ * no `?__draft=` and no draft cookie never reaches the CMS. A draft request
21
+ * checks for the latest release once (`cms.checkRelease()`), shared by the
22
+ * host check, the read below and the page. A draft pointer
23
+ * is read once here (`forDraft(...).revision()`, the read the page makes
24
+ * again): a draft that can't be read gives the release and `unavailable`.
15
25
  */
16
26
  export async function decideDraft(cms, request) {
17
27
  const url = new URL(request.url);
@@ -20,15 +30,55 @@ export async function decideDraft(cms, request) {
20
30
  return INERT;
21
31
  // Ending a preview is allowed on any host, so a cookie set earlier always goes.
22
32
  if (endsPreview(like))
23
- return { version: null, setCookie: draftCookie("", ["Max-Age=0"]) };
24
- const version = await allowedDraft(cms, like);
33
+ return { ...RELEASE_PREVIEW, setCookie: expiredCookie() };
34
+ // One release check per draft request, shared below and by the page.
35
+ const releaseCheck = await draftReleaseCheck(cms, like);
36
+ const version = await allowedDraft(cms, like, releaseCheck);
25
37
  if (version === null)
26
- return INERT;
38
+ return RELEASE_PREVIEW;
39
+ if (releaseCheck !== undefined) {
40
+ try {
41
+ await cms.forDraft(version.version, { releaseCheck }).revision();
42
+ }
43
+ catch (error) {
44
+ // Gone (deleted): the preview ends. Any other failure (the drafts' host
45
+ // down, a timeout) leaves the cookie alone, so the next request tries again.
46
+ const setCookie = isDraftGone(error) ? expiredCookie() : null;
47
+ return { ...RELEASE_PREVIEW, setCookie, unavailable: true };
48
+ }
49
+ }
27
50
  const link = draftLinkValue(like);
28
- return { version, setCookie: link ? draftCookie(encodeURIComponent(link)) : null };
51
+ return {
52
+ version,
53
+ setCookie: link ? draftCookie(encodeURIComponent(link)) : null,
54
+ preview: true,
55
+ ...(releaseCheck === undefined ? {} : { releaseCheck }),
56
+ };
57
+ }
58
+ /**
59
+ * Whether a draft read failed because the drafts' host no longer has the
60
+ * draft (`404`/`410`), as opposed to a failure that may pass (a `5xx`, a
61
+ * timeout, a network error). Reads the error and its causes.
62
+ */
63
+ function isDraftGone(error) {
64
+ for (let e = error, depth = 0; e instanceof Error && depth < 5; e = e.cause, depth++) {
65
+ if (/draft changes: HTTP (404|410)\b/.test(e.message))
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ /** The `Set-Cookie` that ends a preview. */
71
+ function expiredCookie() {
72
+ return draftCookie("", ["Max-Age=0"]);
29
73
  }
30
74
  /** The headers the proxy writes for the pages: never taken from outside. */
31
- const OWN_HEADERS = [VERSION_HEADER, VISITOR_HEADER, ASSIGNMENTS_HEADER];
75
+ const OWN_HEADERS = [
76
+ VERSION_HEADER,
77
+ VISITOR_HEADER,
78
+ ASSIGNMENTS_HEADER,
79
+ DRAFT_UNAVAILABLE_HEADER,
80
+ RELEASE_CHECK_HEADER,
81
+ ];
32
82
  /**
33
83
  * The request headers to forward: the accepted version on `x-deco-version`,
34
84
  * so the root layout can bind the draft on the same request (a layout gets no
@@ -43,6 +93,11 @@ export function draftRequestHeaders(request, decision) {
43
93
  headers.delete(name);
44
94
  if (decision.version)
45
95
  headers.set(VERSION_HEADER, decision.version.version);
96
+ if (decision.version && decision.releaseCheck) {
97
+ headers.set(RELEASE_CHECK_HEADER, decision.releaseCheck);
98
+ }
99
+ if (decision.unavailable)
100
+ headers.set(DRAFT_UNAVAILABLE_HEADER, "1");
46
101
  return headers;
47
102
  }
48
103
  /** Whether the visitor asked over https (`x-forwarded-proto` first, as a host in front reports it). */
@@ -84,13 +139,20 @@ export function applyVisitor(response, decision) {
84
139
  }
85
140
  /**
86
141
  * Each test and campaign on or off for this request, decided once:
87
- * `cms.client({ visitor, ...version })` (the visitor, and the Studio link's
88
- * version, moment and switched tests and campaigns) resolving the saved
142
+ * `cms.client({ visitor, ...version, releaseCheck })` (the visitor, the Studio
143
+ * link's version, moment and switched tests and campaigns, and the request's
144
+ * release check) resolving the saved
89
145
  * `experiments` and `campaigns` blocks. Missing blocks, or content that
90
146
  * doesn't load, give none, never an error.
91
147
  */
92
148
  export function decideAssignments(cms, draft, visitor) {
93
- return readAssignmentsAndScope(cms, { visitor: visitor.visitor, ...draft.version }).then((read) => read.assignments);
149
+ // A draft's read shares the request's release check (`decideDraft`), never one of its own.
150
+ const releaseCheck = draft.version ? draft.releaseCheck : undefined;
151
+ return readAssignmentsAndScope(cms, {
152
+ visitor: visitor.visitor,
153
+ ...draft.version,
154
+ ...(releaseCheck ? { releaseCheck } : {}),
155
+ }).then((read) => read.assignments);
94
156
  }
95
157
  /**
96
158
  * The forwarded headers with the decided tests and campaigns on
@@ -116,13 +178,15 @@ export function isDraftRoutePath(pathname) {
116
178
  return pathname === DRAFT_ROUTE_PREFIX || pathname.startsWith(`${DRAFT_ROUTE_PREFIX}/`);
117
179
  }
118
180
  /**
119
- * The rewrite of a drafted request onto the draft route, keeping its path and
120
- * forwarding the version and, when given, the visitor (`decideVisitor`) and
121
- * the assignments (`decideAssignments`); `null` when the request isn't
122
- * drafted or is already on the draft route.
181
+ * The rewrite of a preview request (a drafted one, or any other with a draft
182
+ * signal: `off`, a refused host, a draft that can't be read) onto the draft
183
+ * route, which is dynamic and never cached, keeping its path and forwarding
184
+ * the version and, when given, the visitor (`decideVisitor`) and the
185
+ * assignments (`decideAssignments`); `null` when the request isn't a preview
186
+ * or is already on the draft route.
123
187
  */
124
188
  export function rewriteToDraftRoute(request, decision, visitor, assignments) {
125
- if (!decision.version)
189
+ if (!decision.version && !decision.preview)
126
190
  return null;
127
191
  const url = request.nextUrl.clone();
128
192
  if (isDraftRoutePath(url.pathname))
@@ -149,7 +213,7 @@ const ASSIGNED_CACHE_CONTROL = "private, no-cache";
149
213
  * minutes stale (the first request after that still gets it while Next
150
214
  * renders it again), a banner switching on late, which is accepted.
151
215
  *
152
- * `null` for a drafted request (it renders on the draft route, never cached)
216
+ * `null` for a preview request (it renders on the draft route, never cached)
153
217
  * and for a path already on the draft or assignments route. When the content
154
218
  * saves tests or campaigns, the response is marked `Cache-Control: private,
155
219
  * no-cache`: Next keeps one cached page per set of assignments, but the URL
@@ -159,7 +223,7 @@ const ASSIGNED_CACHE_CONTROL = "private, no-cache";
159
223
  * (`isAssignmentsRoutePath`).
160
224
  */
161
225
  export async function rewriteToAssignmentsRoute(cms, request, draft, visitor) {
162
- if (draft.version)
226
+ if (draft.version || draft.preview)
163
227
  return null;
164
228
  const url = request.nextUrl.clone();
165
229
  if (isDraftRoutePath(url.pathname) || isAssignmentsRoutePath(url.pathname))
@@ -176,16 +240,21 @@ export async function rewriteToAssignmentsRoute(cms, request, draft, visitor) {
176
240
  return response;
177
241
  }
178
242
  /**
179
- * The cookie and the response headers of a draft: `Set-Cookie`, and for a
180
- * drafted response `Cache-Control: no-store, private`, `Vary: Cookie` and
243
+ * The cookie and the response headers of a preview: `Set-Cookie`, and for
244
+ * any request with a draft signal (`preview`, or a version)
245
+ * `Cache-Control: max-age=0, no-cache, must-revalidate`, `Vary: Cookie` and
181
246
  * `X-Robots-Tag: noindex, nofollow` (`draftResponseHeaders`). With the
182
247
  * version in a cookie a draft shares its URL with the published page, so a
183
- * cache keyed on the URL alone would serve it to visitors.
248
+ * cache keyed on the URL alone would serve it to visitors. The proxy answers
249
+ * before the page renders, so it can't hold the cookie back from a failed
250
+ * render; `decideDraft` reads the draft first instead, and a draft that
251
+ * can't be read expires the cookie.
184
252
  */
185
253
  export function applyDraft(response, decision) {
186
254
  if (decision.setCookie)
187
255
  response.headers.append("set-cookie", decision.setCookie);
188
- for (const [name, value] of Object.entries(draftResponseHeaders(decision.version?.version))) {
256
+ const preview = decision.preview === true || decision.version !== null;
257
+ for (const [name, value] of Object.entries(draftResponseHeaders(preview))) {
189
258
  response.headers.set(name, value);
190
259
  }
191
260
  return response;
@@ -204,7 +273,7 @@ export function draftProxy(cms, options = {}) {
204
273
  }
205
274
  const decision = await decideDraft(cms, request);
206
275
  const visitor = decideVisitor(request, decision);
207
- const onAssignmentsRoute = decision.version === null && assignments?.(request) === true;
276
+ const onAssignmentsRoute = decision.version === null && decision.preview !== true && assignments?.(request) === true;
208
277
  const rewritten = onAssignmentsRoute
209
278
  ? await rewriteToAssignmentsRoute(cms, request, decision, visitor)
210
279
  : null;