@decocms/nextjs 8.5.1 → 8.6.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 (46) hide show
  1. package/README.md +81 -14
  2. package/dist/assignments.d.ts +95 -0
  3. package/dist/assignments.d.ts.map +1 -0
  4. package/dist/assignments.js +229 -0
  5. package/dist/assignments.js.map +1 -0
  6. package/dist/draft.d.ts +9 -7
  7. package/dist/draft.d.ts.map +1 -1
  8. package/dist/draft.js +10 -8
  9. package/dist/draft.js.map +1 -1
  10. package/dist/drafts.d.ts +29 -60
  11. package/dist/drafts.d.ts.map +1 -1
  12. package/dist/drafts.js +38 -117
  13. package/dist/drafts.js.map +1 -1
  14. package/dist/index.d.ts +5 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +5 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/instrumentation/index.d.ts +1 -1
  19. package/dist/instrumentation/index.js +3 -3
  20. package/dist/instrumentation/index.js.map +1 -1
  21. package/dist/proxy/index.d.ts +134 -30
  22. package/dist/proxy/index.d.ts.map +1 -1
  23. package/dist/proxy/index.js +169 -37
  24. package/dist/proxy/index.js.map +1 -1
  25. package/dist/server/index.d.ts +59 -20
  26. package/dist/server/index.d.ts.map +1 -1
  27. package/dist/server/index.js +129 -25
  28. package/dist/server/index.js.map +1 -1
  29. package/dist/server/slot.d.ts +19 -4
  30. package/dist/server/slot.d.ts.map +1 -1
  31. package/dist/server/slot.js +3 -9
  32. package/dist/server/slot.js.map +1 -1
  33. package/dist/visitor.d.ts +16 -0
  34. package/dist/visitor.d.ts.map +1 -0
  35. package/dist/visitor.js +16 -0
  36. package/dist/visitor.js.map +1 -0
  37. package/package.json +3 -3
  38. package/src/assignments.ts +278 -0
  39. package/src/draft.ts +10 -8
  40. package/src/drafts.ts +38 -126
  41. package/src/index.ts +5 -2
  42. package/src/instrumentation/index.ts +3 -3
  43. package/src/proxy/index.ts +269 -56
  44. package/src/server/index.ts +152 -33
  45. package/src/server/slot.ts +18 -5
  46. package/src/visitor.ts +16 -0
