@decocms/nextjs 8.1.0-next.0 → 8.4.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 +77 -330
- package/dist/DraftPreviewIndicator.d.ts +7 -0
- package/dist/DraftPreviewIndicator.d.ts.map +1 -0
- package/dist/DraftPreviewIndicator.js +19 -0
- package/dist/DraftPreviewIndicator.js.map +1 -0
- package/dist/StudioBridge.d.ts +16 -0
- package/dist/StudioBridge.d.ts.map +1 -0
- package/dist/StudioBridge.js +17 -0
- package/dist/StudioBridge.js.map +1 -0
- package/dist/draft.d.ts +35 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +43 -0
- package/dist/draft.js.map +1 -0
- package/dist/drafts.d.ts +94 -0
- package/dist/drafts.d.ts.map +1 -0
- package/dist/drafts.js +210 -0
- package/dist/drafts.js.map +1 -0
- package/dist/image-loader/index.d.ts +36 -0
- package/dist/image-loader/index.d.ts.map +1 -0
- package/dist/image-loader/index.js +27 -0
- package/dist/image-loader/index.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy/index.d.ts +79 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +92 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/server/index.d.ts +46 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +64 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/slot.d.ts +6 -0
- package/dist/server/slot.d.ts.map +1 -0
- package/dist/server/slot.js +13 -0
- package/dist/server/slot.js.map +1 -0
- package/dist/studio.d.ts +30 -0
- package/dist/studio.d.ts.map +1 -0
- package/dist/studio.js +55 -0
- package/dist/studio.js.map +1 -0
- package/package.json +41 -22
- package/src/DraftPreviewIndicator.tsx +17 -16
- package/src/StudioBridge.ts +23 -0
- package/src/config/index.cjs +78 -0
- package/src/config/index.d.cts +27 -0
- package/src/draft.ts +30 -216
- package/src/drafts.ts +229 -0
- package/src/image-loader/index.ts +62 -0
- package/src/index.ts +31 -45
- package/src/proxy/index.ts +161 -0
- package/src/server/index.ts +77 -0
- package/src/server/slot.ts +14 -0
- package/src/studio.ts +54 -0
- package/src/ClientOnlySection.tsx +0 -20
- package/src/DecoPageRenderer.test.tsx +0 -37
- package/src/DecoPageRenderer.tsx +0 -107
- package/src/DecoRootLayout.test.tsx +0 -22
- package/src/DecoRootLayout.tsx +0 -82
- package/src/DeferredSection.test.tsx +0 -27
- package/src/DeferredSection.tsx +0 -59
- package/src/DraftPreviewBadge.tsx +0 -16
- package/src/SectionRenderer.test.tsx +0 -97
- package/src/SectionRenderer.tsx +0 -58
- package/src/__conformance__/observability.test.ts +0 -66
- package/src/config.cjs +0 -92
- package/src/config.d.cts +0 -4
- package/src/config.test.ts +0 -86
- package/src/createDecoPage.draft.test.tsx +0 -91
- package/src/createDecoPage.test.tsx +0 -63
- package/src/createDecoPage.tsx +0 -173
- package/src/createDecoPreviewPage.test.tsx +0 -71
- package/src/createDecoPreviewPage.tsx +0 -108
- package/src/draft.test.ts +0 -73
- package/src/draftConstants.test.ts +0 -21
- package/src/draftConstants.ts +0 -30
- package/src/draftMiddleware.test.ts +0 -179
- package/src/draftMiddleware.ts +0 -163
- package/src/draftShell.test.ts +0 -76
- package/src/routeHandlers.test.ts +0 -243
- package/src/routeHandlers.ts +0 -227
- package/src/setup.test.ts +0 -253
- package/src/setup.ts +0 -180
- package/tsconfig.json +0 -7
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@decocms/nextjs/server`: draft previews in Server Components (pages,
|
|
3
|
+
* layouts, route handlers). Server only: it reads `next/headers`.
|
|
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)`:
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* export default async function Page({ searchParams }) {
|
|
11
|
+
* await bindDraft(cms, { searchParams: await searchParams, site: "my-store" });
|
|
12
|
+
* const client = contentClient(cms);
|
|
13
|
+
* …
|
|
14
|
+
* return <>{sections}<DraftPreviewIndicator /></>;
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
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.
|
|
21
|
+
*/
|
|
22
|
+
import type { Client, CMS } from "@decocms/blocks";
|
|
23
|
+
import { type PreviewPolicy } from "../drafts.ts";
|
|
24
|
+
import { activeDraftPointer } from "./slot.ts";
|
|
25
|
+
export { type PreviewPolicy, requestLike } from "../drafts.ts";
|
|
26
|
+
export { activeDraftPointer };
|
|
27
|
+
/** A page's `searchParams` prop, awaited. */
|
|
28
|
+
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`). */
|
|
31
|
+
searchParams?: SearchParams;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
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.
|
|
39
|
+
*/
|
|
40
|
+
export declare function bindDraft(cms: Pick<CMS, "draftPointer" | "settings" | "forRelease">, options?: BindDraftOptions): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* The content client for this request: the draft `bindDraft` bound, or the
|
|
43
|
+
* release.
|
|
44
|
+
*/
|
|
45
|
+
export declare function contentClient(cms: Pick<CMS, "forDraft" | "forRelease">): Client;
|
|
46
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@decocms/nextjs/server`: draft previews in Server Components (pages,
|
|
3
|
+
* layouts, route handlers). Server only: it reads `next/headers`.
|
|
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)`:
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* export default async function Page({ searchParams }) {
|
|
11
|
+
* await bindDraft(cms, { searchParams: await searchParams, site: "my-store" });
|
|
12
|
+
* const client = contentClient(cms);
|
|
13
|
+
* …
|
|
14
|
+
* return <>{sections}<DraftPreviewIndicator /></>;
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
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.
|
|
21
|
+
*/
|
|
22
|
+
import { headers } from "next/headers.js";
|
|
23
|
+
import { DRAFT_HEADER, DRAFT_PARAM } from "../draft.js";
|
|
24
|
+
import { allowedDraft, isDraftPreviewEnabled, requestLike } from "../drafts.js";
|
|
25
|
+
import { activeDraftPointer, draftSlot } from "./slot.js";
|
|
26
|
+
export { requestLike } from "../drafts.js";
|
|
27
|
+
export { activeDraftPointer };
|
|
28
|
+
function firstParam(searchParams, key) {
|
|
29
|
+
const raw = searchParams?.[key];
|
|
30
|
+
return (Array.isArray(raw) ? raw[0] : raw) ?? null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Binds the request's draft, if any: the page's own `?__draft=` first (`off`
|
|
34
|
+
* leaves preview), then the pointer the proxy forwarded on `x-deco-draft`,
|
|
35
|
+
* then the draft cookie, each on a host the options allow. Returns whether a
|
|
36
|
+
* draft was bound. With previews off on every host it returns `false`
|
|
37
|
+
* without reading the request, so a static page stays static.
|
|
38
|
+
*/
|
|
39
|
+
export async function bindDraft(cms, options = {}) {
|
|
40
|
+
const { searchParams, ...policy } = options;
|
|
41
|
+
if (!(await isDraftPreviewEnabled(cms, policy)))
|
|
42
|
+
return false;
|
|
43
|
+
const param = firstParam(searchParams, DRAFT_PARAM);
|
|
44
|
+
if (param === "off")
|
|
45
|
+
return false;
|
|
46
|
+
const requestHeaders = await headers();
|
|
47
|
+
const forwarded = param ?? requestHeaders.get(DRAFT_HEADER);
|
|
48
|
+
const query = forwarded ? `?${DRAFT_PARAM}=${encodeURIComponent(forwarded)}` : "";
|
|
49
|
+
const request = requestLike(requestHeaders, `/${query}`);
|
|
50
|
+
const pointer = request ? await allowedDraft(cms, request, policy) : null;
|
|
51
|
+
if (pointer === null)
|
|
52
|
+
return false;
|
|
53
|
+
draftSlot().pointer = pointer;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The content client for this request: the draft `bindDraft` bound, or the
|
|
58
|
+
* release.
|
|
59
|
+
*/
|
|
60
|
+
export function contentClient(cms) {
|
|
61
|
+
const pointer = activeDraftPointer();
|
|
62
|
+
return pointer ? cms.forDraft(pointer) : cms.forRelease();
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAsB,WAAW,EAAE,MAAM,cAAc,CAAC;AACpG,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE1D,OAAO,EAAsB,WAAW,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAU9B,SAAS,UAAU,CAAC,YAAsC,EAAE,GAAW;IACrE,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC,GAAG,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AACrD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAA0D,EAC1D,OAAO,GAAqB,EAAE;IAE9B,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC;IAC5C,IAAI,CAAC,CAAC,MAAM,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IACpD,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAClC,MAAM,cAAc,GAAG,MAAM,OAAO,EAAE,CAAC;IACvC,MAAM,SAAS,GAAG,KAAK,IAAI,cAAc,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,WAAW,IAAI,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAClF,MAAM,OAAO,GAAG,WAAW,CAAC,cAAyB,EAAE,IAAI,KAAK,EAAE,CAAC,CAAC;IACpE,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC1E,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACnC,SAAS,EAAE,CAAC,OAAO,GAAG,OAAO,CAAC;IAC9B,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,GAAyC;IACrE,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;IACrC,OAAO,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const draftSlot: () => {
|
|
2
|
+
pointer: string | null;
|
|
3
|
+
};
|
|
4
|
+
/** The draft pointer bound to this request (`bindDraft`), or `null` when it renders the release. */
|
|
5
|
+
export declare function activeDraftPointer(): string | null;
|
|
6
|
+
//# sourceMappingURL=slot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot.d.ts","sourceRoot":"","sources":["../../src/server/slot.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,SAAS,QAAa;IAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAA0B,CAAC;AAEtF,oGAAoG;AACpG,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,IAAI,CAElD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The draft a request renders, held for the request's Server Components.
|
|
3
|
+
* React's `cache()` gives one slot per request in the React Server
|
|
4
|
+
* Components graph, the scope a draft needs; no `next/headers` here, so the
|
|
5
|
+
* root components can read it without binding anything.
|
|
6
|
+
*/
|
|
7
|
+
import { cache } from "react";
|
|
8
|
+
export const draftSlot = cache(() => ({ pointer: null }));
|
|
9
|
+
/** The draft pointer bound to this request (`bindDraft`), or `null` when it renders the release. */
|
|
10
|
+
export function activeDraftPointer() {
|
|
11
|
+
return draftSlot().pointer;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=slot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot.js","sourceRoot":"","sources":["../../src/server/slot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAE9B,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,CAAC,GAA+B,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAEtF,oGAAoG;AACpG,MAAM,UAAU,kBAAkB;IAChC,OAAO,SAAS,EAAE,CAAC,OAAO,CAAC;AAC7B,CAAC"}
|
package/dist/studio.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where Deco Studio runs: the origins its site editor frames a site from and
|
|
3
|
+
* talks to it from (`postMessage`). One list, so the Studio bridge
|
|
4
|
+
* (`StudioBridge`), `withDeco()`'s dev origins and a site's own
|
|
5
|
+
* `frame-ancestors` never drift apart. This package's own copy: it never
|
|
6
|
+
* imports `@decocms/tanstack`.
|
|
7
|
+
*
|
|
8
|
+
* No module state and no framework code: safe in a browser bundle.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Studio's origins, in the CSP source form (`https://*.decocms.com` matches
|
|
12
|
+
* any subdomain). Test an origin with {@link isStudioOrigin}: the list holds a
|
|
13
|
+
* wildcard, so `includes()` isn't the check.
|
|
14
|
+
*/
|
|
15
|
+
export declare const STUDIO_ORIGINS: readonly string[];
|
|
16
|
+
/**
|
|
17
|
+
* The `frame-ancestors` sources a site sends so Studio can show it in the
|
|
18
|
+
* site editor: the site itself, {@link STUDIO_ORIGINS}, and v7's
|
|
19
|
+
* `https://*.deco.studio`. For a site that writes its own CSP; add the
|
|
20
|
+
* loopback origins in local development (`http://localhost:*`), where a
|
|
21
|
+
* Studio runs on the same machine.
|
|
22
|
+
*/
|
|
23
|
+
export declare const STUDIO_FRAME_ANCESTORS: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* Whether `origin` (a `MessageEvent.origin`, an `Origin` header) is one of
|
|
26
|
+
* {@link STUDIO_ORIGINS}: `https`, the default port, and `decocms.com` or a
|
|
27
|
+
* name under it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isStudioOrigin(origin: string | null | undefined): boolean;
|
|
30
|
+
//# sourceMappingURL=studio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"studio.d.ts","sourceRoot":"","sources":["../src/studio.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAK1C,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAIlD,CAAC;AAEH;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAazE"}
|
package/dist/studio.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where Deco Studio runs: the origins its site editor frames a site from and
|
|
3
|
+
* talks to it from (`postMessage`). One list, so the Studio bridge
|
|
4
|
+
* (`StudioBridge`), `withDeco()`'s dev origins and a site's own
|
|
5
|
+
* `frame-ancestors` never drift apart. This package's own copy: it never
|
|
6
|
+
* imports `@decocms/tanstack`.
|
|
7
|
+
*
|
|
8
|
+
* No module state and no framework code: safe in a browser bundle.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Studio's origins, in the CSP source form (`https://*.decocms.com` matches
|
|
12
|
+
* any subdomain). Test an origin with {@link isStudioOrigin}: the list holds a
|
|
13
|
+
* wildcard, so `includes()` isn't the check.
|
|
14
|
+
*/
|
|
15
|
+
export const STUDIO_ORIGINS = Object.freeze([
|
|
16
|
+
"https://studio.decocms.com",
|
|
17
|
+
"https://studio-stg.decocms.com",
|
|
18
|
+
"https://decocms.com",
|
|
19
|
+
"https://*.decocms.com",
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* The `frame-ancestors` sources a site sends so Studio can show it in the
|
|
23
|
+
* site editor: the site itself, {@link STUDIO_ORIGINS}, and v7's
|
|
24
|
+
* `https://*.deco.studio`. For a site that writes its own CSP; add the
|
|
25
|
+
* loopback origins in local development (`http://localhost:*`), where a
|
|
26
|
+
* Studio runs on the same machine.
|
|
27
|
+
*/
|
|
28
|
+
export const STUDIO_FRAME_ANCESTORS = Object.freeze([
|
|
29
|
+
"'self'",
|
|
30
|
+
...STUDIO_ORIGINS,
|
|
31
|
+
"https://*.deco.studio",
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* Whether `origin` (a `MessageEvent.origin`, an `Origin` header) is one of
|
|
35
|
+
* {@link STUDIO_ORIGINS}: `https`, the default port, and `decocms.com` or a
|
|
36
|
+
* name under it.
|
|
37
|
+
*/
|
|
38
|
+
export function isStudioOrigin(origin) {
|
|
39
|
+
if (typeof origin !== "string" || origin === "")
|
|
40
|
+
return false;
|
|
41
|
+
let url;
|
|
42
|
+
try {
|
|
43
|
+
url = new URL(origin);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// An origin is the scheme and host alone: `https://decocms.com/x` or one with
|
|
49
|
+
// credentials isn't one.
|
|
50
|
+
if (url.origin !== origin || url.protocol !== "https:" || url.port !== "")
|
|
51
|
+
return false;
|
|
52
|
+
const host = url.hostname;
|
|
53
|
+
return host === "decocms.com" || host.endsWith(".decocms.com");
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=studio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"studio.js","sourceRoot":"","sources":["../src/studio.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsB,MAAM,CAAC,MAAM,CAAC;IAC7D,4BAA4B;IAC5B,gCAAgC;IAChC,qBAAqB;IACrB,uBAAuB;CACxB,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAsB,MAAM,CAAC,MAAM,CAAC;IACrE,QAAQ;IACR,GAAG,cAAc;IACjB,uBAAuB;CACxB,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAiC;IAC9D,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAC9D,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,8EAA8E;IAC9E,yBAAyB;IACzB,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IACxF,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;IAC1B,OAAO,IAAI,KAAK,aAAa,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;AACjE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,48 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/nextjs",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
7
7
|
},
|
|
8
|
-
"description": "Deco
|
|
8
|
+
"description": "Deco CMS for any Next.js App Router app: draft previews in the proxy and the pages, the preview badge, the Studio bridge, withDeco() and a next/image loader",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "https://github.com/decocms/blocks.git",
|
|
12
12
|
"directory": "packages/nextjs"
|
|
13
13
|
},
|
|
14
|
-
"
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"!src/**/*.test.ts",
|
|
18
|
+
"!src/**/*.test.tsx",
|
|
19
|
+
"!src/**/__tests__"
|
|
20
|
+
],
|
|
15
21
|
"exports": {
|
|
16
|
-
".":
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"source": "./src/index.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./proxy": {
|
|
28
|
+
"types": "./dist/proxy/index.d.ts",
|
|
29
|
+
"source": "./src/proxy/index.ts",
|
|
30
|
+
"default": "./dist/proxy/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./server": {
|
|
33
|
+
"types": "./dist/server/index.d.ts",
|
|
34
|
+
"source": "./src/server/index.ts",
|
|
35
|
+
"default": "./dist/server/index.js"
|
|
36
|
+
},
|
|
19
37
|
"./config": {
|
|
20
|
-
"types": "./src/config.d.cts",
|
|
21
|
-
"default": "./src/config.cjs"
|
|
38
|
+
"types": "./src/config/index.d.cts",
|
|
39
|
+
"default": "./src/config/index.cjs"
|
|
22
40
|
},
|
|
23
|
-
"./
|
|
41
|
+
"./image-loader": {
|
|
42
|
+
"types": "./dist/image-loader/index.d.ts",
|
|
43
|
+
"source": "./src/image-loader/index.ts",
|
|
44
|
+
"default": "./dist/image-loader/index.js"
|
|
45
|
+
}
|
|
24
46
|
},
|
|
25
47
|
"scripts": {
|
|
26
|
-
"build": "tsc",
|
|
48
|
+
"build": "node ../../scripts/tsc.mjs --clean dist -p tsconfig.build.json",
|
|
49
|
+
"prepack": "npm run build",
|
|
27
50
|
"test": "vitest run --root ../.. packages/nextjs/",
|
|
28
|
-
"typecheck": "tsc
|
|
29
|
-
"lint:unused": "knip"
|
|
30
|
-
},
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"@decocms/blocks": "8.1.0-next.0",
|
|
33
|
-
"@decocms/blocks-admin": "8.1.0-next.0"
|
|
51
|
+
"typecheck": "node ../../scripts/tsc.mjs -p tsconfig.json"
|
|
34
52
|
},
|
|
35
53
|
"peerDependencies": {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"react
|
|
54
|
+
"@decocms/blocks": "^8.4.0",
|
|
55
|
+
"next": "^16.0.0",
|
|
56
|
+
"react": "^19.0.0"
|
|
39
57
|
},
|
|
40
58
|
"devDependencies": {
|
|
59
|
+
"@decocms/blocks": "8.4.0",
|
|
41
60
|
"@types/react": "^19.0.0",
|
|
42
61
|
"@types/react-dom": "^19.0.0",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
62
|
+
"next": "16.3.8",
|
|
63
|
+
"react": "19.2.7",
|
|
64
|
+
"react-dom": "19.2.7"
|
|
46
65
|
},
|
|
47
66
|
"publishConfig": {
|
|
48
67
|
"registry": "https://registry.npmjs.org",
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The "Preview mode" badge for the request's draft: a Server Component that
|
|
3
|
+
* renders `DraftBadge` (`@decocms/blocks/preview`) only when `bindDraft`
|
|
4
|
+
* bound a draft, so a published page emits nothing.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* nothing at all.
|
|
8
|
-
*
|
|
9
|
-
* Placement rule (inherited from `ensureDraft`): render this INSIDE the page
|
|
10
|
-
* subtree, after `ensureDraft` has been awaited — never from a layout, whose
|
|
11
|
-
* children render concurrently with the page that fills the slot. Sites using
|
|
12
|
-
* `createDecoPage` get it wired automatically; hand-rolled routes drop
|
|
13
|
-
* `<DraftPreviewIndicator />` at the end of their page.
|
|
6
|
+
* Render it inside the page, after `bindDraft` was awaited, never from a
|
|
7
|
+
* layout: a layout renders alongside the page that fills it, so it may run
|
|
8
|
+
* before the page bound its draft.
|
|
14
9
|
*/
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
10
|
+
import { DraftBadge } from "@decocms/blocks/preview";
|
|
11
|
+
import type { ReactNode } from "react";
|
|
12
|
+
import { activeDraftPointer } from "./server/slot.ts";
|
|
13
|
+
|
|
14
|
+
export interface DraftPreviewIndicatorProps {
|
|
15
|
+
/** Where "Exit preview" goes; default: the current URL with `?__draft=off`. */
|
|
16
|
+
exitHref?: string;
|
|
17
|
+
}
|
|
17
18
|
|
|
18
|
-
export function DraftPreviewIndicator() {
|
|
19
|
-
const pointer =
|
|
19
|
+
export function DraftPreviewIndicator({ exitHref }: DraftPreviewIndicatorProps = {}): ReactNode {
|
|
20
|
+
const pointer = activeDraftPointer();
|
|
20
21
|
if (!pointer) return null;
|
|
21
|
-
return <
|
|
22
|
+
return <DraftBadge pointer={pointer} exitHref={exitHref} />;
|
|
22
23
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `StudioBridge` of `@decocms/blocks/preview` with Studio's origins filled in
|
|
3
|
+
* (`STUDIO_ORIGINS`, `./studio.ts`): the package owns where Studio runs, so a
|
|
4
|
+
* site passes only its name. One inline script and no hooks, so a Server
|
|
5
|
+
* Component renders it as is.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
StudioBridge as BridgeScript,
|
|
9
|
+
type StudioBridgeProps as BridgeScriptProps,
|
|
10
|
+
} from "@decocms/blocks/preview";
|
|
11
|
+
import { createElement, type ReactNode } from "react";
|
|
12
|
+
import { STUDIO_ORIGINS } from "./studio.ts";
|
|
13
|
+
|
|
14
|
+
/** `site`, and optionally `editShortcut` and `nonce`; Studio's origins are the package's. */
|
|
15
|
+
export type StudioBridgeProps = Omit<BridgeScriptProps, "trustedOrigins">;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Lets Studio's site editor select and edit sections on the page when Studio
|
|
19
|
+
* frames it. Render it once, in the root layout's `<body>`.
|
|
20
|
+
*/
|
|
21
|
+
export function StudioBridge(props: StudioBridgeProps): ReactNode {
|
|
22
|
+
return createElement(BridgeScript, { ...props, trustedOrigins: STUDIO_ORIGINS });
|
|
23
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@decocms/nextjs/config`: `withDeco(nextConfig)` for `next.config`.
|
|
3
|
+
* Plain CommonJS JavaScript, shipped as is, so a CommonJS `next.config.js`
|
|
4
|
+
* can `require()` it; an ESM `next.config.mjs`/`.ts` imports it the same way.
|
|
5
|
+
*
|
|
6
|
+
* It adds two things and nothing about hosting (no rewrites, no image hosts,
|
|
7
|
+
* no CDN rules):
|
|
8
|
+
*
|
|
9
|
+
* 1. `draftHeaders` (default on): v7's two header rules, first, so a draft
|
|
10
|
+
* is never stored or indexed. A rule's `has` entries are ANDed and the two
|
|
11
|
+
* signals never meet: a draft link carries the query (the proxy sets the
|
|
12
|
+
* cookie on that response), every navigation after it the cookie.
|
|
13
|
+
* On a dynamic App Router response Next sets `Cache-Control` and `Vary`
|
|
14
|
+
* itself, so only `X-Robots-Tag` is sure to arrive from here; the proxy's
|
|
15
|
+
* `applyDraft` sets all three on the responses it makes.
|
|
16
|
+
* 2. `studioOrigins` (default on): Studio's origins in `allowedDevOrigins`,
|
|
17
|
+
* so `next dev` serves Studio's local mode its dev assets.
|
|
18
|
+
*
|
|
19
|
+
* The names repeat the package's (`DRAFT_PARAM`, `DRAFT_COOKIE`,
|
|
20
|
+
* `draftResponseHeaders` in `src/draft.ts`, `STUDIO_ORIGINS` in
|
|
21
|
+
* `src/studio.ts`): a CommonJS file can't load an ES module everywhere
|
|
22
|
+
* `next.config` runs. `config.test.ts` holds them equal.
|
|
23
|
+
*/
|
|
24
|
+
"use strict";
|
|
25
|
+
|
|
26
|
+
/** `draftResponseHeaders(pointer)` (`src/draft.ts`), as `next.config` header entries. */
|
|
27
|
+
const DRAFT_NO_STORE_HEADERS = [
|
|
28
|
+
{ key: "Cache-Control", value: "no-store, private" },
|
|
29
|
+
{ key: "Vary", value: "Cookie" },
|
|
30
|
+
{ key: "X-Robots-Tag", value: "noindex, nofollow" },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** v7's draft rules: the `?__draft=` query, or the `__deco_draft` cookie. */
|
|
34
|
+
const DECO_DRAFT_HEADERS = [
|
|
35
|
+
{
|
|
36
|
+
source: "/:path*",
|
|
37
|
+
has: [{ type: "query", key: "__draft" }],
|
|
38
|
+
headers: DRAFT_NO_STORE_HEADERS,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
source: "/:path*",
|
|
42
|
+
has: [{ type: "cookie", key: "__deco_draft" }],
|
|
43
|
+
headers: DRAFT_NO_STORE_HEADERS,
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `STUDIO_ORIGINS` as `allowedDevOrigins` hosts: `https://*.decocms.com`
|
|
49
|
+
* matches any name under `decocms.com` (`isStudioOrigin`), which Next writes
|
|
50
|
+
* as `**.decocms.com`.
|
|
51
|
+
*/
|
|
52
|
+
const STUDIO_DEV_ORIGINS = [
|
|
53
|
+
"studio.decocms.com",
|
|
54
|
+
"studio-stg.decocms.com",
|
|
55
|
+
"decocms.com",
|
|
56
|
+
"**.decocms.com",
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function withDeco(nextConfig = {}, options = {}) {
|
|
60
|
+
const { draftHeaders = true, studioOrigins = true } = options;
|
|
61
|
+
const config = { ...nextConfig };
|
|
62
|
+
if (studioOrigins) {
|
|
63
|
+
config.allowedDevOrigins = [
|
|
64
|
+
...new Set([...(nextConfig.allowedDevOrigins ?? []), ...STUDIO_DEV_ORIGINS]),
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
if (draftHeaders) {
|
|
68
|
+
const siteHeaders = nextConfig.headers;
|
|
69
|
+
config.headers = async () => [
|
|
70
|
+
// First: a site-wide Cache-Control rule must not win over a draft's.
|
|
71
|
+
...DECO_DRAFT_HEADERS,
|
|
72
|
+
...(siteHeaders ? await siteHeaders() : []),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
return config;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { withDeco, DECO_DRAFT_HEADERS, STUDIO_DEV_ORIGINS };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
/** A `next.config` header rule. */
|
|
4
|
+
export interface HeaderRule {
|
|
5
|
+
source: string;
|
|
6
|
+
has?: { type: string; key: string; value?: string }[];
|
|
7
|
+
headers: { key: string; value: string }[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WithDecoOptions {
|
|
11
|
+
/** v7's draft header rules (no-store, `Vary: Cookie`, noindex), before the site's own. Default `true`. */
|
|
12
|
+
draftHeaders?: boolean;
|
|
13
|
+
/** Studio's origins in `allowedDevOrigins`. Default `true`. */
|
|
14
|
+
studioOrigins?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The site's `next.config` with Deco's draft header rules and Studio's dev origins. */
|
|
18
|
+
export declare function withDeco<C extends NextConfig>(
|
|
19
|
+
nextConfig?: C,
|
|
20
|
+
options?: WithDecoOptions,
|
|
21
|
+
): C & NextConfig;
|
|
22
|
+
|
|
23
|
+
/** The header rules `withDeco` puts first: a `?__draft=` query, or the `__deco_draft` cookie. */
|
|
24
|
+
export declare const DECO_DRAFT_HEADERS: HeaderRule[];
|
|
25
|
+
|
|
26
|
+
/** `STUDIO_ORIGINS` (`@decocms/nextjs`) in `allowedDevOrigins` form. */
|
|
27
|
+
export declare const STUDIO_DEV_ORIGINS: string[];
|