@decocms/nextjs 8.1.0-next.0 → 8.5.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 (89) hide show
  1. package/README.md +97 -328
  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 +37 -0
  11. package/dist/draft.d.ts.map +1 -0
  12. package/dist/draft.js +45 -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/instrumentation/index.d.ts +74 -0
  27. package/dist/instrumentation/index.d.ts.map +1 -0
  28. package/dist/instrumentation/index.js +294 -0
  29. package/dist/instrumentation/index.js.map +1 -0
  30. package/dist/proxy/index.d.ts +82 -0
  31. package/dist/proxy/index.d.ts.map +1 -0
  32. package/dist/proxy/index.js +91 -0
  33. package/dist/proxy/index.js.map +1 -0
  34. package/dist/server/index.d.ts +46 -0
  35. package/dist/server/index.d.ts.map +1 -0
  36. package/dist/server/index.js +64 -0
  37. package/dist/server/index.js.map +1 -0
  38. package/dist/server/slot.d.ts +6 -0
  39. package/dist/server/slot.d.ts.map +1 -0
  40. package/dist/server/slot.js +13 -0
  41. package/dist/server/slot.js.map +1 -0
  42. package/dist/studio.d.ts +30 -0
  43. package/dist/studio.d.ts.map +1 -0
  44. package/dist/studio.js +55 -0
  45. package/dist/studio.js.map +1 -0
  46. package/package.json +53 -22
  47. package/src/DraftPreviewIndicator.tsx +17 -16
  48. package/src/StudioBridge.ts +23 -0
  49. package/src/config/index.cjs +78 -0
  50. package/src/config/index.d.cts +27 -0
  51. package/src/draft.ts +32 -215
  52. package/src/drafts.ts +229 -0
  53. package/src/image-loader/index.ts +62 -0
  54. package/src/index.ts +31 -45
  55. package/src/instrumentation/index.ts +337 -0
  56. package/src/proxy/index.ts +164 -0
  57. package/src/server/index.ts +77 -0
  58. package/src/server/slot.ts +14 -0
  59. package/src/studio.ts +54 -0
  60. package/src/ClientOnlySection.tsx +0 -20
  61. package/src/DecoPageRenderer.test.tsx +0 -37
  62. package/src/DecoPageRenderer.tsx +0 -107
  63. package/src/DecoRootLayout.test.tsx +0 -22
  64. package/src/DecoRootLayout.tsx +0 -82
  65. package/src/DeferredSection.test.tsx +0 -27
  66. package/src/DeferredSection.tsx +0 -59
  67. package/src/DraftPreviewBadge.tsx +0 -16
  68. package/src/SectionRenderer.test.tsx +0 -97
  69. package/src/SectionRenderer.tsx +0 -58
  70. package/src/__conformance__/observability.test.ts +0 -66
  71. package/src/config.cjs +0 -92
  72. package/src/config.d.cts +0 -4
  73. package/src/config.test.ts +0 -86
  74. package/src/createDecoPage.draft.test.tsx +0 -91
  75. package/src/createDecoPage.test.tsx +0 -63
  76. package/src/createDecoPage.tsx +0 -173
  77. package/src/createDecoPreviewPage.test.tsx +0 -71
  78. package/src/createDecoPreviewPage.tsx +0 -108
  79. package/src/draft.test.ts +0 -73
  80. package/src/draftConstants.test.ts +0 -21
  81. package/src/draftConstants.ts +0 -30
  82. package/src/draftMiddleware.test.ts +0 -179
  83. package/src/draftMiddleware.ts +0 -163
  84. package/src/draftShell.test.ts +0 -76
  85. package/src/routeHandlers.test.ts +0 -243
  86. package/src/routeHandlers.ts +0 -227
  87. package/src/setup.test.ts +0 -253
  88. package/src/setup.ts +0 -180
  89. package/tsconfig.json +0 -7
package/README.md CHANGED
@@ -1,365 +1,134 @@
1
- # `@decocms/nextjs`
2
-
3
- Deco framework binding for Next.js App Router — the Next.js sibling of
4
- `@decocms/tanstack`. Four surfaces, composed together:
5
-
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`.
18
-
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
- ```
1
+ # @decocms/nextjs
28
2
 
29
- `next`, `react`, and `react-dom` are peer dependencies — the site already
30
- has them.
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`.
31
4
 
32
- ## 1. `next.config` — `withDeco`
5
+ Peers: `@decocms/blocks` (same version), `next` 16, `react` 19; optionally `@opentelemetry/api` (see [Request metric](#request-metric)).
33
6
 
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`.
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/instrumentation` | `register`, the `instrumentation.ts` hook that records `http.server.request.duration` from Next's own request span, and `serverRequestSpanProcessor()` for a site with its own OpenTelemetry SDK. |
14
+ | `@decocms/nextjs/image-loader` | `createDecoImageLoader(imageUrl)`: a next/image loader over an `ImageUrl` from `@decocms/blocks/image`. |
38
15
 
39
- TypeScript (`next.config.ts`):
16
+ `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.
40
17
 