@@ -1,5 +1,6 @@
1
1
  /**
2
- * `@decocms/nextjs/proxy`: draft previews in a Next.js proxy (`proxy.ts`, or
2
+ * `@decocms/nextjs/proxy`: draft previews, the visitor and what a request's
3
+ * content gives it, decided once, in a Next.js proxy (`proxy.ts`, or
3
4
  * `middleware.ts` before Next 16). No React and no block map: pass the proxy
4
5
  * a CMS handle made with an empty block map and the app's options, which
5
6
  * shares the app's instance (one per process) without pulling the site's
@@ -10,73 +11,176 @@
10
11
  * export const proxyCms = createCMS({ blocks: {}, ...cmsConfig });
11
12
  * ```
12
13
  *
13
- * A site with a proxy of its own calls the pieces (`decideDraft`, then
14
- * `draftRequestHeaders` on the request it forwards, then `applyDraft` on the
15
- * response); a site without one exports `draftProxy(...)`.
14
+ * A site with a proxy of its own calls the pieces (`decideDraft`,
15
+ * `decideVisitor` and `decideAssignments`, then `draftRequestHeaders`,
16
+ * `visitorRequestHeaders` and `assignmentsRequestHeaders` on the request it
17
+ * forwards, then `applyDraft` and `applyVisitor` on the response); a site
18
+ * without one exports `draftProxy(...)`.
16
19
  *
17
- * Statically rendered pages can't read a draft, so a drafted request to one
18
- * is rewritten onto a dynamic draft route (`/_draft/<path>`, mounted as
20
+ * The proxy decides: the version (`decideDraft`: a Studio link's draft or old
21
+ * release, after the host allowlist, with the preview's moment and switched
22
+ * tests and campaigns), the visitor (`decideVisitor`: `readVisitor` from
23
+ * `@decocms/blocks/experiments`, the `deco_segment` cookie or a new visitor)
24
+ * and, from both, each test and campaign on or off (`decideAssignments`:
25
+ * `cms.client({ version, visitor, date, experiments, campaigns })` resolving
26
+ * the saved `experiments` and `campaigns` blocks). It forwards them on
27
+ * `x-deco-version`, `x-deco-visitor` and `x-deco-assignments` (copies from
28
+ * outside are always removed), and the page builds
29
+ * `cms.client({ version, experiments, campaigns })` from them
30
+ * (`contentClient`), deciding nothing again. It sends the visitor's cookie on
31
+ * a first visit only, never from a preview.
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
19
35
  * `app/%5Fdraft/...`: a literal `_draft` folder is private in Next and never
20
- * routes).
36
+ * routes), and a published one onto the assignments route
37
+ * (`/_assignments/<assignments>/<path>`, `rewriteToAssignmentsRoute`), where
38
+ * `assignedClient` from `@decocms/nextjs/server` builds the client from the
39
+ * path.
40
+ *
41
+ * The binding caches nothing. A site that caches in front of its pages (a
42
+ * CDN keyed on a request header) keys on the request's assignments, never on
43
+ * the visitor: `assignmentsKey(await decideAssignments(proxyCms, draft,
44
+ * visitor))`, `""` when there are none.
21
45
  *
22
46
  * The proxy measures no requests: it runs before Next routes and renders the
23
47
  * page. `http.server.request.duration` comes from Next's own request span
24
48
  * (`register` from `@decocms/nextjs/instrumentation`).
25
49
  */
26
- import type { CMS } from "@decocms/blocks";
50
+ import type { CMS, RequestVersion } from "@decocms/blocks";
51
+ import { type Visitor } from "@decocms/blocks/experiments";
27
52
  import { type NextRequest, NextResponse } from "next/server.js";
28
- import { type PreviewPolicy } from "../drafts.ts";
29
- export { inferredPreviewHosts, type PreviewPolicy, previewHostDecision, requestLike, } from "../drafts.ts";
30
- /** What the proxy needs from the CMS: the site's declared preview hosts and their check. */
31
- export type CMSHandle = Pick<CMS, "draftPointer" | "settings" | "forRelease">;
53
+ import { type RequestAssignments } from "../assignments.ts";
54
+ 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">;
32
57
  /** What the proxy does with a request's draft state. */
33
58
  export interface DraftDecision {
34
- /** The draft this request renders, after the host check; `null` for the release. */
35
- pointer: string | null;
36
- /** The `Set-Cookie` the response carries: storing a draft link's pointer, or ending a preview. */
59
+ /**
60
+ * What this request's Studio link asks for, after the host check: the
61
+ * version (a draft pointer, or a release's commit SHA) and the preview's
62
+ * moment and switched tests and campaigns, in the shape of
63
+ * `cms.client()`'s options; `null` for the current release.
64
+ */
65
+ version: RequestVersion | null;
66
+ /** The `Set-Cookie` the response carries: storing a link's version, or ending a preview. */
37
67
  setCookie: string | null;
38
68
  }
39
- export { DRAFT_ROUTE_PREFIX } from "../draft.ts";
69
+ export { ASSIGNMENTS_HEADER, ASSIGNMENTS_ROUTE_PREFIX, assignmentsKey, assignmentsRoutePath, assignmentsSegment, isAssignmentsRoutePath, type PathAssignments, parseAssignmentsSegment, type RequestAssignments, readAssignments, } from "../assignments.ts";
70
+ export { DRAFT_ROUTE_PREFIX, VERSION_HEADER } from "../draft.ts";
40
71
  /**
41
- * The draft a request renders and the cookie its response sets, under
42
- * `policy` (the same options the pages pass to `bindDraft`). A request with
72
+ * The version a request renders and the cookie its response sets, on a host
73
+ * the CMS allows (as the pages' `bindDraft` checks again). A request with
43
74
  * no `?__draft=` and no draft cookie never reaches the CMS.
44
75
  */
