@decocms/nextjs 8.0.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 (85) hide show
  1. package/README.md +77 -332
  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 -210
  48. package/src/drafts.ts +229 -0
  49. package/src/image-loader/index.ts +62 -0
  50. package/src/index.ts +31 -43
  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.render.test.tsx +0 -16
  63. package/src/DraftPreviewBadge.test.ts +0 -37
  64. package/src/DraftPreviewBadge.tsx +0 -252
  65. package/src/SectionRenderer.test.tsx +0 -97
  66. package/src/SectionRenderer.tsx +0 -58
  67. package/src/config.cjs +0 -92
  68. package/src/config.d.cts +0 -4
  69. package/src/config.test.ts +0 -86
  70. package/src/createDecoPage.draft.test.tsx +0 -91
  71. package/src/createDecoPage.test.tsx +0 -63
  72. package/src/createDecoPage.tsx +0 -173
  73. package/src/createDecoPreviewPage.test.tsx +0 -71
  74. package/src/createDecoPreviewPage.tsx +0 -108
  75. package/src/decoMark.ts +0 -11
  76. package/src/draft.test.ts +0 -73
  77. package/src/draftConstants.test.ts +0 -21
  78. package/src/draftConstants.ts +0 -15
  79. package/src/draftMiddleware.test.ts +0 -147
  80. package/src/draftMiddleware.ts +0 -136
  81. package/src/routeHandlers.test.ts +0 -243
  82. package/src/routeHandlers.ts +0 -227
  83. package/src/setup.test.ts +0 -253
  84. package/src/setup.ts +0 -180
  85. package/tsconfig.json +0 -7
package/README.md CHANGED
@@ -1,367 +1,112 @@
1
- # `@decocms/nextjs`
1
+ # @decocms/nextjs
2
2
 