41
- ```ts
42
- import type { NextConfig } from "next";
43
- import { withDeco } from "@decocms/nextjs/config";
18
+ ## Drafts
44
19
 
45
- const nextConfig: NextConfig = {
46
- // ...your own config
47
- };
20
+ ```ts
21
+ // src/cms.config.ts: the options both CMS handles share, and where drafts preview
22
+ export const cmsConfig = { content };
23
+ export const preview = { site: "my-store" };
24
+
25
+ // src/cms.ts and src/cms.proxy.ts
26
+ export const cms = createCMS({ blocks, ...cmsConfig });
27
+ export const proxyCms = createCMS({ blocks: {}, ...cmsConfig }); // no block map in the proxy
28
+
29
+ // src/proxy.ts
30
+ export async function proxy(request: NextRequest) {
31
+ if (isDraftRoutePath(request.nextUrl.pathname)) return new NextResponse(null, { status: 404 });
32
+ const draft = await decideDraft(proxyCms, request, preview);
33
+ const response =
34
+ rewriteToDraftRoute(request, draft) ??
35
+ NextResponse.next({ request: { headers: draftRequestHeaders(request, draft) } });
36
+ return applyDraft(response, draft);
37
+ }
48
38
 
49
- export default withDeco(nextConfig);
39
+ // src/app/%5Fdraft/[[...path]]/page.tsx: the dynamic draft route (a literal `_draft` folder never routes)
40
+ export const dynamic = "force-dynamic";
41
+ export default async function DraftPage({ params, searchParams }) {
42
+ await bindDraft(cms, { searchParams: await searchParams, ...preview });
43
+ const client = contentClient(cms);
44
+ // … resolve and render the page as on the published route …
45
+ return <>{page}<DraftPreviewIndicator /></>;
46
+ }
50
47
  ```
51
48
 
52
- CommonJS (`next.config.js`) — most Next.js sites still use this form:
49
+ 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.
53
50
 
54
- ```js
55
- /** @type {import('next').NextConfig} */
56
- const { withDeco } = require("@decocms/nextjs/config");
51
+ ### Draft hosts
57
52
 
58
- const nextConfig = {
59
- // ...your own config
60
- };
53
+ 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.
61
54
 
62
- module.exports = withDeco(nextConfig);
63
- ```
55
+ ## The layout
64
56
 
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`.
57
+ ```tsx
58
+ import { StudioBridge } from "@decocms/nextjs";
69
59
 
70
- ## 2. The catch-all route
60
+ <body>
61
+ {children}
62
+ <StudioBridge site="my-store" />
63
+ </body>
64
+ ```
71
65
 
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.
66
+ 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.
76
67
 
77
- ```ts
78
- // src/app/deco/[[...deco]]/route.ts
79
- import { createDecoRouteHandlers } from "@decocms/nextjs/routeHandlers";
80
- import { ensureSetup } from "../../../deco/setup";
68
+ 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:
81
69
 
82
- export const dynamic = "force-dynamic";
70
+ ```tsx
71
+ import { StudioPage } from "@decocms/nextjs";
83
72
 
84
- export const { GET, POST, OPTIONS } = createDecoRouteHandlers({
85
- setup: ensureSetup,
86
- });
73
+ const match = matchRoute(pathname, { routes: pages });
74
+ // …
75
+ return <>{sections}<StudioPage pathTemplate={match.entry.path} /></>;
87
76
  ```
88
77
 
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).
78
+ 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.
92
79
 
93
- ### The RSC preview page
80
+ ## Names and Studio's origins
94
81
 
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.
82
+ 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.
100
83
 
101
- ```tsx
102
- // src/app/deco/preview/[[...path]]/page.tsx
103
- import { createDecoPreviewPage } from "@decocms/nextjs";
104
- import { ensureSetup } from "../../../../deco/setup";
84
+ ## next.config
105
85
 
106
- export const dynamic = "force-dynamic";
107
-
108
- export default createDecoPreviewPage({ setup: ensureSetup });
86
+ ```js
87
+ const { withDeco } = require("@decocms/nextjs/config");
88
+ module.exports = withDeco({ images: { loader: "custom", loaderFile: "./src/image-loader.ts" } });
109
89
  ```
110
90
 
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`
152
-
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:
159
-
160
- ```json
161
- // tsconfig.json
162
- {
163
- "compilerOptions": {
164
- "paths": {
165
- "deco/*": [".deco/*"]
166
- }
167
- }
168
- }
169
- ```
91
+ `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.
92
+
93
+ ## Request metric
170
94
 