45
- export declare function decideDraft(cms: CMSHandle, request: NextRequest | Request, policy?: PreviewPolicy): Promise<DraftDecision>;
76
+ export declare function decideDraft(cms: CMSHandle, request: NextRequest | Request): Promise<DraftDecision>;
46
77
  /**
47
- * The request headers to forward: the accepted pointer on `x-deco-draft`, so
48
- * the root layout can bind the draft on the same request (a layout gets no
78
+ * The request headers to forward: the accepted version on `x-deco-version`,
79
+ * so the root layout can bind the draft on the same request (a layout gets no
49
80
  * `searchParams`, and on a draft link the cookie is only on the response).
50
- * A copy that arrives from outside is always removed.
81
+ * A copy that arrives from outside is always removed, and so are any
82
+ * `x-deco-visitor` and `x-deco-assignments` (`visitorRequestHeaders` and
83
+ * `assignmentsRequestHeaders` set the proxy's own).
51
84
  */
52
85
  export declare function draftRequestHeaders(request: NextRequest | Request, decision: DraftDecision): Headers;
86
+ /** The visitor of one request. */
87
+ export interface VisitorDecision {
88
+ /** Who is visiting: the `deco_segment` cookie's visitor, or a new one. */
89
+ visitor: Visitor;
90
+ /** The `deco_segment` `Set-Cookie`, on a first visit to the live site; `null` otherwise. */
91
+ setCookie: string | null;
92
+ }
93
+ /**
94
+ * The visitor of a request (`readVisitor`: the `deco_segment` cookie, or a
95
+ * new visitor when it had no id) and the cookie to send back, only on a first
96
+ * visit and never from a preview (`draft` holds a version): Studio is not a
97
+ * visitor. Reads no content.
98
+ */
99
+ export declare function decideVisitor(request: NextRequest | Request, draft: DraftDecision): VisitorDecision;
100
+ /**
101
+ * The forwarded headers with the visitor on `x-deco-visitor`. Pass it the
102
+ * headers `draftRequestHeaders` gave, which already removed a copy from
103
+ * outside.
104
+ */
105
+ export declare function visitorRequestHeaders(headers: Headers, decision: VisitorDecision): Headers;
106
+ /** The `deco_segment` cookie on the response, on a first visit. */
107
+ export declare function applyVisitor<R extends Response>(response: R, decision: VisitorDecision): R;
108
+ /**
109
+ * 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
112
+ * `experiments` and `campaigns` blocks. Missing blocks, or content that
113
+ * doesn't load, give none, never an error.
114
+ */
115
+ export declare function decideAssignments(cms: Pick<CMS, "client">, draft: DraftDecision, visitor: VisitorDecision): Promise<RequestAssignments>;
116
+ /**
117
+ * The forwarded headers with the decided tests and campaigns on
118
+ * `x-deco-assignments` (`assignmentsSegment`), for `bindDraft` and
119
+ * `contentClient`. Pass it the headers `draftRequestHeaders` gave.
120
+ */
121
+ export declare function assignmentsRequestHeaders(headers: Headers, assignments: RequestAssignments): Headers;
53
122
  /** Whether `pathname` is on the draft route: a site answers a direct hit there with its 404. */
54
123
  export declare function isDraftRoutePath(pathname: string): boolean;
55
124
  /**
56
125
  * The rewrite of a drafted request onto the draft route, keeping its path and
57
- * forwarding the pointer; `null` when the request isn't drafted or is already
58
- * on the draft route.
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.
59
129
  */
