@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.
- package/README.md +97 -328
- package/dist/DraftPreviewIndicator.d.ts +7 -0
- package/dist/DraftPreviewIndicator.d.ts.map +1 -0
- package/dist/DraftPreviewIndicator.js +19 -0
- package/dist/DraftPreviewIndicator.js.map +1 -0
- package/dist/StudioBridge.d.ts +16 -0
- package/dist/StudioBridge.d.ts.map +1 -0
- package/dist/StudioBridge.js +17 -0
- package/dist/StudioBridge.js.map +1 -0
- package/dist/draft.d.ts +37 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +45 -0
- package/dist/draft.js.map +1 -0
- package/dist/drafts.d.ts +94 -0
- package/dist/drafts.d.ts.map +1 -0
- package/dist/drafts.js +210 -0
- package/dist/drafts.js.map +1 -0
- package/dist/image-loader/index.d.ts +36 -0
- package/dist/image-loader/index.d.ts.map +1 -0
- package/dist/image-loader/index.js +27 -0
- package/dist/image-loader/index.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation/index.d.ts +74 -0
- package/dist/instrumentation/index.d.ts.map +1 -0
- package/dist/instrumentation/index.js +294 -0
- package/dist/instrumentation/index.js.map +1 -0
- package/dist/proxy/index.d.ts +82 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +91 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/server/index.d.ts +46 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +64 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/slot.d.ts +6 -0
- package/dist/server/slot.d.ts.map +1 -0
- package/dist/server/slot.js +13 -0
- package/dist/server/slot.js.map +1 -0
- package/dist/studio.d.ts +30 -0
- package/dist/studio.d.ts.map +1 -0
- package/dist/studio.js +55 -0
- package/dist/studio.js.map +1 -0
- package/package.json +53 -22
- package/src/DraftPreviewIndicator.tsx +17 -16
- package/src/StudioBridge.ts +23 -0
- package/src/config/index.cjs +78 -0
- package/src/config/index.d.cts +27 -0
- package/src/draft.ts +32 -215
- package/src/drafts.ts +229 -0
- package/src/image-loader/index.ts +62 -0
- package/src/index.ts +31 -45
- package/src/instrumentation/index.ts +337 -0
- package/src/proxy/index.ts +164 -0
- package/src/server/index.ts +77 -0
- package/src/server/slot.ts +14 -0
- package/src/studio.ts +54 -0
- package/src/ClientOnlySection.tsx +0 -20
- package/src/DecoPageRenderer.test.tsx +0 -37
- package/src/DecoPageRenderer.tsx +0 -107
- package/src/DecoRootLayout.test.tsx +0 -22
- package/src/DecoRootLayout.tsx +0 -82
- package/src/DeferredSection.test.tsx +0 -27
- package/src/DeferredSection.tsx +0 -59
- package/src/DraftPreviewBadge.tsx +0 -16
- package/src/SectionRenderer.test.tsx +0 -97
- package/src/SectionRenderer.tsx +0 -58
- package/src/__conformance__/observability.test.ts +0 -66
- package/src/config.cjs +0 -92
- package/src/config.d.cts +0 -4
- package/src/config.test.ts +0 -86
- package/src/createDecoPage.draft.test.tsx +0 -91
- package/src/createDecoPage.test.tsx +0 -63
- package/src/createDecoPage.tsx +0 -173
- package/src/createDecoPreviewPage.test.tsx +0 -71
- package/src/createDecoPreviewPage.tsx +0 -108
- package/src/draft.test.ts +0 -73
- package/src/draftConstants.test.ts +0 -21
- package/src/draftConstants.ts +0 -30
- package/src/draftMiddleware.test.ts +0 -179
- package/src/draftMiddleware.ts +0 -163
- package/src/draftShell.test.ts +0 -76
- package/src/routeHandlers.test.ts +0 -243
- package/src/routeHandlers.ts +0 -227
- package/src/setup.test.ts +0 -253
- package/src/setup.ts +0 -180
- package/tsconfig.json +0 -7
package/README.md
CHANGED
|
@@ -1,365 +1,134 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
5
|
+
Peers: `@decocms/blocks` (same version), `next` 16, `react` 19; optionally `@opentelemetry/api` (see [Request metric](#request-metric)).
|
|
33
6
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
import type { NextConfig } from "next";
|
|
43
|
-
import { withDeco } from "@decocms/nextjs/config";
|
|
18
|
+
## Drafts
|
|
44
19
|
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
/** @type {import('next').NextConfig} */
|
|
56
|
-
const { withDeco } = require("@decocms/nextjs/config");
|
|
51
|
+
### Draft hosts
|
|
57
52
|
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
```
|
|
55
|
+
## The layout
|
|
64
56
|
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
60
|
+
<body>
|
|
61
|
+
{children}
|
|
62
|
+
<StudioBridge site="my-store" />
|
|
63
|
+
</body>
|
|
64
|
+
```
|
|
71
65
|
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
```tsx
|
|
71
|
+
import { StudioPage } from "@decocms/nextjs";
|
|
83
72
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
73
|
+
const match = matchRoute(pathname, { routes: pages });
|
|
74
|
+
// …
|
|
75
|
+
return <>{sections}<StudioPage pathTemplate={match.entry.path} /></>;
|
|
87
76
|
```
|
|
88
77
|
|
|
89
|
-
|
|
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
|
-
|
|
80
|
+
## Names and Studio's origins
|
|
94
81
|
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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/
|
|
173
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
| `
|
|
263
|
-
| `
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
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/
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"}
|
package/dist/draft.d.ts
ADDED
|
@@ -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"}
|