171
95
  ```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";
176
-
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
- });
96
+ // src/instrumentation.ts (or instrumentation.ts at the root, next to app/)
97
+ export { register } from "@decocms/nextjs/instrumentation";
184
98
  ```
185
99
 
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";
245
-
246
- export default async function RootLayout({ children }: { children: React.ReactNode }) {
247
- await ensureSetup();
248
- return <DecoRootLayout siteName="my-site">{children}</DecoRootLayout>;
249
- }
250
- ```
251
-
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.
257
-
258
- ### `NextSetupOptions` at a glance
259
-
260
- | Option | Purpose |
100
+ Next.js already measures every request it serves: one root span per request (`BaseServer.handleRequest`) that ends with the status, the method and the route's template. `register` gives Next a tracer provider whose only job is that span: when it ends, the request goes to the CMS's telemetry as `http.server.request.duration` (through `recordServerRequest` from `@decocms/blocks/metrics`) (seconds, the same buckets as the upstream clients' `http.client.request.duration`), next to everything else `createCMS` sends. No span is exported anywhere, and every other span Next asks for is a shared non-recording one. With no telemetry destination (no `token` and no `endpoint`) nothing is recorded.
101
+
102
+ | Label | Value |
261
103
  | --- | --- |
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
- }
287
- ```
104
+ | `http.route` | Next's route template: `/[[...path]]`, `/blog`, `/_draft/[[...path]]`. An RSC request counts under its page's route. `unmatched` when Next names none (an Edge route under `next start`) |
105
+ | `http.request.method` | `GET`, `POST`, …; any other is `_OTHER` |
106
+ | `status_class` | `2xx` … `5xx`; a request that threw is `5xx` |
107
+ | `cache` | `DRAFT` for a draft render (the proxy forwarded `x-deco-draft`, or the request was rewritten onto `/_draft`), otherwise `BYPASS`: Next decides its own cache (`x-nextjs-cache`) on the response, which the span never sees |
108
+
109
+ - `@opentelemetry/api` is an optional peer: `register` uses the same copy Next does (yours when installed, else Next's compiled one) and does nothing if neither loads.
110
+ - **Your own OpenTelemetry setup** (`@vercel/otel`, `NodeSDK`): only one tracer provider can be registered, so `register` leaves yours alone and logs how to combine them. Add the processor to yours instead: `registerOTel({ serviceName, spanProcessors: ["auto", serverRequestSpanProcessor()] })`.
111
+ - **The proxy**: under `next start` Next runs the proxy through a request span of its own; that one is not counted, and the request the proxy hands on is. A response the proxy makes itself (a redirect, a 404 for a direct `/_draft` hit) never reaches a page and is not counted.
112
+ - **Edge runtime**: `register` does nothing there. Under `next start` an Edge route is still measured by the Node.js server that serves it (`unmatched`, no template); on a host that runs Edge functions on their own, nothing measures them.
288
113
 
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.
317
-
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/`):
114
+ ## Images
330
115
 
331
116
  ```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";
117
+ // src/image-loader.ts
118
+ import { vtexCatalogWebp, vtexFileManager } from "@decocms/apps-vtex/image"; // your platform's helpers, if any
119
+ import { createImageUrl, decoCdn } from "@decocms/blocks/image";
120
+ import { createDecoImageLoader, nextImageOptimizer } from "@decocms/nextjs/image-loader";
121
+
122
+ const imageUrl = createImageUrl({
123
+ providers: [
124
+ vtexCatalogWebp(),
125
+ vtexFileManager({ via: "query" }),
126
+ decoCdn({ quality: "high", fit: "contain", layout: "quality-last" }),
127
+ ],
128
+ });
129
+ export default createDecoImageLoader(imageUrl, { fallback: nextImageOptimizer });
335
130
  ```
336
131
 
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
132
+ 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`.
342
133
 
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
361
-
362
- No example app in this monorepo exercises these surfaces any more:
363
- `examples/nextjs-smoke` now follows the next-major Next.js guide, which uses
364
- `@decocms/blocks` alone. The surfaces above are covered by this package's unit
365
- tests.
134
+ `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,37 @@
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
+ /** The URL prefix a drafted request is rewritten onto (`app/%5Fdraft/[[...path]]`). */
29
+ export declare const DRAFT_ROUTE_PREFIX = "/_draft";
30
+ /**
31
+ * The response headers of a page rendered from a draft: never stored by a
32
+ * browser or a CDN (`Cache-Control: no-store, private`, `Vary: Cookie`) and
33
+ * never indexed (`X-Robots-Tag: noindex, nofollow`). `{}` when `pointer` is
34
+ * `null`, so the result of the draft decision applies as is.
35
+ */
36
+ export declare function draftResponseHeaders(pointer: string | null | undefined): Record<string, string>;
37
+ //# 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,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO/F"}