60
- export declare function rewriteToDraftRoute(request: NextRequest, decision: DraftDecision): NextResponse | null;
130
+ export declare function rewriteToDraftRoute(request: NextRequest, decision: DraftDecision, visitor?: VisitorDecision, assignments?: RequestAssignments): NextResponse | null;
131
+ /**
132
+ * The rewrite of a published request onto the assignments route,
133
+ * `/_assignments/<assignments>/<path>`: each test on or off for the visitor
134
+ * and each campaign running or not now (`readAssignments` on the current
135
+ * release), as one path segment, so Next's own path-keyed cache (ISR, the
136
+ * full route cache) keeps one entry per set of assignments. Visitors on the
137
+ * same side of every test share a path; the visitor id is never in it, and
138
+ * the page reads neither the visitor nor the version (the proxy's headers are
139
+ * removed from the request it forwards), so Next can cache it. The page
140
+ * builds its client with `assignedClient` from `@decocms/nextjs/server`,
141
+ * `cms.client({ experiments, campaigns })` from the path: the proxy decides,
142
+ * the page renders that. A cached page (ISR, `revalidate = 600`) is about 10
143
+ * minutes stale (the first request after that still gets it while Next
144
+ * renders it again), a banner switching on late, which is accepted.
145
+ *
146
+ * `null` for a drafted request (it renders on the draft route, never cached)
147
+ * and for a path already on the draft or assignments route. When the content
148
+ * saves tests or campaigns, the response is marked `Cache-Control: private,
149
+ * no-cache`: Next keeps one cached page per set of assignments, but the URL
150
+ * the visitor asked for answers by their cookie and the moment, so a CDN in
151
+ * front must not share it. Call it only for the paths the assignments route
152
+ * renders (the CMS's pages), and answer a direct hit on the route with a 404
153
+ * (`isAssignmentsRoutePath`).
154
+ */
155
+ export declare function rewriteToAssignmentsRoute(cms: Pick<CMS, "client">, request: NextRequest, draft: DraftDecision, visitor: VisitorDecision): Promise<NextResponse | null>;
61
156
  /**
62
157
  * The cookie and the response headers of a draft: `Set-Cookie`, and for a
63
158
  * drafted response `Cache-Control: no-store, private`, `Vary: Cookie` and
64
159
  * `X-Robots-Tag: noindex, nofollow` (`draftResponseHeaders`). With the
65
- * pointer in a cookie a draft shares its URL with the published page, so a
160
+ * version in a cookie a draft shares its URL with the published page, so a
66
161
  * cache keyed on the URL alone would serve it to visitors.
67
162
  */
68
163
  export declare function applyDraft<R extends Response>(response: R, decision: DraftDecision): R;