3
- Deco framework binding for Next.js App Router — the Next.js sibling of
4
- `@decocms/tanstack`. Four surfaces, composed together:
3
+ Deco CMS for any [Next.js](https://nextjs.org) App Router app. It makes no hosting assumptions: no CDN, ISR or platform behaviour is built in. It is a set of small helpers your site calls from its own proxy, layout, pages and `next.config`.
5
4
 
6
- - **`@decocms/nextjs/config`** — `withDeco(nextConfig)`, a `next.config`
7
- wrapper that adds the rewrites and `transpilePackages` Deco's admin
8
- protocol needs.
9
- - **`@decocms/nextjs/routeHandlers`** — `createDecoRouteHandlers({ setup })`,
10
- a single catch-all dispatcher for the whole Studio admin protocol
11
- (decofile read/reload, meta schema, invoke, live previews).
12
- - **`@decocms/nextjs`** — `createDecoPreviewPage({ setup })`, an App Router
13
- Server Component that renders live previews through Next's RSC pipeline,
14
- including sections marked with `"use client"`.
15
- - **`@decocms/nextjs/setup`** — `createNextSetup(options)`, a one-call site
16
- bootstrap: the Next.js analogue of Vite's
17
- `createSiteSetup` + `createAdminSetup` + `import.meta.glob`.
5
+ Peers: `@decocms/blocks` (same version), `next` 16, `react` 19.
18
6
 
19
- This document is the complete recipe a new Next.js site follows to wire all
20
- all four together. Every code block below is meant to be copied, not
21
- paraphrased.
22
-
23
- ## Install
24
-
25
- ```bash
26
- bun add @decocms/nextjs @decocms/blocks @decocms/blocks-admin
27
- ```
28
-
29
- `next`, `react`, and `react-dom` are peer dependencies — the site already
30
- has them.
31
-
32
- ## 1. `next.config` — `withDeco`
33
-
34
- `withDeco` adds three rewrites (mapping the Studio-protocol URLs Next.js
35
- cannot express as route segments onto `/deco/*`, where the catch-all route
36
- below serves them) and appends the three `@decocms/*` packages (which ship
37
- raw TypeScript, not a prebuilt `dist/`) to `transpilePackages`.
38
-
39
- TypeScript (`next.config.ts`):
40
-
41
- ```ts
42
- import type { NextConfig } from "next";
43
- import { withDeco } from "@decocms/nextjs/config";
44
-
45
- const nextConfig: NextConfig = {
46
- // ...your own config
47
- };
48
-
49
- export default withDeco(nextConfig);
50
- ```
51
-
52
- CommonJS (`next.config.js`) — most Next.js sites still use this form:
53
-
54
- ```js
55
- /** @type {import('next').NextConfig} */
56
- const { withDeco } = require("@decocms/nextjs/config");
57
-
58
- const nextConfig = {
59
- // ...your own config
60
- };
61
-
62
- module.exports = withDeco(nextConfig);
63
- ```
64
-
65
- `withDeco` merges with a `rewrites`/`transpilePackages` you already have —
66
- it never replaces them. If your own `rewrites()` returns the array form,
67
- Deco's rewrites are prepended; if it returns the `{ beforeFiles, afterFiles,
68
- fallback }` object form, Deco's rewrites are prepended to `beforeFiles`.
7
+ | Import | What it is for |
8
+ | --- | --- |
9
+ | `@decocms/nextjs/proxy` | Draft previews in `proxy.ts`: `decideDraft`, `draftRequestHeaders`, `rewriteToDraftRoute`, `applyDraft`, `isDraftRoutePath`, or `draftProxy` for a site with no proxy of its own. No React and no block map. |
10
+ | `@decocms/nextjs/server` | Draft previews in Server Components: `bindDraft`, `activeDraftPointer`, `contentClient`. |
11
+ | `@decocms/nextjs` | `DraftPreviewIndicator` (the "Preview mode" badge on a draft page), `StudioBridge` (lets Studio's site editor drive the page) and `StudioPage` (tells it which page is on screen), plus the draft names and Studio's origins (below). |
12
+ | `@decocms/nextjs/config` | `withDeco(nextConfig)`: the draft header rules and Studio's dev origins. CommonJS, so `next.config.js` can `require()` it. |
13
+ | `@decocms/nextjs/image-loader` | `createDecoImageLoader(imageUrl)`: a next/image loader over an `ImageUrl` from `@decocms/blocks/image`. |
69
14
 
70
- ## 2. The catch-all route
15
+ `createCMS` reads `NODE_ENV` to decide dev mode, so `next dev` never swaps the hosted release in over your local content; nothing needs passing. `createCMS({ dev })` still overrides it when a site needs to.
71
16
 
72
- Mount `createDecoRouteHandlers` at `app/deco/[[...deco]]/route.ts` — this
73
- one file serves the entire admin protocol (decofile, meta, invoke, live
74
- previews) for both the rewritten public URLs (`/.decofile`, `/live/_meta`,
75
- `/live/previews/*`) and their `/deco/*` destinations directly.
17
+ ## Drafts
76
18
 
77
19
  ```ts
78
- // src/app/deco/[[...deco]]/route.ts
79
- import { createDecoRouteHandlers } from "@decocms/nextjs/routeHandlers";
80
- import { ensureSetup } from "../../../deco/setup";
20
+ // src/cms.config.ts: the options both CMS handles share, and where drafts preview
21
+ export const cmsConfig = { content };
22
+ export const preview = { site: "my-store" };
23
+
24
+ // src/cms.ts and src/cms.proxy.ts
25
+ export const cms = createCMS({ blocks, ...cmsConfig });
26
+ export const proxyCms = createCMS({ blocks: {}, ...cmsConfig }); // no block map in the proxy
27
+
28
+ // src/proxy.ts
29
+ export async function proxy(request: NextRequest) {
30
+ if (isDraftRoutePath(request.nextUrl.pathname)) return new NextResponse(null, { status: 404 });
31
+ const draft = await decideDraft(proxyCms, request, preview);
32
+ const response =
33
+ rewriteToDraftRoute(request, draft) ??
34
+ NextResponse.next({ request: { headers: draftRequestHeaders(request, draft) } });
35
+ return applyDraft(response, draft);
36
+ }
81
37
 
38
+ // src/app/%5Fdraft/[[...path]]/page.tsx: the dynamic draft route (a literal `_draft` folder never routes)
82
39
  export const dynamic = "force-dynamic";
83
-
84
- export const { GET, POST, OPTIONS } = createDecoRouteHandlers({
85
- setup: ensureSetup,
86
- });
40
+ export default async function DraftPage({ params, searchParams }) {
41
+ await bindDraft(cms, { searchParams: await searchParams, ...preview });
42
+ const client = contentClient(cms);
43
+ // … resolve and render the page as on the published route …
44
+ return <>{page}<DraftPreviewIndicator /></>;
45
+ }
87
46
  ```
88
47
 
89
- `dynamic = "force-dynamic"` is required — this route must never be
90
- statically cached (decofile reloads, invoke calls, and previews all need a
91
- fresh response per request).
48
+ Published pages stay static: the proxy sends only drafted requests to the draft route, sets the draft cookie, forwards the accepted pointer on `x-deco-draft` (removing any copy sent from outside), and marks the response `Cache-Control: no-store, private` and `X-Robots-Tag: noindex, nofollow`. A root layout that reads content calls `bindDraft(cms, preview)` too: it reads the forwarded header.
92
49
 
93
- ### The RSC preview page
50
+ ### Draft hosts
94
51
 
95
- Mount `createDecoPreviewPage` at the framework-owned `/deco/preview` path.
96
- This path is intentionally not configurable: preview GET requests are always
97
- redirected from the catch-all route to this App Router page, with the query
98
- string preserved. Other admin requests, including `POST /deco/render`, remain
99
- on the catch-all handler.
52
+ v7's allowlist. A draft link (`?__draft=`) previews on the preview hosts the site declares (the `CMS` block's `preview.hosts`, or `createCMS`'s; a site that declares none has none, as in v7) and always on the hosts Deco runs the site on, `<site>.deco.site` and `<site>.deco-cx.workers.dev`. `previewHosts: [...]` replaces the declared list (v7's `DECO_ALLOWED_PREVIEW_HOSTS`, such as `["localhost:3000"]` in development); `previewHosts: "none"` turns previews off on every host. Hosts compare exactly, port included, on the host the visitor asked for (`x-forwarded-host`, else `host`). Pass the same options to `decideDraft` and `bindDraft`. On a host that may not preview, the page renders the release; `?__draft=off` still ends a preview anywhere.
100
53
 
101
- ```tsx
102
- // src/app/deco/preview/[[...path]]/page.tsx
103
- import { createDecoPreviewPage } from "@decocms/nextjs";
104
- import { ensureSetup } from "../../../../deco/setup";
54
+ ## The layout
105
55
 
106
- export const dynamic = "force-dynamic";
56
+ ```tsx
57
+ import { StudioBridge } from "@decocms/nextjs";
107
58
 
108
- export default createDecoPreviewPage({ setup: ensureSetup });
59
+ <body>
60
+ {children}
61
+ <StudioBridge site="my-store" />
62
+ </body>
109
63
  ```
110
64
 
111
- The page boundary is essential for Client Components. A route handler can
112
- produce plain HTML with `react-dom/server`, but that renderer cannot execute
113
- the client-reference proxies Next creates for modules marked
114
- `"use client"`. An App Router page is rendered by Next's React Server
115
- Components pipeline, so it can render a server section tree containing
116
- Client Components and preserve their hydration metadata.
117
-
118
- Do not work around this error by removing `"use client"` from interactive
119
- components. Keep sections and wrappers as Server Components when they only
120
- render markup, but retain a Client Component boundary wherever hooks, event
121
- handlers, browser APIs, or client-only context are required. The RSC preview
122
- page exists so those two kinds of component can compose exactly as they do in
123
- the storefront.
124
-
125
- Importing the root barrel is correct in this `page.tsx`; the subpath-only
126
- rule below applies specifically to `route.ts` files.
127
-
128
- ### Route-handler import rule: subpaths, never the root barrel
129
-
130
- **Always import from `@decocms/nextjs/routeHandlers` (or `/config`,
131
- `/setup`) in a `route.ts` file. Never `import { ... } from "@decocms/nextjs"`
132
- there.**
133
-
134
- App Router route handlers evaluate their entire module graph against
135
- React's **react-server** build, and this happens regardless of any
136
- `"use client"` directive in that graph — route handlers have no client
137
- bundle to move a `"use client"` module into, so the directive is simply
138
- ignored. The root barrel (`@decocms/nextjs`'s `.` export) re-exports the
139
- render components (`DecoRootLayout`, `SectionRenderer`, `DecoPageRenderer`,
140
- ...), whose module graph reaches component code that uses client-only React
141
- APIs (`createContext`, `useContext`, class components with
142
- `componentDidCatch`, etc.) at module scope. Importing the root barrel from
143
- a route file pulls that whole graph into react-server evaluation and
144
- crashes at **import time**, before your handler ever runs, with errors like
145
- `"...createContext is not a function"` or `"Class extends value undefined
146
- is not a constructor"` — even if the route handler itself never touches
147
- those components. The `/routeHandlers`, `/config`, and `/setup` subpaths
148
- are each scoped to keep their own module graph free of component code, so
149
- they're safe to import from anywhere, including a route file.
150
-
151
- ## 3. `src/deco/setup.ts` — `createNextSetup`
65
+ Studio's origins are the package's own (`STUDIO_ORIGINS`), so the bridge takes only the site's name. v7's "." edit shortcut is on by default (`editShortcut={false}` turns it off), and `nonce` is optional.
152
66
 
153
- The codegen artifacts (`sections.gen.ts`, `meta.gen.json`) are generated into
154
- `.deco/` at the site root — the same default `@decocms/blocks-cli`'s
155
- generators use everywhere else (framework artifacts live in the framework's
156
- folder, not scattered across `src/`). `src/deco/setup.ts` isn't adjacent to
157
- `.deco/`, so import it through a `deco/*` tsconfig path alias instead of a
158
- relative path:
67
+ On a page that isn't in Studio's frame, "." (or Ctrl+Shift+E) opens the page in Studio's editor, and Ctrl or Cmd+"." opens it in a new tab; Studio asks a visitor who isn't signed in to sign in first. To open the right page, each CMS page tells the bridge its path template: the root layout stays across navigations, so the page renders the marker, next to the sections it resolved:
159
68
 
160
- ```json
161
- // tsconfig.json
162
- {
163
- "compilerOptions": {
164
- "paths": {
165
- "deco/*": [".deco/*"]
166
- }
167
- }
168
- }
169
- ```
170
-
171
- ```ts
172
- // src/deco/setup.ts
173
- import { createNextSetup } from "@decocms/nextjs/setup";
174
- import blocks from "deco/blocksManifest.gen"; // .deco/blocksManifest.gen.ts — see below
175
- import { sectionImports, sectionMeta, syncComponents, loadingFallbacks } from "deco/sections.gen";
69
+ ```tsx
70
+ import { StudioPage } from "@decocms/nextjs";
176
71
 
177
- export const ensureSetup = createNextSetup({
178
- blocks,
179
- blocksDir: false, // the manifest replaces the runtime fs read
180
- sections: sectionImports,
181
- conventions: { meta: sectionMeta, syncComponents, loadingFallbacks },
182
- meta: () => import("deco/meta.gen.json").then((m) => m.default),
183
- });
72
+ const match = matchRoute(pathname, { routes: pages });
73
+ // …
74
+ return <>{sections}<StudioPage pathTemplate={match.entry.path} /></>;
184
75
  ```
185
76
 
186
- ### Recommended block source: the static-import manifest
187
-
188
- `generate-blocks-manifest` (from `@decocms/blocks-cli`) emits
189
- `.deco/blocksManifest.gen.ts` — a module that **statically imports** every
190
- `.deco/blocks/*.json` file (raw on-disk filename in the specifier; keys are
191
- the filename minus `.json`, verbatim, exactly like
192
- `loadDecofileDirectory`). Passing its default export as `blocks` with
193
- `blocksDir: false` (as in the snippet above) makes the manifest the sole
194
- block source and the bootstrap pure — no filesystem access.
195
-
196
- Why this is the recommended wiring over the default `blocksDir` runtime
197
- read:
198
-
199
- - **CMS content hot-reloads in `next dev`.** The block JSONs are part of
200
- Next's module graph, so editing one (Studio daemon write, sync, manual
201
- edit) invalidates the server module graph, re-evaluates the setup module,
202
- and rebuilds `createNextSetup`'s memo with the fresh content —
203
- ~120–165ms per edit, measured on a 500-block site. The runtime fs read is
204
- invisible to the bundler: edits invalidate nothing and dev serves stale
205
- content until a restart.
206
- - **Deploys need no `outputFileTracingIncludes` hack** — the JSON is
207
- bundled into the build output instead of read from disk at runtime.
208
-
209
- Trade-offs to know about:
210
-
211
- - **Content is baked at build time** in production. Studio's
212
- `POST /.decofile` still overrides the in-memory snapshot at runtime
213
- (live preview/publish keeps working on a warm instance); the baked
214
- manifest is the cold-start baseline.
215
- - **Adding or removing a block *file* requires re-running the generator**
216
- (content edits to existing files do not). Wire it into the site's
217
- `generate` chain — see the scripts section below.
218
-
219
- If you skip the manifest, the default `blocksDir: ".deco/blocks"` runtime
220
- read still works — just without dev reload, and your deploy must ship the
221
- directory alongside the server bundle.
222
-
223
- Keep the manifest out of any client-reachable import graph (import it only
224
- from the server-side setup module) so 5MB of CMS JSON never lands in a
225
- client bundle.
226
-
227
- `createNextSetup` returns a **memoized** `ensureSetup()` function — a
228
- successful bootstrap is cached for the life of the warm serverless
229
- instance; a *rejected* bootstrap clears the memo so the next call retries
230
- from scratch (the triggering call still rejects with the original error).
231
-
232
- Three call sites need `ensureSetup()`:
233
-
234
- 1. **The catch-all route** (above) passes it as `{ setup: ensureSetup }` —
235
- `createDecoRouteHandlers` awaits it before dispatching every admin
236
- request.
237
- 2. **The root layout** (`app/layout.tsx`) must await it directly before
238
- rendering, since page rendering (`createDecoPage`'s resolver) has no
239
- setup hook of its own:
240
-
241
- ```tsx
242
- // src/app/layout.tsx
243
- import { DecoRootLayout } from "@decocms/nextjs";
244
- import { ensureSetup } from "../deco/setup";
77
+ It renders a `<meta>` that React places in the `<head>`, so the page's own markup doesn't change. Without it, Studio picks the page from the URL.
245
78
 
246
- export default async function RootLayout({ children }: { children: React.ReactNode }) {
247
- await ensureSetup();
248
- return <DecoRootLayout siteName="my-site">{children}</DecoRootLayout>;
249
- }
250
- ```
79
+ ## Names and Studio's origins
251
80
 
252
- (`app/layout.tsx` is a Server Component, not a route handler, so it's
253
- fine to import the root barrel here — see the rule above.)
254
- 3. **The RSC preview page** passes it to `createDecoPreviewPage`, as shown
255
- above. The setup function is memoized, so these call sites share one
256
- successful bootstrap per warm process.
81
+ For a site that needs them in its own code (a route that sets its own headers, a CSP it writes itself), `@decocms/nextjs` exports what the proxy, the pages and `withDeco` use: `DRAFT_PARAM` (`__draft`), `DRAFT_COOKIE` (`__deco_draft`), `DRAFT_HEADER` (`x-deco-draft`), `draftResponseHeaders(pointer)`, `STUDIO_ORIGINS`, `STUDIO_FRAME_ANCESTORS` and `isStudioOrigin(origin)`. They have the same values as `@decocms/tanstack`'s; each binding keeps its own copy and never imports the other.
257
82
 
258
- ### `NextSetupOptions` at a glance
83
+ ## next.config
259
84
 
260
- | Option | Purpose |
261
- | --- | --- |
262
- | `blocksDir` | Directory of decofile JSON snapshots (`.deco/blocks` by default), read with a plain fs scan at bootstrap. Pass `false` to skip filesystem loading entirely — the recommended setting when `blocks` carries the static-import manifest (see above). |
263
- | `blocks` | Extra/override blocks, merged **over** the directory's blocks. Pass the manifest's default export here (with `blocksDir: false`) to make it the sole block source. |
264
- | `sections` | The lazy section registry — `sectionImports` from `generate-sections --registry` (see below). |
265
- | `conventions` | `{ meta, syncComponents, loadingFallbacks }` from `sections.gen.ts` — wires the `export const sync/layout/seo/cache/eager/clientOnly` conventions (see below). |
266
- | `meta` | Lazy admin meta schema loader: `() => import("deco/meta.gen.json").then(m => m.default)`. Wire this even with a trivial schema — without it, `/deco/meta` (and its `/live/_meta` alias) 503s with `"Schema not initialized"`. |
267
- | `renderShell` | Admin preview shell config (`{ css, fonts }`). |
268
- | `previewWrapper` | Admin preview wrapper component. |
269
- | `productionOrigins`, `customMatchers`, `onResolveError`, `onDanglingReference` | Passed through to `createSiteSetup`. |
270
- | `extend` | Site-specific wiring that must run *after* core setup (section loaders, legacy SEO key shims, curated post-processing). Receives the loaded blocks. |
271
-
272
- ## 4. `package.json` scripts — non-colliding names
273
-
274
- Add three codegen scripts. **Do not name any of these `generate:schema`** —
275
- FastStore sites already own that script name for their own commerce-schema
276
- codegen, and a collision silently shadows one of the two generators
277
- depending on script-merge order. Use these names instead:
278
-
279
- ```json
280
- {
281
- "scripts": {
282
- "generate:deco-meta": "tsx node_modules/@decocms/blocks-cli/scripts/generate-schema.ts",
283
- "generate:deco-sections": "tsx node_modules/@decocms/blocks-cli/scripts/generate-sections.ts --registry",
284
- "generate:deco-blocks": "tsx node_modules/@decocms/blocks-cli/scripts/generate-blocks-manifest.ts"
285
- }
286
- }
85
+ ```js
86
+ const { withDeco } = require("@decocms/nextjs/config");
87
+ module.exports = withDeco({ images: { loader: "custom", loaderFile: "./src/image-loader.ts" } });
287
88
  ```
288
89
 
289
- Neither script needs an `--out`/`--out-file` flag — both generators default
290
- to `.deco/`, which is exactly where `src/deco/setup.ts` reads them from via
291
- the `deco/*` path alias (see above). Only pass `--out`/`--out-file` if your
292
- site has a reason to put the artifact somewhere else.
293
-
294
- - `generate:deco-meta` runs `blocks-cli`'s `generate-schema.ts`, which scans
295
- `src/sections/`, `src/loaders/`, and `src/apps/` for `Props` interfaces
296
- and emits the JSON Schema the admin's `/deco/meta` endpoint serves, to
297
- `.deco/meta.gen.json` by default.
298
- - `generate:deco-sections` runs `generate-sections.ts` **with `--registry`**
299
- — the flag that additionally emits `sectionImports`, the Next.js/webpack
300
- equivalent of Vite's `import.meta.glob("./sections/**/*.tsx")` (Next has
301
- no `import.meta.glob` or Vite plugin, so this is generated instead of
302
- computed at build time). Without `--registry` you only get
303
- `sectionMeta`/`syncComponents`/`loadingFallbacks`, not the lazy loader map
304
- `setup.ts` needs for its `sections` option. Defaults to
305
- `.deco/sections.gen.ts`.
306
- - `generate:deco-blocks` runs `generate-blocks-manifest.ts`, which emits the
307
- static-import blocks manifest (see the recommended-block-source section
308
- above) to `.deco/blocksManifest.gen.ts` by default. Regeneration is
309
- idempotent — an unchanged block set rewrites nothing, so no-op runs never
310
- tickle Next's file watcher.
311
-
312
- Run the first two any time `src/sections/` changes, and
313
- `generate:deco-blocks` any time a block file is **added or removed** in
314
- `.deco/blocks/` (content edits to existing files don't need it — the static
315
- imports pick those up by themselves). Simplest is to wire all three into a
316
- `predev`/`prebuild` `generate` chain.
90
+ `withDeco` puts v7's two draft header rules before your own (a `?__draft=` query, or the draft cookie: no-store, `Vary: Cookie`, noindex) and adds Studio's origins to `allowedDevOrigins`. Each can be turned off: `withDeco(config, { draftHeaders: false, studioOrigins: false })`. Next sets `Cache-Control` and `Vary` itself on dynamic pages, so only `X-Robots-Tag` is sure to arrive from these rules; the proxy's `applyDraft` sets all three on the responses it makes.
317
91
 
