@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.
Files changed (84) hide show
  1. package/README.md +77 -330
  2. package/dist/DraftPreviewIndicator.d.ts +7 -0
  3. package/dist/DraftPreviewIndicator.d.ts.map +1 -0
  4. package/dist/DraftPreviewIndicator.js +19 -0
  5. package/dist/DraftPreviewIndicator.js.map +1 -0
  6. package/dist/StudioBridge.d.ts +16 -0
  7. package/dist/StudioBridge.d.ts.map +1 -0
  8. package/dist/StudioBridge.js +17 -0
  9. package/dist/StudioBridge.js.map +1 -0
  10. package/dist/draft.d.ts +35 -0
  11. package/dist/draft.d.ts.map +1 -0
  12. package/dist/draft.js +43 -0
  13. package/dist/draft.js.map +1 -0
  14. package/dist/drafts.d.ts +94 -0
  15. package/dist/drafts.d.ts.map +1 -0
  16. package/dist/drafts.js +210 -0
  17. package/dist/drafts.js.map +1 -0
  18. package/dist/image-loader/index.d.ts +36 -0
  19. package/dist/image-loader/index.d.ts.map +1 -0
  20. package/dist/image-loader/index.js +27 -0
  21. package/dist/image-loader/index.js.map +1 -0
  22. package/dist/index.d.ts +29 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +29 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/proxy/index.d.ts +79 -0
  27. package/dist/proxy/index.d.ts.map +1 -0
  28. package/dist/proxy/index.js +92 -0
  29. package/dist/proxy/index.js.map +1 -0
  30. package/dist/server/index.d.ts +46 -0
  31. package/dist/server/index.d.ts.map +1 -0
  32. package/dist/server/index.js +64 -0
  33. package/dist/server/index.js.map +1 -0
  34. package/dist/server/slot.d.ts +6 -0
  35. package/dist/server/slot.d.ts.map +1 -0
  36. package/dist/server/slot.js +13 -0
  37. package/dist/server/slot.js.map +1 -0
  38. package/dist/studio.d.ts +30 -0
  39. package/dist/studio.d.ts.map +1 -0
  40. package/dist/studio.js +55 -0
  41. package/dist/studio.js.map +1 -0
  42. package/package.json +41 -22
  43. package/src/DraftPreviewIndicator.tsx +17 -16
  44. package/src/StudioBridge.ts +23 -0
  45. package/src/config/index.cjs +78 -0
  46. package/src/config/index.d.cts +27 -0
  47. package/src/draft.ts +30 -216
  48. package/src/drafts.ts +229 -0
  49. package/src/image-loader/index.ts +62 -0
  50. package/src/index.ts +31 -45
  51. package/src/proxy/index.ts +161 -0
  52. package/src/server/index.ts +77 -0
  53. package/src/server/slot.ts +14 -0
  54. package/src/studio.ts +54 -0
  55. package/src/ClientOnlySection.tsx +0 -20
  56. package/src/DecoPageRenderer.test.tsx +0 -37
  57. package/src/DecoPageRenderer.tsx +0 -107
  58. package/src/DecoRootLayout.test.tsx +0 -22
  59. package/src/DecoRootLayout.tsx +0 -82
  60. package/src/DeferredSection.test.tsx +0 -27
  61. package/src/DeferredSection.tsx +0 -59
  62. package/src/DraftPreviewBadge.tsx +0 -16
  63. package/src/SectionRenderer.test.tsx +0 -97
  64. package/src/SectionRenderer.tsx +0 -58
  65. package/src/__conformance__/observability.test.ts +0 -66
  66. package/src/config.cjs +0 -92
  67. package/src/config.d.cts +0 -4
  68. package/src/config.test.ts +0 -86
  69. package/src/createDecoPage.draft.test.tsx +0 -91
  70. package/src/createDecoPage.test.tsx +0 -63
  71. package/src/createDecoPage.tsx +0 -173
  72. package/src/createDecoPreviewPage.test.tsx +0 -71
  73. package/src/createDecoPreviewPage.tsx +0 -108
  74. package/src/draft.test.ts +0 -73
  75. package/src/draftConstants.test.ts +0 -21
  76. package/src/draftConstants.ts +0 -30
  77. package/src/draftMiddleware.test.ts +0 -179
  78. package/src/draftMiddleware.ts +0 -163
  79. package/src/draftShell.test.ts +0 -76
  80. package/src/routeHandlers.test.ts +0 -243
  81. package/src/routeHandlers.ts +0 -227
  82. package/src/setup.test.ts +0 -253
  83. package/src/setup.ts +0 -180
  84. 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"}
@@ -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.1.0-next.0",
3
+ "version": "8.4.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
7
7
  },
8
- "description": "Deco framework binding for Next.js App Router",
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
- "main": "./src/index.ts",
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "!src/**/*.test.ts",
18
+ "!src/**/*.test.tsx",
19
+ "!src/**/__tests__"
20
+ ],
15
21
  "exports": {
16
- ".": "./src/index.ts",
17
- "./routeHandlers": "./src/routeHandlers.ts",
18
- "./setup": "./src/setup.ts",
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
- "./middleware": "./src/draftMiddleware.ts"
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 --noEmit",
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
- "next": ">=15.0.0",
37
- "react": "^19.0.0",
38
- "react-dom": "^19.0.0"
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
- "knip": "^5.86.0",
44
- "next": "^15.4.0",
45
- "typescript": "^5.9.0"
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
- * Server-side gate for the preview-mode badge.
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
- * A Server Component (no `"use client"`) so it can read the request-scoped
5
- * draft pointer via `getActiveDraftPointer()` and mount the client badge ONLY
6
- * when a draft is actually bound — an unconfigured/published request renders
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 { DraftPreviewBadge } from "./DraftPreviewBadge";
16
- import { getActiveDraftPointer } from "./draft";
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 = getActiveDraftPointer();
19
+ export function DraftPreviewIndicator({ exitHref }: DraftPreviewIndicatorProps = {}): ReactNode {
20
+ const pointer = activeDraftPointer();
20
21
  if (!pointer) return null;
21
- return <DraftPreviewBadge pointer={pointer} />;
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[];