69
- export interface DraftProxyOptions extends PreviewPolicy {
164
+ export interface DraftProxyOptions {
70
165
  /**
71
166
  * Which drafted requests go to the draft route (a statically rendered
72
167
  * page's). Default: every drafted request outside the draft route. Return
73
168
  * `false` for a route that is already dynamic and binds the draft itself.
74
169
  */
75
170
  rewrite?: (request: NextRequest) => boolean;
171
+ /**
172
+ * Which published requests render on the assignments route
173
+ * (`rewriteToAssignmentsRoute`): the CMS's pages, when the site mounts
174
+ * `app/%5Fassignments/[assignments]/[[...path]]`. Default: none. A direct
175
+ * hit on the route gets a 404 once this is set.
176
+ */
177
+ assignments?: (request: NextRequest) => boolean;
76
178
  }
77
179
  /**
78
- * The whole draft flow for a site with no proxy of its own:
79
- * `export const proxy = draftProxy(proxyCms, { site: "my-store" })`.
180
+ * The whole flow for a site with no proxy of its own:
181
+ * `export const proxy = draftProxy(proxyCms)`. Every
182
+ * request it forwards carries the version, the visitor and the assignments
183
+ * it decided.
80
184
  */
81
- export declare function draftProxy(cms: CMSHandle, options?: DraftProxyOptions): (request: NextRequest) => Promise<NextResponse>;
185
+ export declare function draftProxy(cms: CMSHandle & Pick<CMS, "client">, options?: DraftProxyOptions): (request: NextRequest) => Promise<NextResponse>;
82
186
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEhE,OAAO,EAML,KAAK,aAAa,EAEnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,oBAAoB,EACpB,KAAK,aAAa,EAClB,mBAAmB,EACnB,WAAW,GACZ,MAAM,cAAc,CAAC;AAEtB,4FAA4F;AAC5F,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,GAAG,UAAU,GAAG,YAAY,CAAC,CAAC;AAE9E,wDAAwD;AACxD,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,kGAAkG;IAClG,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAIjD;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,WAAW,GAAG,OAAO,EAC9B,MAAM,GAAE,aAAkB,GACzB,OAAO,CAAC,aAAa,CAAC,CAYxB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,WAAW,GAAG,OAAO,EAC9B,QAAQ,EAAE,aAAa,GACtB,OAAO,CAKT;AAED,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,aAAa,GACtB,YAAY,GAAG,IAAI,CAQrB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,aAAa,GAAG,CAAC,CAMtF;AAED,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;CAC7C;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,SAAS,EACd,OAAO,GAAE,iBAAsB,GAC9B,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAUjD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"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"}
@@ -1,42 +1,114 @@
1
+ import { readVisitor, serializeVisitor } from "@decocms/blocks/experiments";
1
2
  import { NextResponse } from "next/server.js";
2
- import { DRAFT_HEADER, DRAFT_ROUTE_PREFIX, draftResponseHeaders } from "../draft.js";
3
- import { allowedDraft, draftCookie, endsPreview, fromDraftLink, hasDraftSignal, requestLike, } from "../drafts.js";
4
- export { inferredPreviewHosts, previewHostDecision, requestLike, } from "../drafts.js";
5
- export { DRAFT_ROUTE_PREFIX } from "../draft.js";
6
- const INERT = Object.freeze({ pointer: null, setCookie: null });
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";
6
+ import { VISITOR_HEADER } from "../visitor.js";
7
+ export { requestLike } from "../drafts.js";
8
+ export { ASSIGNMENTS_HEADER, ASSIGNMENTS_ROUTE_PREFIX, assignmentsKey, assignmentsRoutePath, assignmentsSegment, isAssignmentsRoutePath, parseAssignmentsSegment, readAssignments, } from "../assignments.js";
9
+ export { DRAFT_ROUTE_PREFIX, VERSION_HEADER } from "../draft.js";
10
+ const INERT = Object.freeze({ version: null, setCookie: null });
7
11
  /**
8
- * The draft a request renders and the cookie its response sets, under
9
- * `policy` (the same options the pages pass to `bindDraft`). A request with
12
+ * The version a request renders and the cookie its response sets, on a host
13
+ * the CMS allows (as the pages' `bindDraft` checks again). A request with
10
14
  * no `?__draft=` and no draft cookie never reaches the CMS.
11
15
  */