318
- ## 5. `src/sections/` — the entry-file convention
319
-
320
- Every non-test `.tsx`/`.ts` file directly under `src/sections/` (recursively)
321
- becomes a section key, keyed as `site/sections/<path-relative-to-sections-dir>`.
322
- **This is not opt-in** — `generate-sections.ts` walks the whole directory
323
- and turns every matching file into a registry entry, whether or not it
324
- carries any convention exports. Files ending in `.test.ts(x)`, `.spec.ts(x)`,
325
- `.stories.ts(x)`, or `.gen.ts(x)` are the only ones excluded.
326
-
327
- The established pattern is a **thin re-export entry file** per section, with
328
- the actual component implementation living elsewhere (e.g. alongside its
329
- own subcomponents, styles, and tests, outside `src/sections/`):
92
+ ## Images
330
93
 
331
94
  ```ts
332
- // src/sections/Hero.tsx — thin entry file, becomes "site/sections/Hero.tsx"
333
- export { default } from "../components/Hero/Hero";
334
- export type { HeroProps as Props } from "../components/Hero/Hero";
95
+ // src/image-loader.ts
96
+ import { vtexCatalogWebp, vtexFileManager } from "@decocms/apps-vtex/image"; // your platform's helpers, if any
97
+ import { createImageUrl, decoCdn } from "@decocms/blocks/image";
98
+ import { createDecoImageLoader, nextImageOptimizer } from "@decocms/nextjs/image-loader";
99
+
100
+ const imageUrl = createImageUrl({
101
+ providers: [
102
+ vtexCatalogWebp(),
103
+ vtexFileManager({ via: "query" }),
104
+ decoCdn({ quality: "high", fit: "contain", layout: "quality-last" }),
105
+ ],
106
+ });
107
+ export default createDecoImageLoader(imageUrl, { fallback: nextImageOptimizer });
335
108
  ```
