@decocms/nextjs 8.0.0 → 8.1.0-next.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 +4 -6
- package/package.json +3 -3
- package/src/DraftPreviewBadge.tsx +12 -248
- package/src/__conformance__/observability.test.ts +66 -0
- package/src/createDecoPage.tsx +1 -1
- package/src/draft.ts +11 -5
- package/src/draftConstants.ts +15 -0
- package/src/draftMiddleware.test.ts +38 -6
- package/src/draftMiddleware.ts +31 -4
- package/src/draftShell.test.ts +76 -0
- package/src/index.ts +2 -0
- package/src/routeHandlers.ts +1 -1
- package/src/DraftPreviewBadge.render.test.tsx +0 -16
- package/src/DraftPreviewBadge.test.ts +0 -37
- package/src/decoMark.ts +0 -11
package/README.md
CHANGED
|
@@ -359,9 +359,7 @@ implementation file) can opt into these, each read as a literal
|
|
|
359
359
|
|
|
360
360
|
## Verifying your setup
|
|
361
361
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
the build also guards the server/client boundary described above. Use it as
|
|
367
|
-
a working reference if any of the above doesn't compose the way you expect.
|
|
362
|
+
No example app in this monorepo exercises these surfaces any more:
|
|
363
|
+
`examples/nextjs-smoke` now follows the next-major Next.js guide, which uses
|
|
364
|
+
`@decocms/blocks` alone. The surfaces above are covered by this package's unit
|
|
365
|
+
tests.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/nextjs",
|
|
3
|
-
"version": "8.0.0",
|
|
3
|
+
"version": "8.1.0-next.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"lint:unused": "knip"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@decocms/blocks": "8.0.0",
|
|
33
|
-
"@decocms/blocks-admin": "8.0.0"
|
|
32
|
+
"@decocms/blocks": "8.1.0-next.0",
|
|
33
|
+
"@decocms/blocks-admin": "8.1.0-next.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"next": ">=15.0.0",
|
|
@@ -1,252 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* whenever a draft is bound it floats over the page, and clicking it offers the
|
|
11
|
-
* two things a reviewer actually needs — leave preview, or hand the exact draft
|
|
12
|
-
* version to someone else.
|
|
13
|
-
*
|
|
14
|
-
* Deliberately self-contained: `"use client"`, inline styles (no dependency on
|
|
15
|
-
* the host site's CSS/Tailwind), and a very high z-index, so it renders the
|
|
16
|
-
* same on any consumer site. Only mounted when a draft is active
|
|
17
|
-
* (`DraftPreviewIndicator` gates it), so it never costs ordinary traffic.
|
|
4
|
+
* The preview-mode badge now lives in the runtime core so both bindings share
|
|
5
|
+
* one implementation — see `@decocms/blocks/preview`. This module is a thin
|
|
6
|
+
* re-export kept for back-compat: sites and `DraftPreviewIndicator` import the
|
|
7
|
+
* badge (and the pure `buildShareUrl`/`buildExitUrl` helpers) from here.
|
|
8
|
+
* `isFramed` stays internal to `@decocms/blocks/preview` — it was never part of
|
|
9
|
+
* this package's public surface.
|
|
18
10
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
pointer: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* URL that re-enters this exact draft version — what "Share Preview Link"
|
|
30
|
-
* copies. Pins the version (the token carries the content ETag), so the
|
|
31
|
-
* recipient sees the same working-tree state, not merely "some draft".
|
|
32
|
-
*
|
|
33
|
-
* Pure and exported for tests: builds off a full href so it needs no DOM.
|
|
34
|
-
*/
|
|
35
|
-
export function buildShareUrl(href: string, pointer: string): string {
|
|
36
|
-
const url = new URL(href);
|
|
37
|
-
url.searchParams.set(DRAFT_PARAM, pointer);
|
|
38
|
-
return url.toString();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* URL that leaves preview mode — `?__draft=off`, the exit sentinel the
|
|
43
|
-
* middleware/page pair understands (it clears the cookie and renders
|
|
44
|
-
* published). Pure and exported for tests.
|
|
45
|
-
*/
|
|
46
|
-
export function buildExitUrl(href: string): string {
|
|
47
|
-
const url = new URL(href);
|
|
48
|
-
url.searchParams.set(DRAFT_PARAM, "off");
|
|
49
|
-
return url.toString();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const DECO_GREEN = "#0b3d1e";
|
|
53
|
-
/** Toast background — the deco lime. Text sits on it in dark green for contrast. */
|
|
54
|
-
const DECO_LIME = "#d0ec1a";
|
|
55
|
-
|
|
56
|
-
/** Leaving preview: an arrow back to the published site. */
|
|
57
|
-
function ExitIcon() {
|
|
58
|
-
return (
|
|
59
|
-
<svg
|
|
60
|
-
width={14}
|
|
61
|
-
height={14}
|
|
62
|
-
viewBox="0 0 24 24"
|
|
63
|
-
fill="none"
|
|
64
|
-
stroke="currentColor"
|
|
65
|
-
strokeWidth={2}
|
|
66
|
-
strokeLinecap="round"
|
|
67
|
-
strokeLinejoin="round"
|
|
68
|
-
aria-hidden="true"
|
|
69
|
-
>
|
|
70
|
-
<path d="M19 12H5" />
|
|
71
|
-
<path d="M12 19l-7-7 7-7" />
|
|
72
|
-
</svg>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Sharing the draft: the classic three-node share glyph. */
|
|
77
|
-
function ShareIcon() {
|
|
78
|
-
return (
|
|
79
|
-
<svg
|
|
80
|
-
width={14}
|
|
81
|
-
height={14}
|
|
82
|
-
viewBox="0 0 24 24"
|
|
83
|
-
fill="none"
|
|
84
|
-
stroke="currentColor"
|
|
85
|
-
strokeWidth={2}
|
|
86
|
-
strokeLinecap="round"
|
|
87
|
-
strokeLinejoin="round"
|
|
88
|
-
aria-hidden="true"
|
|
89
|
-
>
|
|
90
|
-
<circle cx={18} cy={5} r={3} />
|
|
91
|
-
<circle cx={6} cy={12} r={3} />
|
|
92
|
-
<circle cx={18} cy={19} r={3} />
|
|
93
|
-
<path d="M8.59 13.51l6.83 3.98" />
|
|
94
|
-
<path d="M15.41 6.51l-6.82 3.98" />
|
|
95
|
-
</svg>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function DraftPreviewBadge({ pointer }: DraftPreviewBadgeProps) {
|
|
100
|
-
const [open, setOpen] = useState(false);
|
|
101
|
-
const [copied, setCopied] = useState(false);
|
|
102
|
-
const rootRef = useRef<HTMLDivElement>(null);
|
|
103
|
-
|
|
104
|
-
// Dismiss on outside click or Escape — standard popover behaviour, and it
|
|
105
|
-
// keeps the badge from trapping focus on a reviewer's own page.
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
if (!open) return;
|
|
108
|
-
const onPointerDown = (e: MouseEvent) => {
|
|
109
|
-
if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
|
|
110
|
-
};
|
|
111
|
-
const onKeyDown = (e: KeyboardEvent) => {
|
|
112
|
-
if (e.key === "Escape") setOpen(false);
|
|
113
|
-
};
|
|
114
|
-
document.addEventListener("mousedown", onPointerDown);
|
|
115
|
-
document.addEventListener("keydown", onKeyDown);
|
|
116
|
-
return () => {
|
|
117
|
-
document.removeEventListener("mousedown", onPointerDown);
|
|
118
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
119
|
-
};
|
|
120
|
-
}, [open]);
|
|
121
|
-
|
|
122
|
-
const exitPreview = useCallback(() => {
|
|
123
|
-
window.location.href = buildExitUrl(window.location.href);
|
|
124
|
-
}, []);
|
|
125
|
-
|
|
126
|
-
const sharePreviewLink = useCallback(async () => {
|
|
127
|
-
const link = buildShareUrl(window.location.href, pointer);
|
|
128
|
-
try {
|
|
129
|
-
await navigator.clipboard.writeText(link);
|
|
130
|
-
setCopied(true);
|
|
131
|
-
window.setTimeout(() => setCopied(false), 2000);
|
|
132
|
-
} catch {
|
|
133
|
-
// Clipboard blocked (insecure context / permissions): fall back to a
|
|
134
|
-
// prompt so the reviewer can still copy the link by hand rather than
|
|
135
|
-
// silently doing nothing.
|
|
136
|
-
window.prompt("Copy preview link:", link);
|
|
137
|
-
}
|
|
138
|
-
}, [pointer]);
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
<div
|
|
142
|
-
ref={rootRef}
|
|
143
|
-
data-deco-preview-badge=""
|
|
144
|
-
style={{
|
|
145
|
-
position: "fixed",
|
|
146
|
-
bottom: 16,
|
|
147
|
-
left: 16,
|
|
148
|
-
zIndex: 2147483647,
|
|
149
|
-
fontFamily:
|
|
150
|
-
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
|
151
|
-
fontSize: 13,
|
|
152
|
-
lineHeight: 1.4,
|
|
153
|
-
}}
|
|
154
|
-
>
|
|
155
|
-
{open ? (
|
|
156
|
-
<div
|
|
157
|
-
role="menu"
|
|
158
|
-
style={{
|
|
159
|
-
position: "absolute",
|
|
160
|
-
bottom: "calc(100% + 8px)",
|
|
161
|
-
left: 0,
|
|
162
|
-
minWidth: 200,
|
|
163
|
-
background: "#fff",
|
|
164
|
-
color: "#1a1a1a",
|
|
165
|
-
borderRadius: 12,
|
|
166
|
-
boxShadow: "0 8px 30px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.05)",
|
|
167
|
-
overflow: "hidden",
|
|
168
|
-
}}
|
|
169
|
-
>
|
|
170
|
-
<MenuItem onClick={exitPreview} label="Exit preview" icon={<ExitIcon />} />
|
|
171
|
-
<div style={{ height: 1, background: "rgba(0,0,0,0.07)" }} />
|
|
172
|
-
<MenuItem
|
|
173
|
-
onClick={sharePreviewLink}
|
|
174
|
-
label={copied ? "Copied!" : "Share preview"}
|
|
175
|
-
icon={<ShareIcon />}
|
|
176
|
-
/>
|
|
177
|
-
</div>
|
|
178
|
-
) : null}
|
|
179
|
-
|
|
180
|
-
<button
|
|
181
|
-
type="button"
|
|
182
|
-
aria-expanded={open}
|
|
183
|
-
aria-label="Preview mode"
|
|
184
|
-
onClick={() => setOpen((v) => !v)}
|
|
185
|
-
style={{
|
|
186
|
-
display: "inline-flex",
|
|
187
|
-
alignItems: "center",
|
|
188
|
-
gap: 8,
|
|
189
|
-
padding: "7px 12px 7px 8px",
|
|
190
|
-
background: DECO_LIME,
|
|
191
|
-
color: DECO_GREEN,
|
|
192
|
-
border: "none",
|
|
193
|
-
borderRadius: 9999,
|
|
194
|
-
cursor: "pointer",
|
|
195
|
-
fontSize: 13,
|
|
196
|
-
fontWeight: 600,
|
|
197
|
-
boxShadow: "0 4px 14px rgba(0,0,0,0.25)",
|
|
198
|
-
}}
|
|
199
|
-
>
|
|
200
|
-
<img
|
|
201
|
-
src={DECO_MARK_DATA_URI}
|
|
202
|
-
alt=""
|
|
203
|
-
width={18}
|
|
204
|
-
height={18}
|
|
205
|
-
style={{ display: "block", borderRadius: 4 }}
|
|
206
|
-
/>
|
|
207
|
-
<span>Preview mode</span>
|
|
208
|
-
</button>
|
|
209
|
-
</div>
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function MenuItem({
|
|
214
|
-
onClick,
|
|
215
|
-
label,
|
|
216
|
-
icon,
|
|
217
|
-
}: {
|
|
218
|
-
onClick: () => void;
|
|
219
|
-
label: string;
|
|
220
|
-
icon: ReactNode;
|
|
221
|
-
}) {
|
|
222
|
-
return (
|
|
223
|
-
<button
|
|
224
|
-
type="button"
|
|
225
|
-
role="menuitem"
|
|
226
|
-
onClick={onClick}
|
|
227
|
-
style={{
|
|
228
|
-
display: "flex",
|
|
229
|
-
alignItems: "center",
|
|
230
|
-
gap: 10,
|
|
231
|
-
width: "100%",
|
|
232
|
-
textAlign: "left",
|
|
233
|
-
padding: "10px 14px",
|
|
234
|
-
background: "transparent",
|
|
235
|
-
border: "none",
|
|
236
|
-
color: "inherit",
|
|
237
|
-
cursor: "pointer",
|
|
238
|
-
fontSize: 13,
|
|
239
|
-
whiteSpace: "nowrap",
|
|
240
|
-
}}
|
|
241
|
-
onMouseEnter={(e) => {
|
|
242
|
-
e.currentTarget.style.background = "rgba(0,0,0,0.05)";
|
|
243
|
-
}}
|
|
244
|
-
onMouseLeave={(e) => {
|
|
245
|
-
e.currentTarget.style.background = "transparent";
|
|
246
|
-
}}
|
|
247
|
-
>
|
|
248
|
-
<span style={{ display: "inline-flex", flexShrink: 0, color: DECO_GREEN }}>{icon}</span>
|
|
249
|
-
<span>{label}</span>
|
|
250
|
-
</button>
|
|
251
|
-
);
|
|
252
|
-
}
|
|
11
|
+
export {
|
|
12
|
+
buildExitUrl,
|
|
13
|
+
buildShareUrl,
|
|
14
|
+
DraftPreviewBadge,
|
|
15
|
+
type DraftPreviewBadgeProps,
|
|
16
|
+
} from "@decocms/blocks/preview";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
/**
|
|
3
|
+
* Conformance: the Next.js half of caching.mdx ("Upstream data": requests go
|
|
4
|
+
* through Next's data cache, no cache of the binding's own) and of
|
|
5
|
+
* telemetry.mdx ("What's sent": the bindings add no measurements of their own;
|
|
6
|
+
* inbound requests come from Next's own OpenTelemetry setup).
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { createInstrumentedFetch } from "@decocms/blocks/fetch";
|
|
12
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
|
|
14
|
+
const SRC = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
|
|
16
|
+
function sources(): { file: string; text: string }[] {
|
|
17
|
+
const out: { file: string; text: string }[] = [];
|
|
18
|
+
const walk = (dir: string) => {
|
|
19
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
20
|
+
const full = path.join(dir, entry.name);
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
if (entry.name !== "__conformance__" && entry.name !== "__tests__") walk(full);
|
|
23
|
+
} else if (/\.(ts|tsx|cjs|js)$/.test(entry.name) && !/\.test\./.test(entry.name)) {
|
|
24
|
+
out.push({ file: path.relative(SRC, full), text: fs.readFileSync(full, "utf8") });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
walk(SRC);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.unstubAllGlobals();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("upstream data on Next.js (caching.mdx)", () => {
|
|
37
|
+
it("cache-04: the instrumented fetch goes through the fetch Next patches, with Next's cache options intact", async () => {
|
|
38
|
+
const instrumented = createInstrumentedFetch({ provider: "acme-search" });
|
|
39
|
+
// Next replaces globalThis.fetch after modules load; the instrumented fetch must use it.
|
|
40
|
+
const nextFetch = vi.fn(async () => new Response("{}"));
|
|
41
|
+
vi.stubGlobal("fetch", nextFetch);
|
|
42
|
+
await instrumented("https://search.example/q", {
|
|
43
|
+
operation: "search",
|
|
44
|
+
next: { revalidate: 60, tags: ["search"] },
|
|
45
|
+
} as RequestInit & { operation: string });
|
|
46
|
+
expect(nextFetch).toHaveBeenCalledTimes(1);
|
|
47
|
+
const init = (nextFetch.mock.calls[0] as unknown as [unknown, Record<string, unknown>])[1];
|
|
48
|
+
expect(init.next).toEqual({ revalidate: 60, tags: ["search"] });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("cache-04: @decocms/nextjs has no upstream cache of its own", () => {
|
|
52
|
+
const own = sources().filter(({ text }) =>
|
|
53
|
+
/caches\.default|caches\.open|createFetchCache|fetchWithCache/.test(text),
|
|
54
|
+
);
|
|
55
|
+
expect(own.map((s) => s.file)).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("what the binding adds (telemetry.mdx › What's sent)", () => {
|
|
60
|
+
it("tel-09: @decocms/nextjs adds no measurements of its own (no inbound or page-cache metrics)", () => {
|
|
61
|
+
const reporters = sources().filter(({ text }) =>
|
|
62
|
+
/http\.server\.request\.duration|decocms\.blocks\.telemetry/.test(text),
|
|
63
|
+
);
|
|
64
|
+
expect(reporters.map((s) => s.file)).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
});
|
package/src/createDecoPage.tsx
CHANGED
|
@@ -57,7 +57,7 @@ function buildSeo(page: DecoPageResult): PageSeo {
|
|
|
57
57
|
*
|
|
58
58
|
* `resolveForPath` is wrapped in React's `cache()` so `generateMetadata` and
|
|
59
59
|
* the page body share one `resolveDecoPage` call per request instead of
|
|
60
|
-
* resolving twice — the same pattern
|
|
60
|
+
* resolving twice — the same pattern a production Next.js site's own
|
|
61
61
|
* `resolveCmsPageByPath` already used against the old /next tier. This relies
|
|
62
62
|
* on Next's RSC renderer establishing a per-request cache boundary that
|
|
63
63
|
* `cache()` memoizes against: calling the returned functions directly outside
|
package/src/draft.ts
CHANGED
|
@@ -29,11 +29,11 @@ import {
|
|
|
29
29
|
} from "@decocms/blocks/cms";
|
|
30
30
|
import { cookies, headers } from "next/headers";
|
|
31
31
|
import { cache } from "react";
|
|
32
|
-
import { DRAFT_COOKIE, DRAFT_PARAM } from "./draftConstants";
|
|
32
|
+
import { DRAFT_COOKIE, DRAFT_HEADER, DRAFT_PARAM } from "./draftConstants";
|
|
33
33
|
|
|
34
34
|
// Re-exported for back-compat: these used to live here, but the client badge
|
|
35
35
|
// needs DRAFT_PARAM and cannot import this module (it pulls in `next/headers`).
|
|
36
|
-
export { DRAFT_COOKIE, DRAFT_PARAM } from "./draftConstants";
|
|
36
|
+
export { DRAFT_COOKIE, DRAFT_HEADER, DRAFT_PARAM } from "./draftConstants";
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Request-scoped slot.
|
|
@@ -124,15 +124,21 @@ export function selectDraftPointer(
|
|
|
124
124
|
export async function ensureDraft(searchParams?: DraftSearchParams): Promise<boolean> {
|
|
125
125
|
registerDraftOverride();
|
|
126
126
|
|
|
127
|
-
const cookieStore = await cookies();
|
|
128
|
-
|
|
127
|
+
const [cookieStore, requestHeaders] = await Promise.all([cookies(), headers()]);
|
|
128
|
+
|
|
129
|
+
// Pointer precedence: the page's own `?__draft=` param wins; then the
|
|
130
|
+
// middleware-forwarded header (the only source a LAYOUT can see on the entry
|
|
131
|
+
// request — see DRAFT_HEADER); then the navigation cookie. This is what lets
|
|
132
|
+
// `ensureDraft()` bind from a layout so shell-resolved Header/Footer reflect
|
|
133
|
+
// the draft, not just page sections.
|
|
134
|
+
const carried = requestHeaders.get(DRAFT_HEADER) ?? cookieStore.get(DRAFT_COOKIE)?.value ?? null;
|
|
135
|
+
const pointer = selectDraftPointer(searchParams, carried);
|
|
129
136
|
if (!pointer) return false;
|
|
130
137
|
|
|
131
138
|
// Host gate, checked only once a pointer exists (headers() is a dynamic
|
|
132
139
|
// API): the same build may serve the preview domain and the production
|
|
133
140
|
// domain, and only hosts named in DECO_ALLOWED_PREVIEW_HOSTS may render
|
|
134
141
|
// drafts — production stays published no matter what the URL carries.
|
|
135
|
-
const requestHeaders = await headers();
|
|
136
142
|
const host = requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host");
|
|
137
143
|
if (!isDraftHostAllowed(host)) return false;
|
|
138
144
|
|
package/src/draftConstants.ts
CHANGED
|
@@ -13,3 +13,18 @@ export const DRAFT_PARAM = "__draft";
|
|
|
13
13
|
|
|
14
14
|
/** Cookie that carries the pointer across in-preview navigation. */
|
|
15
15
|
export const DRAFT_COOKIE = "__deco_draft";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Request header the middleware forwards the active pointer on, so the whole
|
|
19
|
+
* RSC tree — including the root layout / app shell — can bind the draft on the
|
|
20
|
+
* SAME request. The page reads `?__draft=` from its own `searchParams`, but a
|
|
21
|
+
* layout never receives `searchParams` and, on the entry request, the cookie
|
|
22
|
+
* has only just been set on the *response* (not yet readable). Forwarding the
|
|
23
|
+
* pointer as a request header closes both gaps so `ensureDraft()` works from a
|
|
24
|
+
* layout, letting shell-resolved Header/Footer reflect the draft too.
|
|
25
|
+
*
|
|
26
|
+
* Not a new trust surface: the pointer was never a secret (the draft id is the
|
|
27
|
+
* capability, host-scoping bounds blast radius — see `draftSource`), so a
|
|
28
|
+
* forwarded/forged header is equivalent to typing the query param.
|
|
29
|
+
*/
|
|
30
|
+
export const DRAFT_HEADER = "x-deco-draft";
|
|
@@ -2,7 +2,12 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { DRAFT_COOKIE } from "./draft";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
applyDraft,
|
|
7
|
+
draftRequestHeaders,
|
|
8
|
+
prepareDraft,
|
|
9
|
+
rewriteToDraftRoute,
|
|
10
|
+
} from "./draftMiddleware";
|
|
6
11
|
|
|
7
12
|
beforeEach(() => {
|
|
8
13
|
// The middleware is host-gated; these tests run as the allowed host.
|
|
@@ -42,15 +47,18 @@ describe("prepareDraft", () => {
|
|
|
42
47
|
|
|
43
48
|
it("is inert on a host outside the allowlist — the production domain", () => {
|
|
44
49
|
// Same build, different Host: no cookie, no rewrite, nothing touched.
|
|
45
|
-
const req = request("https://
|
|
46
|
-
process.env.DECO_ALLOWED_PREVIEW_HOSTS = "
|
|
50
|
+
const req = request("https://acme.com.br/p?__draft=abc@v1");
|
|
51
|
+
process.env.DECO_ALLOWED_PREVIEW_HOSTS = "acme.vtex.app";
|
|
47
52
|
expect(prepareDraft(req)).toEqual({ pointer: null, setCookie: null, clearCookie: false });
|
|
48
53
|
expect(rewriteToDraftRoute(req, prepareDraft(req))).toBeNull();
|
|
49
54
|
});
|
|
50
55
|
|
|
51
|
-
it("
|
|
52
|
-
//
|
|
53
|
-
//
|
|
56
|
+
it("does not read a client-supplied draft header — the decision is param/cookie only", () => {
|
|
57
|
+
// The forwarded `x-deco-draft` header (read by `ensureDraft` in the layout)
|
|
58
|
+
// is an OUTPUT of the middleware decision, never an input to it: prepareDraft
|
|
59
|
+
// still decides purely from the param + cookie. A client that forges the
|
|
60
|
+
// header directly can't manufacture a decision here (and `draftRequestHeaders`
|
|
61
|
+
// strips it when not previewing).
|
|
54
62
|
const req = new NextRequest(new URL("https://site.example/p"), {
|
|
55
63
|
headers: { "x-deco-draft": "attacker.localhost@v1" },
|
|
56
64
|
});
|
|
@@ -58,6 +66,30 @@ describe("prepareDraft", () => {
|
|
|
58
66
|
});
|
|
59
67
|
});
|
|
60
68
|
|
|
69
|
+
describe("draftRequestHeaders", () => {
|
|
70
|
+
it("forwards the active pointer on x-deco-draft so a layout can bind it", () => {
|
|
71
|
+
const req = request("https://site.example/p?__draft=abc.localhost@v1");
|
|
72
|
+
const headers = draftRequestHeaders(req, prepareDraft(req));
|
|
73
|
+
expect(headers.get("x-deco-draft")).toBe("abc.localhost@v1");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("forwards the cookie-carried pointer on plain navigation", () => {
|
|
77
|
+
const req = request("https://site.example/p", "abc.localhost@v1");
|
|
78
|
+
const headers = draftRequestHeaders(req, prepareDraft(req));
|
|
79
|
+
expect(headers.get("x-deco-draft")).toBe("abc.localhost@v1");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("strips a client-forged header when the request is not previewing", () => {
|
|
83
|
+
// Direct-to-origin request carrying a forged header but no real pointer:
|
|
84
|
+
// the forwarded headers must not carry it downstream.
|
|
85
|
+
const req = new NextRequest(new URL("https://site.example/p"), {
|
|
86
|
+
headers: { "x-deco-draft": "attacker.localhost@v1" },
|
|
87
|
+
});
|
|
88
|
+
const headers = draftRequestHeaders(req, prepareDraft(req));
|
|
89
|
+
expect(headers.get("x-deco-draft")).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
61
93
|
describe("applyDraft", () => {
|
|
62
94
|
it("sets a partitioned cross-site cookie when entering draft mode", () => {
|
|
63
95
|
const decision = prepareDraft(request("https://site.example/p?__draft=abc.localhost@v1"));
|
package/src/draftMiddleware.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { type NextRequest, NextResponse } from "next/server";
|
|
|
25
25
|
import {
|
|
26
26
|
DRAFT_COOKIE,
|
|
27
27
|
DRAFT_COOKIE_OPTIONS,
|
|
28
|
+
DRAFT_HEADER,
|
|
28
29
|
type DraftMiddlewareDecision,
|
|
29
30
|
decideDraft,
|
|
30
31
|
} from "./draft";
|
|
@@ -127,10 +128,36 @@ export function applyDraft(
|
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
131
|
+
* Request headers to forward downstream, carrying the active pointer on
|
|
132
|
+
* `x-deco-draft` (see {@link DRAFT_HEADER}) so the RSC tree — including the app
|
|
133
|
+
* shell — can bind the draft via `ensureDraft()` and reflect it in
|
|
134
|
+
* shell-resolved Header/Footer, not only page sections.
|
|
135
|
+
*
|
|
136
|
+
* Pass to `NextResponse.next({ request: { headers } })`. Strips any
|
|
137
|
+
* client-supplied `x-deco-draft` when this request is not previewing, so the
|
|
138
|
+
* header is only ever present on a legitimately-gated preview request.
|
|
139
|
+
*/
|
|
140
|
+
export function draftRequestHeaders(
|
|
141
|
+
request: NextRequest,
|
|
142
|
+
decision: DraftMiddlewareDecision,
|
|
143
|
+
): Headers {
|
|
144
|
+
const headers = new Headers(request.headers);
|
|
145
|
+
if (decision.pointer) headers.set(DRAFT_HEADER, decision.pointer);
|
|
146
|
+
else headers.delete(DRAFT_HEADER);
|
|
147
|
+
return headers;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Convenience for sites with no middleware of their own: prepare, forward the
|
|
152
|
+
* pointer header, continue, apply. Sites that already have middleware should
|
|
153
|
+
* call the halves directly (`prepareDraft` → `draftRequestHeaders` on the
|
|
154
|
+
* `NextResponse.next({ request: { headers } })` → `applyDraft`) so their own
|
|
155
|
+
* logic sits in between.
|
|
133
156
|
*/
|
|
134
157
|
export function draftMiddleware(request: NextRequest): NextResponse {
|
|
135
|
-
|
|
158
|
+
const decision = prepareDraft(request);
|
|
159
|
+
const response = NextResponse.next({
|
|
160
|
+
request: { headers: draftRequestHeaders(request, decision) },
|
|
161
|
+
});
|
|
162
|
+
return applyDraft(response, decision);
|
|
136
163
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ensureDraft()` must bind from the middleware-forwarded `x-deco-draft` header
|
|
3
|
+
* — the only pointer source a LAYOUT (app shell) can see: it receives no
|
|
4
|
+
* `searchParams`, and on the entry request the cookie has only just been set on
|
|
5
|
+
* the response. This is what lets shell-resolved Header/Footer reflect a draft,
|
|
6
|
+
* not just page sections.
|
|
7
|
+
*/
|
|
8
|
+
import { clearDraftCache } from "@decocms/blocks/cms";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
|
|
11
|
+
// Mutable header/cookie state the mock reads, so each test drives its own.
|
|
12
|
+
let headerState = new Headers();
|
|
13
|
+
let cookieState: Record<string, string> = {};
|
|
14
|
+
|
|
15
|
+
vi.mock("next/headers", () => ({
|
|
16
|
+
headers: async () => headerState,
|
|
17
|
+
cookies: async () => ({
|
|
18
|
+
get: (name: string) =>
|
|
19
|
+
name in cookieState ? { name, value: cookieState[name] } : undefined,
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const { ensureDraft, DRAFT_HEADER, DRAFT_COOKIE } = await import("./draft");
|
|
24
|
+
|
|
25
|
+
const DRAFT_BLOCKS = { "pages-home": { title: "DRAFT" } };
|
|
26
|
+
const POINTER = "abc.localhost:60534/api/o/decofile/m/main?token=t@v1";
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
headerState = new Headers({ host: "site.example" });
|
|
30
|
+
cookieState = {};
|
|
31
|
+
process.env.DECO_ALLOWED_PREVIEW_HOSTS = "site.example";
|
|
32
|
+
clearDraftCache();
|
|
33
|
+
vi.stubGlobal(
|
|
34
|
+
"fetch",
|
|
35
|
+
vi.fn(async (url: string) => {
|
|
36
|
+
if (
|
|
37
|
+
String(url) ===
|
|
38
|
+
"http://abc.localhost:60534/api/o/decofile/m/main?token=t"
|
|
39
|
+
) {
|
|
40
|
+
return new Response(JSON.stringify(DRAFT_BLOCKS), {
|
|
41
|
+
status: 200,
|
|
42
|
+
headers: { "content-type": "application/json" },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`unexpected fetch: ${url}`);
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
vi.unstubAllGlobals();
|
|
52
|
+
delete process.env.DECO_ALLOWED_PREVIEW_HOSTS;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("ensureDraft — app-shell (layout) binding via the forwarded header", () => {
|
|
56
|
+
it("binds the draft from x-deco-draft with no searchParams (the layout case)", async () => {
|
|
57
|
+
headerState.set(DRAFT_HEADER, POINTER);
|
|
58
|
+
// No searchParams (layouts get none) and no cookie (entry request).
|
|
59
|
+
expect(await ensureDraft()).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("still binds from the cookie when there is no forwarded header", async () => {
|
|
63
|
+
cookieState[DRAFT_COOKIE] = POINTER;
|
|
64
|
+
expect(await ensureDraft()).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("does not bind when neither header, cookie, nor param is present", async () => {
|
|
68
|
+
expect(await ensureDraft()).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("stays gated by the host allowlist even with a forwarded header", async () => {
|
|
72
|
+
headerState = new Headers({ host: "prod.example" });
|
|
73
|
+
headerState.set(DRAFT_HEADER, POINTER);
|
|
74
|
+
expect(await ensureDraft()).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { DraftPreviewIndicator } from "./DraftPreviewIndicator";
|
|
|
18
18
|
export {
|
|
19
19
|
DRAFT_COOKIE,
|
|
20
20
|
DRAFT_COOKIE_OPTIONS,
|
|
21
|
+
DRAFT_HEADER,
|
|
21
22
|
DRAFT_PARAM,
|
|
22
23
|
type DraftMiddlewareDecision,
|
|
23
24
|
type DraftSearchParams,
|
|
@@ -30,6 +31,7 @@ export {
|
|
|
30
31
|
export {
|
|
31
32
|
applyDraft,
|
|
32
33
|
DRAFT_ROUTE_PREFIX,
|
|
34
|
+
draftRequestHeaders,
|
|
33
35
|
prepareDraft,
|
|
34
36
|
rewriteToDraftRoute,
|
|
35
37
|
} from "./draftMiddleware";
|
package/src/routeHandlers.ts
CHANGED
|
@@ -57,7 +57,7 @@ function preflight(request: Request): Response {
|
|
|
57
57
|
|
|
58
58
|
/** For app/live/_meta/route.ts: `export { metaGET as GET } from "@decocms/nextjs/routeHandlers"` */
|
|
59
59
|
export async function metaGET(request: Request): Promise<Response> {
|
|
60
|
-
return withCors(request, handleMeta(request));
|
|
60
|
+
return withCors(request, await handleMeta(request));
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** For app/.decofile/route.ts (or an equivalent rewritten path — Next.js route
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { renderToString } from "react-dom/server";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { DraftPreviewBadge } from "./DraftPreviewBadge";
|
|
5
|
-
|
|
6
|
-
describe("DraftPreviewBadge (render)", () => {
|
|
7
|
-
it("surfaces the preview-mode label so a reviewer can't mistake a draft for live", () => {
|
|
8
|
-
const html = renderToString(<DraftPreviewBadge pointer="fila.vtex.app@v1" />);
|
|
9
|
-
expect(html).toContain("Preview mode");
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("embeds the deco mark inline (no external asset that could 404 on a consumer site)", () => {
|
|
13
|
-
const html = renderToString(<DraftPreviewBadge pointer="fila.vtex.app@v1" />);
|
|
14
|
-
expect(html).toContain("data:image/png;base64,");
|
|
15
|
-
});
|
|
16
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { buildExitUrl, buildShareUrl } from "./DraftPreviewBadge";
|
|
4
|
-
|
|
5
|
-
describe("buildShareUrl", () => {
|
|
6
|
-
it("pins the exact draft version so a recipient sees the same working tree", () => {
|
|
7
|
-
expect(buildShareUrl("https://fila.vtex.app/pdp/shoe", "abc.localhost:60534@v7")).toBe(
|
|
8
|
-
"https://fila.vtex.app/pdp/shoe?__draft=abc.localhost%3A60534%40v7",
|
|
9
|
-
);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("preserves existing query params, only setting the draft token", () => {
|
|
13
|
-
expect(buildShareUrl("https://fila.vtex.app/plp?sort=price", "h@v1")).toBe(
|
|
14
|
-
"https://fila.vtex.app/plp?sort=price&__draft=h%40v1",
|
|
15
|
-
);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("replaces a stale token already in the URL rather than appending a second", () => {
|
|
19
|
-
expect(buildShareUrl("https://fila.vtex.app/p?__draft=h@old", "h@new")).toBe(
|
|
20
|
-
"https://fila.vtex.app/p?__draft=h%40new",
|
|
21
|
-
);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe("buildExitUrl", () => {
|
|
26
|
-
it("sets the ?__draft=off exit sentinel", () => {
|
|
27
|
-
expect(buildExitUrl("https://fila.vtex.app/pdp/shoe")).toBe(
|
|
28
|
-
"https://fila.vtex.app/pdp/shoe?__draft=off",
|
|
29
|
-
);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("overrides an active token with off (leaves preview from anywhere)", () => {
|
|
33
|
-
expect(buildExitUrl("https://fila.vtex.app/p?__draft=h@v1")).toBe(
|
|
34
|
-
"https://fila.vtex.app/p?__draft=off",
|
|
35
|
-
);
|
|
36
|
-
});
|
|
37
|
-
});
|
package/src/decoMark.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deco brand mark, inlined as a data URI so the draft-preview badge is fully
|
|
3
|
-
* self-contained: it renders inside an arbitrary consumer site with no asset
|
|
4
|
-
* pipeline, no `<img src>` that could 404, and no dependency on the site
|
|
5
|
-
* shipping the logo. Only loaded when a draft is active (the badge is inert
|
|
6
|
-
* otherwise), so the ~21KB cost never touches ordinary production traffic.
|
|
7
|
-
*
|
|
8
|
-
* Source: the deco "d↗" logo (pos-d-logo). Swap this constant to rebrand.
|
|
9
|
-
*/
|
|
10
|
-
export const DECO_MARK_DATA_URI =
|
|
11
|
-
"data:image/png;base64,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";
|