@decocms/nextjs 7.28.0-beta.3 → 7.28.0-beta.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/nextjs",
3
- "version": "7.28.0-beta.3",
3
+ "version": "7.28.0-beta.5",
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": "7.28.0-beta.3",
33
- "@decocms/blocks-admin": "7.28.0-beta.3"
32
+ "@decocms/blocks": "7.28.0-beta.5",
33
+ "@decocms/blocks-admin": "7.28.0-beta.5"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "next": ">=15.0.0",
@@ -32,6 +32,14 @@ describe("prepareDraft", () => {
32
32
  );
33
33
  });
34
34
 
35
+ it("passes through when no env override is set — the page gate is authoritative", () => {
36
+ // The site-block hosts live in the server-components graph; middleware
37
+ // can't see them, so without the env kill switch it must not block.
38
+ delete process.env.DECO_ALLOWED_PREVIEW_HOSTS;
39
+ const req = request("https://anything.example/p?__draft=abc.localhost@v1");
40
+ expect(prepareDraft(req).pointer).toBe("abc.localhost@v1");
41
+ });
42
+
35
43
  it("is inert on a host outside the allowlist — the production domain", () => {
36
44
  // Same build, different Host: no cookie, no rewrite, nothing touched.
37
45
  const req = request("https://fila.com.br/p?__draft=abc@v1");
@@ -40,12 +40,20 @@ import {
40
40
  const INERT: DraftMiddlewareDecision = { pointer: null, setCookie: null, clearCookie: false };
41
41
 
42
42
  export function prepareDraft(request: NextRequest): DraftMiddlewareDecision {
43
- // Host gate first: on a host not named in DECO_ALLOWED_PREVIEW_HOSTS the
44
- // decision is inert — no cookie written, no rewrite, no headers touched —
45
- // so the production domain behaves as if the feature didn't exist.
46
- const host =
47
- request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? request.nextUrl.host;
48
- if (!isDraftHostAllowed(host)) return INERT;
43
+ // The AUTHORITATIVE host gate lives page-side (`ensureDraft`): the site
44
+ // block's `previewHosts` are installed in the server-components graph, and
45
+ // middleware runs in a separate module graph/runtime that never sees them.
46
+ // Middleware therefore hard-gates only on the env override — keeping
47
+ // DECO_ALLOWED_PREVIEW_HOSTS a full kill switch — and otherwise passes the
48
+ // decision through. Worst case on a non-preview host is a cookie the
49
+ // page-side gate then ignores; drafts never render there.
50
+ if (process.env.DECO_ALLOWED_PREVIEW_HOSTS) {
51
+ const host =
52
+ request.headers.get("x-forwarded-host") ??
53
+ request.headers.get("host") ??
54
+ request.nextUrl.host;
55
+ if (!isDraftHostAllowed(host)) return INERT;
56
+ }
49
57
  return decideDraft(new URL(request.url), request.cookies.get(DRAFT_COOKIE)?.value ?? null);
50
58
  }
51
59
 
package/src/setup.ts CHANGED
@@ -38,7 +38,7 @@
38
38
  * deploys must ship the directory alongside the server bundle).
39
39
  */
40
40
  import type { ApplySectionConventionsInput } from "@decocms/blocks/cms";
41
- import { applySectionConventions, loadBlocks } from "@decocms/blocks/cms";
41
+ import { applySectionConventions, loadBlocks, setDraftPreviewHosts } from "@decocms/blocks/cms";
42
42
  import { loadDecofileDirectory } from "@decocms/blocks/cms/loadDecofileDirectory";
43
43
  import { createSiteSetup, type SiteSetupOptions } from "@decocms/blocks/setup";
44
44
 
@@ -110,9 +110,29 @@ export interface NextSetupOptions {
110
110
  * instead, nothing imports the JSON, so edits invalidate nothing and dev
111
111
  * serves stale content until a full restart.
112
112
  */
113
+ /**
114
+ * Install the site block's `previewHosts` as the draft-preview allowlist.
115
+ *
116
+ * Reads the BASE blocks handed to setup — never `loadBlocks()` at request
117
+ * time, where the draft override is merged in: an allowlist readable through
118
+ * the override could be rewritten by the very draft it gates.
119
+ * `DECO_ALLOWED_PREVIEW_HOSTS` remains an operational override that replaces
120
+ * this list when set.
121
+ */
122
+ function installPreviewHosts(blocks: Record<string, unknown> | undefined): void {
123
+ const site = blocks?.site as { previewHosts?: unknown } | undefined;
124
+ if (Array.isArray(site?.previewHosts)) setDraftPreviewHosts(site.previewHosts);
125
+ }
126
+
113
127
  export function createNextSetup(options: NextSetupOptions): () => Promise<void> {
114
128
  let setupPromise: Promise<void> | null = null;
115
129
 
130
+ // Draft preview opt-in from the repo: read SYNCHRONOUSLY at createNextSetup
131
+ // time (module evaluation), not inside the lazy ensureSetup — pages call
132
+ // `ensureDraft` BEFORE they resolve CMS content, so hosts installed lazily
133
+ // would arrive after the gate already said no on the first request.
134
+ installPreviewHosts(options.blocks);
135
+
116
136
  return function ensureSetup(): Promise<void> {
117
137
  setupPromise ??= (async () => {
118
138
  const dirBlocks =
@@ -120,6 +140,8 @@ export function createNextSetup(options: NextSetupOptions): () => Promise<void>
120
140
  ? {}
121
141
  : await loadDecofileDirectory(options.blocksDir ?? ".deco/blocks");
122
142
  const blocks = { ...dirBlocks, ...options.blocks };
143
+ // Covers the blocksDir mode, where blocks only exist after the fs read.
144
+ installPreviewHosts(blocks);
123
145
 
124
146
  createSiteSetup({
125
147
  sections: options.sections,