336
109
 
337
- This keeps `src/sections/` a clean, flat index of exactly what's
338
- CMS-addressable, instead of a directory contest between "real" component
339
- code and registry wiring.
340
-
341
- ### Convention exports
342
-
343
- A section's entry file (or the file it re-exports from — the scanner reads
344
- the entry file's own source, so re-exported `export const` conventions must
345
- be re-declared or forwarded on the entry file itself, not just the
346
- implementation file) can opt into these, each read as a literal
347
- `export const <name> = <value>`:
348
-
349
- | Export | Effect |
350
- | --- | --- |
351
- | `export const sync = true` | Bundled synchronously (not lazy-loaded) — for above-the-fold, always-rendered sections. |
352
- | `export const layout = true` | Cached as a layout section (Header, Footer, Theme) — resolved once, not per-page. |
353
- | `export const seo = true` | SEO section — its resolved props are merged into page-level SEO. |
354
- | `export const cache = "listing"` | SWR-cached section loader results, keyed by the given cache name. |
355
- | `export const eager = true` | Prefers eager rendering (defers only past the fold threshold). |
356
- | `export const neverDefer = true` | Always eager, ignoring the fold threshold entirely. |
357
- | `export const clientOnly = true` | Skips SSR — client-only rendering. |
358
- | `export function LoadingFallback` | Skeleton component shown while the section loads. |
359
-
360
- ## Verifying your setup
110
+ The loader knows no commerce platform: the site lists its platform's helpers (`@decocms/apps-vtex/image`, `@decocms/apps-shopify/image`) before the Deco CDN, and the first provider that takes a source makes its URL. Any other source goes to `fallback` (`nextImageOptimizer` uses Next's own `/_next/image`), or comes back unchanged. With the providers above, a Next storefront's current loader URLs come out byte for byte (`tests/nextjs-image-loader.golden.test.ts` in the repository), except an already-resized Deco CDN URL in content: it is now re-resized from its `src` instead of becoming `src=image`.
361
111
 