12
- export async function decideDraft(cms, request, policy = {}) {
16
+ export async function decideDraft(cms, request) {
13
17
  const url = new URL(request.url);
14
18
  const like = requestLike(request.headers, url.pathname + url.search, url.host);
15
19
  if (like === null || !hasDraftSignal(like))
16
20
  return INERT;
17
21
  // Ending a preview is allowed on any host, so a cookie set earlier always goes.
18
22
  if (endsPreview(like))
19
- return { pointer: null, setCookie: draftCookie("", ["Max-Age=0"]) };
20
- const pointer = await allowedDraft(cms, like, policy);
21
- if (pointer === null)
23
+ return { version: null, setCookie: draftCookie("", ["Max-Age=0"]) };
24
+ const version = await allowedDraft(cms, like);
25
+ if (version === null)
22
26
  return INERT;
23
- return {
24
- pointer,
25
- setCookie: fromDraftLink(like, pointer) ? draftCookie(encodeURIComponent(pointer)) : null,
26
- };
27
+ const link = draftLinkValue(like);
28
+ return { version, setCookie: link ? draftCookie(encodeURIComponent(link)) : null };
27
29
  }
30
+ /** The headers the proxy writes for the pages: never taken from outside. */
31
+ const OWN_HEADERS = [VERSION_HEADER, VISITOR_HEADER, ASSIGNMENTS_HEADER];
28
32
  /**
29
- * The request headers to forward: the accepted pointer on `x-deco-draft`, so
30
- * the root layout can bind the draft on the same request (a layout gets no
33
+ * The request headers to forward: the accepted version on `x-deco-version`,
34
+ * so the root layout can bind the draft on the same request (a layout gets no
31
35
  * `searchParams`, and on a draft link the cookie is only on the response).
32
- * A copy that arrives from outside is always removed.
36
+ * A copy that arrives from outside is always removed, and so are any
37
+ * `x-deco-visitor` and `x-deco-assignments` (`visitorRequestHeaders` and
38
+ * `assignmentsRequestHeaders` set the proxy's own).
33
39
  */
34
40
  export function draftRequestHeaders(request, decision) {
35
41
  const headers = new Headers(request.headers);
36
- if (decision.pointer)
37
- headers.set(DRAFT_HEADER, decision.pointer);
38
- else
39
- headers.delete(DRAFT_HEADER);
42
+ for (const name of OWN_HEADERS)
43
+ headers.delete(name);
44
+ if (decision.version)
45
+ headers.set(VERSION_HEADER, decision.version.version);
46
+ return headers;
47
+ }
48
+ /** Whether the visitor asked over https (`x-forwarded-proto` first, as a host in front reports it). */
49
+ function isHttps(request) {
50
+ const forwarded = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
51
+ if (forwarded)
52
+ return forwarded === "https";
53
+ return new URL(request.url).protocol === "https:";
54
+ }
55
+ /**
56
+ * The visitor of a request (`readVisitor`: the `deco_segment` cookie, or a
57
+ * new visitor when it had no id) and the cookie to send back, only on a first
58
+ * visit and never from a preview (`draft` holds a version): Studio is not a
59
+ * visitor. Reads no content.
60
+ */
61
+ export function decideVisitor(request, draft) {
62
+ // `readVisitor` makes the cookie `Secure` unless the request came over plain http.
63
+ const { visitor, cookie } = readVisitor({
64
+ url: isHttps(request) ? "https://localhost/" : "http://localhost/",
65
+ headers: request.headers,
66
+ });
67
+ return { visitor, setCookie: draft.version === null ? cookie : null };
68
+ }
69
+ /**
70
+ * The forwarded headers with the visitor on `x-deco-visitor`. Pass it the
71
+ * headers `draftRequestHeaders` gave, which already removed a copy from
72
+ * outside.
73
+ */
74
+ export function visitorRequestHeaders(headers, decision) {
75
+ const out = new Headers(headers);
76
+ out.set(VISITOR_HEADER, serializeVisitor(decision.visitor));
77
+ return out;
78
+ }
79
+ /** The `deco_segment` cookie on the response, on a first visit. */
80
+ export function applyVisitor(response, decision) {
81
+ if (decision.setCookie)
82
+ response.headers.append("set-cookie", decision.setCookie);
83
+ return response;
84
+ }
85
+ /**
86
+ * 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
89
+ * `experiments` and `campaigns` blocks. Missing blocks, or content that
90
+ * doesn't load, give none, never an error.
91
+ */
92
+ export function decideAssignments(cms, draft, visitor) {
93
+ return readAssignmentsAndScope(cms, { visitor: visitor.visitor, ...draft.version }).then((read) => read.assignments);
94
+ }
95
+ /**
96
+ * The forwarded headers with the decided tests and campaigns on
97
+ * `x-deco-assignments` (`assignmentsSegment`), for `bindDraft` and
98
+ * `contentClient`. Pass it the headers `draftRequestHeaders` gave.
99
+ */
100
+ export function assignmentsRequestHeaders(headers, assignments) {
101
+ const out = new Headers(headers);
102
+ out.set(ASSIGNMENTS_HEADER, assignmentsSegment(assignments));
103
+ return out;
104
+ }
105
+ /** The request headers to forward: the version's and, when given, the visitor's and the assignments. */
106
+ function forwardedHeaders(request, draft, visitor, assignments) {
107
+ let headers = draftRequestHeaders(request, draft);
108
+ if (visitor !== undefined)
109
+ headers = visitorRequestHeaders(headers, visitor);
110
+ if (assignments !== undefined)
111
+ headers = assignmentsRequestHeaders(headers, assignments);
40
112
  return headers;
41
113
  }
42
114
  /** Whether `pathname` is on the draft route: a site answers a direct hit there with its 404. */
@@ -45,47 +117,107 @@ export function isDraftRoutePath(pathname) {
45
117
  }
46
118
  /**
47
119
  * The rewrite of a drafted request onto the draft route, keeping its path and
48
- * forwarding the pointer; `null` when the request isn't drafted or is already
49
- * on the draft route.
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.
50
123
  */
51
- export function rewriteToDraftRoute(request, decision) {
52
- if (!decision.pointer)
124
+ export function rewriteToDraftRoute(request, decision, visitor, assignments) {
125
+ if (!decision.version)
53
126
  return null;
54
127
  const url = request.nextUrl.clone();
55
128
  if (isDraftRoutePath(url.pathname))
56
129
  return null;
57
130
  url.pathname = `${DRAFT_ROUTE_PREFIX}${url.pathname}`;
58
131
  return NextResponse.rewrite(url, {
59
- request: { headers: draftRequestHeaders(request, decision) },
132
+ request: { headers: forwardedHeaders(request, decision, visitor, assignments) },
133
+ });
134
+ }
135
+ /** An assignments-route response's `Cache-Control` when its content has tests or campaigns. */
136
+ const ASSIGNED_CACHE_CONTROL = "private, no-cache";
137
+ /**
138
+ * The rewrite of a published request onto the assignments route,
139
+ * `/_assignments/<assignments>/<path>`: each test on or off for the visitor
140
+ * and each campaign running or not now (`readAssignments` on the current
141
+ * release), as one path segment, so Next's own path-keyed cache (ISR, the
142
+ * full route cache) keeps one entry per set of assignments. Visitors on the
143
+ * same side of every test share a path; the visitor id is never in it, and
144
+ * the page reads neither the visitor nor the version (the proxy's headers are
145
+ * removed from the request it forwards), so Next can cache it. The page
146
+ * builds its client with `assignedClient` from `@decocms/nextjs/server`,
147
+ * `cms.client({ experiments, campaigns })` from the path: the proxy decides,
148
+ * the page renders that. A cached page (ISR, `revalidate = 600`) is about 10
149
+ * minutes stale (the first request after that still gets it while Next
150
+ * renders it again), a banner switching on late, which is accepted.
151
+ *
152
+ * `null` for a drafted request (it renders on the draft route, never cached)
153
+ * and for a path already on the draft or assignments route. When the content
154
+ * saves tests or campaigns, the response is marked `Cache-Control: private,
155
+ * no-cache`: Next keeps one cached page per set of assignments, but the URL
156
+ * the visitor asked for answers by their cookie and the moment, so a CDN in
157
+ * front must not share it. Call it only for the paths the assignments route
158
+ * renders (the CMS's pages), and answer a direct hit on the route with a 404
159
+ * (`isAssignmentsRoutePath`).
160
+ */
161
+ export async function rewriteToAssignmentsRoute(cms, request, draft, visitor) {
162
+ if (draft.version)
163
+ return null;
164
+ const url = request.nextUrl.clone();
165
+ if (isDraftRoutePath(url.pathname) || isAssignmentsRoutePath(url.pathname))
166
+ return null;
167
+ const { assignments, varies } = await readAssignmentsAndScope(cms, { visitor: visitor.visitor });
168
+ url.pathname = assignmentsRoutePath(url.pathname, assignments);
169
+ const response = NextResponse.rewrite(url, {
170
+ request: { headers: draftRequestHeaders(request, INERT) },
60
171
  });
172
+ // The URL the visitor asked for answers differently by their cookie and the moment: a
173
+ // shared cache in front (a CDN) must not keep Next's ISR `s-maxage` under it.
174
+ if (varies)
175
+ response.headers.set("Cache-Control", ASSIGNED_CACHE_CONTROL);
176
+ return response;
61
177
  }
62
178
  /**
63
179
  * The cookie and the response headers of a draft: `Set-Cookie`, and for a
64
180
  * drafted response `Cache-Control: no-store, private`, `Vary: Cookie` and
65
181
  * `X-Robots-Tag: noindex, nofollow` (`draftResponseHeaders`). With the
66
- * pointer in a cookie a draft shares its URL with the published page, so a
182
+ * version in a cookie a draft shares its URL with the published page, so a
67
183
  * cache keyed on the URL alone would serve it to visitors.
68
184
  */
69
185
  export function applyDraft(response, decision) {
70
186
  if (decision.setCookie)
71
187
  response.headers.append("set-cookie", decision.setCookie);
72
- for (const [name, value] of Object.entries(draftResponseHeaders(decision.pointer))) {
188
+ for (const [name, value] of Object.entries(draftResponseHeaders(decision.version?.version))) {
73
189
  response.headers.set(name, value);
74
190
  }
75
191
  return response;
76
192
  }
77
193
  /**
78
- * The whole draft flow for a site with no proxy of its own:
79
- * `export const proxy = draftProxy(proxyCms, { site: "my-store" })`.
194
+ * The whole flow for a site with no proxy of its own:
195
+ * `export const proxy = draftProxy(proxyCms)`. Every
196
+ * request it forwards carries the version, the visitor and the assignments
197
+ * it decided.
80
198
  */
81
199
  export function draftProxy(cms, options = {}) {
82
- const { rewrite = () => true, ...policy } = options;
200
+ const { rewrite = () => true, assignments } = options;
83
201
  return async (request) => {
84
- const decision = await decideDraft(cms, request, policy);
85
- const rewritten = rewrite(request) ? rewriteToDraftRoute(request, decision) : null;
86
- const response = rewritten ??
87
- NextResponse.next({ request: { headers: draftRequestHeaders(request, decision) } });
88
- return applyDraft(response, decision);
202
+ if (assignments && isAssignmentsRoutePath(request.nextUrl.pathname)) {
203
+ return new NextResponse(null, { status: 404 });
204
+ }
205
+ const decision = await decideDraft(cms, request);
206
+ const visitor = decideVisitor(request, decision);
207
+ const onAssignmentsRoute = decision.version === null && assignments?.(request) === true;
208
+ const rewritten = onAssignmentsRoute
209
+ ? await rewriteToAssignmentsRoute(cms, request, decision, visitor)
210
+ : null;
211
+ let response = rewritten;
212
+ if (response === null) {
213
+ const decided = await decideAssignments(cms, decision, visitor);
214
+ response =
215
+ (rewrite(request) ? rewriteToDraftRoute(request, decision, visitor, decided) : null) ??
216
+ NextResponse.next({
217
+ request: { headers: forwardedHeaders(request, decision, visitor, decided) },
218
+ });
219
+ }
220
+ return applyVisitor(applyDraft(response, decision), visitor);
89
221
  };
90
222
  }
91
223
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"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"}
@@ -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">;
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;AAYlE,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,CAAC,CAAC;AAOvD;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ9F;AAmDD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,MAAM,CAS9D;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"}