362
- `examples/nextjs-smoke` in this monorepo is a minimal, real Next.js App
363
- Router build exercising all four surfaces end to end — `withDeco` rewrites,
364
- the catch-all route, the RSC preview page, `createNextSetup`, and a resolved
365
- page render. Its preview fixture is a real interactive Client Component, so
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.
112
+ `examples/nextjs-binding-smoke` in the repository is a complete app, and its `bun run smoke` checks the whole flow on `next start`.
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ export interface DraftPreviewIndicatorProps {
3
+ /** Where "Exit preview" goes; default: the current URL with `?__draft=off`. */
4
+ exitHref?: string;
5
+ }
6
+ export declare function DraftPreviewIndicator({ exitHref }?: DraftPreviewIndicatorProps): ReactNode;
7
+ //# sourceMappingURL=DraftPreviewIndicator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DraftPreviewIndicator.d.ts","sourceRoot":"","sources":["../src/DraftPreviewIndicator.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,MAAM,WAAW,0BAA0B;IACzC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,GAAE,0BAA+B,GAAG,SAAS,CAI9F"}
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * The "Preview mode" badge for the request's draft: a Server Component that
4
+ * renders `DraftBadge` (`@decocms/blocks/preview`) only when `bindDraft`
5
+ * bound a draft, so a published page emits nothing.
6
+ *
7
+ * Render it inside the page, after `bindDraft` was awaited, never from a
8
+ * layout: a layout renders alongside the page that fills it, so it may run
9
+ * before the page bound its draft.
10
+ */
11
+ import { DraftBadge } from "@decocms/blocks/preview";
12
+ import { activeDraftPointer } from "./server/slot.js";
13
+ export function DraftPreviewIndicator({ exitHref } = {}) {
14
+ const pointer = activeDraftPointer();
15
+ if (!pointer)
16
+ return null;
17
+ return _jsx(DraftBadge, { pointer: pointer, exitHref: exitHref });
18
+ }
19
+ //# sourceMappingURL=DraftPreviewIndicator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DraftPreviewIndicator.js","sourceRoot":"","sources":["../src/DraftPreviewIndicator.tsx"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAOtD,MAAM,UAAU,qBAAqB,CAAC,EAAE,QAAQ,EAAE,GAA+B,EAAE;IACjF,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;IACrC,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,OAAO,KAAC,UAAU,IAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAI,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,16 @@
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 { type StudioBridgeProps as BridgeScriptProps } from "@decocms/blocks/preview";
8
+ import { type ReactNode } from "react";
9
+ /** `site`, and optionally `editShortcut` and `nonce`; Studio's origins are the package's. */
10
+ export type StudioBridgeProps = Omit<BridgeScriptProps, "trustedOrigins">;
11
+ /**
12
+ * Lets Studio's site editor select and edit sections on the page when Studio
13
+ * frames it. Render it once, in the root layout's `<body>`.
14
+ */
15
+ export declare function StudioBridge(props: StudioBridgeProps): ReactNode;
16
+ //# sourceMappingURL=StudioBridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StudioBridge.d.ts","sourceRoot":"","sources":["../src/StudioBridge.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAEL,KAAK,iBAAiB,IAAI,iBAAiB,EAC5C,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGtD,6FAA6F;AAC7F,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,CAAC;AAE1E;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,SAAS,CAEhE"}
@@ -0,0 +1,17 @@
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 { StudioBridge as BridgeScript, } from "@decocms/blocks/preview";
8
+ import { createElement } from "react";
9
+ import { STUDIO_ORIGINS } from "./studio.js";
10
+ /**
11
+ * Lets Studio's site editor select and edit sections on the page when Studio
12
+ * frames it. Render it once, in the root layout's `<body>`.
13
+ */
14
+ export function StudioBridge(props) {
15
+ return createElement(BridgeScript, { ...props, trustedOrigins: STUDIO_ORIGINS });
16
+ }
17
+ //# sourceMappingURL=StudioBridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StudioBridge.js","sourceRoot":"","sources":["../src/StudioBridge.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,IAAI,YAAY,GAE7B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,aAAa,EAAkB,MAAM,OAAO,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAK7C;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,OAAO,aAAa,CAAC,YAAY,EAAE,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC,CAAC;AACnF,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The draft names and headers of a Deco Next.js site: the `?__draft=` link,
3
+ * the cookie that keeps a preview going, the request header the proxy hands
4
+ * the pages, and the response headers of a page rendered from a draft. The
5
+ * proxy reads and writes them (`./drafts.ts`, `./proxy`); the pages read the
6
+ * header (`./server`); `./config/index.cjs` repeats them (`config.test.ts`
7
+ * holds them equal). This package's own copy: it never imports
8
+ * `@decocms/tanstack`. No imports, no state: safe in every bundle.
9
+ */
10
+ /**
11
+ * The query parameter a draft link carries (`?__draft=<pointer>`, or `off` to
12
+ * end a preview). The proxy reads it after its host check; reading it
13
+ * directly would skip that check.
14
+ */
15
+ export declare const DRAFT_PARAM = "__draft";
16
+ /**
17
+ * The cookie that keeps a preview going after the first draft link. Like
18
+ * {@link DRAFT_PARAM}, the proxy reads it after its host check.
19
+ */
20
+ export declare const DRAFT_COOKIE = "__deco_draft";
21
+ /**
22
+ * The request header the proxy sets on the request it hands the pages once it
23
+ * accepted the draft: the pointer, already checked. The proxy removes any
24
+ * copy that arrives from outside, and `bindDraft` repeats the host check, so a
25
+ * client can't send one itself.
26
+ */
27
+ export declare const DRAFT_HEADER = "x-deco-draft";
28
+ /**
29
+ * The response headers of a page rendered from a draft: never stored by a
30
+ * browser or a CDN (`Cache-Control: no-store, private`, `Vary: Cookie`) and
31
+ * never indexed (`X-Robots-Tag: noindex, nofollow`). `{}` when `pointer` is
32
+ * `null`, so the result of the draft decision applies as is.
33
+ */
34
+ export declare function draftResponseHeaders(pointer: string | null | undefined): Record<string, string>;
35
+ //# sourceMappingURL=draft.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO/F"}
package/dist/draft.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The draft names and headers of a Deco Next.js site: the `?__draft=` link,
3
+ * the cookie that keeps a preview going, the request header the proxy hands
4
+ * the pages, and the response headers of a page rendered from a draft. The
5
+ * proxy reads and writes them (`./drafts.ts`, `./proxy`); the pages read the
6
+ * header (`./server`); `./config/index.cjs` repeats them (`config.test.ts`
7
+ * holds them equal). This package's own copy: it never imports
8
+ * `@decocms/tanstack`. No imports, no state: safe in every bundle.
9
+ */
10
+ /**
11
+ * The query parameter a draft link carries (`?__draft=<pointer>`, or `off` to
12
+ * end a preview). The proxy reads it after its host check; reading it
13
+ * directly would skip that check.
14
+ */
15
+ export const DRAFT_PARAM = "__draft";
16
+ /**
17
+ * The cookie that keeps a preview going after the first draft link. Like
18
+ * {@link DRAFT_PARAM}, the proxy reads it after its host check.
19
+ */
20
+ export const DRAFT_COOKIE = "__deco_draft";
21
+ /**
22
+ * The request header the proxy sets on the request it hands the pages once it
23
+ * accepted the draft: the pointer, already checked. The proxy removes any
24
+ * copy that arrives from outside, and `bindDraft` repeats the host check, so a
25
+ * client can't send one itself.
26
+ */
27
+ export const DRAFT_HEADER = "x-deco-draft";
28
+ /**
29
+ * The response headers of a page rendered from a draft: never stored by a
30
+ * browser or a CDN (`Cache-Control: no-store, private`, `Vary: Cookie`) and
31
+ * never indexed (`X-Robots-Tag: noindex, nofollow`). `{}` when `pointer` is
32
+ * `null`, so the result of the draft decision applies as is.
33
+ */
34
+ export function draftResponseHeaders(pointer) {
35
+ if (!pointer)
36
+ return {};
37
+ return {
38
+ "Cache-Control": "no-store, private",
39
+ Vary: "Cookie",
40
+ "X-Robots-Tag": "noindex, nofollow",
41
+ };
42
+ }
43
+ //# sourceMappingURL=draft.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft.js","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;AAErC;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAkC;IACrE,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,OAAO;QACL,eAAe,EAAE,mBAAmB;QACpC,IAAI,EAAE,QAAQ;QACd,cAAc,EAAE,mBAAmB;KACpC,CAAC;AACJ,CAAC"}