@avocadostudio-ai/skills 0.21.0 → 0.22.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/NOTICE ADDED
@@ -0,0 +1,8 @@
1
+ Avocado Studio
2
+ Copyright 2026 Avocado Studio Contributors
3
+
4
+ This product includes software developed by the Avocado Studio
5
+ Contributors (https://www.avocadostudio.dev).
6
+
7
+ Licensed under the Apache License, Version 2.0. See LICENSE for the
8
+ full license text.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avocadostudio-ai/skills",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Install Avocado Studio's agent skills into a project, so a coding agent reads instructions that match the version you have",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,7 +11,9 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "skills",
14
- "README.md"
14
+ "README.md",
15
+ "LICENSE",
16
+ "NOTICE"
15
17
  ],
16
18
  "keywords": [
17
19
  "avocado",
@@ -17,14 +17,18 @@ every path.
17
17
 
18
18
  | The user has | Load |
19
19
  |---|---|
20
- | A Next.js app that already exists, with its own components and content | `avocado-integrate` |
20
+ | A Next.js site that already exists, with its own components and content | `avocado-integrate` — its Next.js branch |
21
+ | An Astro site that already exists | `avocado-integrate` — its Astro branch |
22
+ | A Next.js or Astro site whose copy is written into its templates rather than held in a CMS or data files | `avocado-integrate`, plus the file-backed recipe it links: https://docs.avocadostudio.dev/integration/file-backed-sites |
21
23
  | Nothing yet, or wants to see it working before committing | `avocado-demo` |
22
24
  | Either, and you are now declaring their components as editable blocks | `avocado-blocks` |
23
25
  | Either, and the content those blocks render comes from a CMS | `avocado-cms` |
24
26
 
25
- Before deciding, if the site is live, `npx avocado-scope <url>` will tell you
26
- what one of its pages would become as blocks without installing or writing
27
- anything. It is read-only, deterministic and free.
27
+ Before deciding, `npx -p @avocadostudio-ai/migration-sdk avocado-scope <url>`
28
+ will tell you what one of the site's pages would become as blocks without
29
+ installing or writing anything — add `--allow-localhost` for a site on a local
30
+ dev server. It is read-only, deterministic and free. The command lives in
31
+ `migration-sdk`, so a bare `npx avocado-scope` is a 404 from the registry.
28
32
 
29
33
  If it is ambiguous, ask one question: *"Is this going onto a site you already
30
34
  have, or do you want a demo first?"* Do not guess — the two paths write
@@ -47,9 +51,16 @@ edit go through without telling the user.
47
51
 
48
52
  - **Install with the package manager the project already uses.** The lockfile
49
53
  says which. Do not introduce a second one.
50
- - **Import only from `@avocadostudio-ai/site-sdk`.** `registerBlock` and `z`
51
- come from `@avocadostudio-ai/site-sdk/blocks`; the attribute helpers from
52
- `@avocadostudio-ai/site-sdk/markers`. Never import `@avocadostudio-ai/shared`,
54
+ - **Import only from `@avocadostudio-ai/site-sdk`** — and, on Astro, from
55
+ `@avocadostudio-ai/astro`. The types (`PageDoc`, `BlockInstance`,
56
+ `SiteConfig`) come from the SDK's root as `import type { PageDoc } from
57
+ "@avocadostudio-ai/site-sdk"`; `registerBlock` and `z` from
58
+ `@avocadostudio-ai/site-sdk/blocks`. The attribute helpers come from
59
+ `@avocadostudio-ai/site-sdk/markers` on React and Next, and from
60
+ `@avocadostudio-ai/astro/markers` on Astro: `editorMarkers(Astro)` there
61
+ emits Astro's `class` and a style string where the React helpers emit
62
+ `className` and a style object, which spread onto an `.astro` element as
63
+ attributes nothing matches. Never import `@avocadostudio-ai/shared`,
53
64
  `@avocadostudio-ai/blocks`, `@avocadostudio-ai/preview-adapter` or a bare
54
65
  `zod` from the user's source. Under pnpm they will not resolve; under npm's
55
66
  flat hoisting they resolve today and break the first time something
@@ -67,13 +78,17 @@ edit go through without telling the user.
67
78
  - **`PUBLISH_TOKEN` is not optional in production.** `/api/editor/publish`
68
79
  overwrites the site's content. With no `publishSecret` configured it answers
69
80
  401 and names the variable rather than running open.
70
- - **Wrap the Next config.** `withAvocado` from
81
+ - **On Next, wrap the Next config.** `withAvocado` from
71
82
  `@avocadostudio-ai/site-sdk/next-config` sets `transpilePackages`,
72
83
  `serverExternalPackages`, the matching server externals and
73
84
  `skipTrailingSlashRedirect` together. Setting one of them by hand looks right
74
- and fails quietly on the native dependencies.
85
+ and fails quietly on the native dependencies. Wrap the exported value as the
86
+ file's last expression — `export default withAvocado(existingConfig)` — and
87
+ leave the config body as it was, so the diff stays reviewable.
75
88
  - **Finish on a number, not on "it builds."** Every path ends with a
76
- verification step that produces a count. Report it.
89
+ verification step that produces a count. Report it. An integration of an
90
+ existing site is not done until `npx avocado qa` passes and the manual pass
91
+ in `avocado-integrate` is checked off.
77
92
 
78
93
  ## Versions
79
94
 
@@ -72,7 +72,8 @@ export const { GET, POST, OPTIONS } = createEditorApiHandler({
72
72
  | **A string of markup** rendered with `dangerouslySetInnerHTML` | `html` |
73
73
  | An image path or URL | `image` |
74
74
  | A list of plain strings | `stringList` |
75
- | A list of images | `imageList` |
75
+ | A list of images, each only an image and its alt text | `imageList` — rows are `{ image, alt }` |
76
+ | Rows that are an image plus anything else — a caption, a link | a list field: `listFields` with `itemFields` |
76
77
 
77
78
  `richtext` and `html` are the one people get wrong. `richtext` means a
78
79
  *document*. Declare a markup string as `richtext` and the panel renders the tags
@@ -83,6 +84,56 @@ editor and preserves the elements and attributes it cannot model.
83
84
  Declare presentation props — variants, spacing, feature flags — **nowhere**. If
84
85
  it is not content, leaving it out is the point.
85
86
 
87
+ ## What the site cannot store: `fixed` and `readOnly`
88
+
89
+ Declare them, or the editor offers controls that can only end in a refused
90
+ publish.
91
+
92
+ - **`fixed: true`** in the block's `meta` — for a section that is a **slice of
93
+ one CMS entry rendered in a fixed order** (a blog post's hero, body and
94
+ related-articles grid are three blocks and one entry). The editor hides move,
95
+ delete and add on it, and the ops engine refuses to move, remove or duplicate
96
+ it — for chat and MCP too. Its fields stay editable.
97
+ - **`readOnly: true`** plus **`readOnlyReason`** on a field — for **asset alt
98
+ text shared across entries**, **slugs**, **dates**: anything the publisher
99
+ refuses to write. The panel shows it disabled with the reason; an edit is
100
+ refused with the reason.
101
+
102
+ ```ts
103
+ meta: {
104
+ displayName: "Article hero",
105
+ fixed: true,
106
+ fields: {
107
+ imageAlt: { kind: "imageAlt", readOnly: true, readOnlyReason: "It is the asset's title, shared by every entry." },
108
+ slug: { kind: "text", readOnly: true },
109
+ },
110
+ }
111
+ ```
112
+
113
+ ## Site-wide content: `shared`
114
+
115
+ A header, footer, business details or a closing CTA that every page carries is
116
+ **one block with `shared: true`** in its `meta` (or on its field-table spec),
117
+ placed on every page under **the same block id**. Do not model it as a
118
+ separate block per page, and do not leave it inline in the layout.
119
+
120
+ - An edit on any page is applied to every page holding that id, in the same
121
+ step; undo reverts it everywhere; the publish review lists it once as
122
+ "Footer — affects 5 pages". The planner is told it is site-wide.
123
+ - Adding, moving and removing stay per page.
124
+ - Identity is the id plus the type. A page-owned instance of the same type
125
+ needs an id no other page uses, or it becomes part of the shared content.
126
+
127
+ ```ts
128
+ meta: {
129
+ displayName: "Footer",
130
+ shared: true,
131
+ fields: { copyright: { kind: "text" } },
132
+ }
133
+ ```
134
+
135
+ `avocado-integrate` covers injecting it into every page and writing it back once.
136
+
86
137
  ## Names that collide with the built-ins
87
138
 
88
139
  Avocado ships twenty built-in types: `Hero`, `FeatureGrid`, `Testimonials`,
@@ -106,6 +157,12 @@ only once the markup says where it is. Three attributes do that, all from
106
157
  `@avocadostudio-ai/site-sdk/markers` — never from `/editor`, see the rule in the
107
158
  `avocado` skill about the public bundle.
108
159
 
160
+ **On Astro, take them from `@avocadostudio-ai/astro/markers` instead:**
161
+ `const { block, field, scope } = editorMarkers(Astro)` gives the same three in
162
+ Astro's spelling (`class`, a style string), reads the editor flag off the
163
+ component's own `Astro`, and returns `{}` on a visitor's render. The rules below
164
+ are the same; only the spelling of the calls differs.
165
+
109
166
  ### The block boundary comes first
110
167
 
111
168
  ```tsx
@@ -212,7 +269,10 @@ import {
212
269
  ```
213
270
 
214
271
  - **`editableCoverage`** reports `marked/expected` — of the fields the manifest
215
- declares, how many the rendered page actually carries a marker for.
272
+ declares, how many the rendered page actually carries a marker for. Read the
273
+ page with `extractMarkedBlocks(html, { blocks: page.blocks })`: the page's
274
+ blocks attach each block's props, and without them a field that is empty on
275
+ every page is reported as a gap nobody can close.
216
276
  - **`panelCoverage`** reports `rowsLabelled/rowsExamined` plus findings: list
217
277
  rows nobody can tell apart, polymorphic branches that never narrow, props in
218
278
  the content that nothing describes, and type names colliding with the
@@ -222,11 +282,18 @@ import {
222
282
  you cannot reach it, do not quietly stop — list every remaining gap with the
223
283
  block type, the field path and why.
224
284
 
225
- Markers are emitted only on an **editor render**. Run the check against
226
- `next dev` through the editor path: pointed at a production build it finds no
227
- blocks and reports zero, which reads exactly like an integration that marks
228
- nothing. When building the page URL, use `new URL(page.slug, origin)` — a slug
229
- already starts with `/`, and string-joining it onto the origin fetches
285
+ Markers are emitted only on an **editor render**. Run the check against the
286
+ dev server — `next dev`, `astro dev` — through the editor path
287
+ (`?__editor=1&siteId=…`): pointed at a production build it finds no blocks and
288
+ reports zero, which reads exactly like an integration that marks nothing.
289
+
290
+ **Pass the page's blocks to `extractMarkedBlocks`** —
291
+ `extractMarkedBlocks(html, { blocks: page.blocks })`, with the page from
292
+ `GET /api/editor/pages` — so each marked block carries its `props`. Without them an optional field that is empty on this page is still
293
+ expected to carry a marker, nothing drew it, and the gap can never close.
294
+
295
+ When building the page URL, use `new URL(page.slug, origin)` — a slug already
296
+ starts with `/`, and string-joining it onto the origin fetches
230
297
  `http://localhost:3000//` and measures nothing.
231
298
 
232
299
  Cross-check the manifest directly too — `GET /api/editor/blocks` should list
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: avocado-cms
3
- description: Make a CMS-backed site editable in Avocado Studio — the field table, the lens that projects documents into block props and merges edits back, the Storyblok and Sanity primitive packs, and the write rules that stop a publish corrupting content. Use when the site's content lives in Sanity, Storyblok, Contentful, Strapi or another headless CMS rather than in files.
3
+ description: Make a CMS-backed site editable in Avocado Studio — the field table, the lens that projects documents into block props and merges edits back, the Storyblok, Sanity and Contentful primitive packs, the read-only fixture to develop against, and the write rules that stop a publish corrupting content. Use when the site's content lives in Sanity, Storyblok, Contentful, Strapi or another headless CMS rather than in files.
4
4
  ---
5
5
 
6
6
  # Editing a CMS through Avocado
@@ -62,6 +62,15 @@ export const TABLE: FieldTable = {
62
62
  because the merge patches the source document rather than replacing it. That
63
63
  is the lever for scope: declare what an editor should change, leave the layout
64
64
  and behaviour switches out.
65
+ - **`fixed: true`** on a block spec marks a section that is a slice of one
66
+ entry, drawn by the template in a fixed order. Declare **one block per
67
+ rendered section** even when several share an entry, and mark those fixed:
68
+ the editor then hides move/delete/add on them and the ops engine refuses the
69
+ change, instead of the publish refusing it later.
70
+ - **`readOnly: true`** with a **`readOnlyReason`** marks a value the merge must
71
+ not write — a slug, a date. For an image whose alt text is the shared asset's
72
+ title (Contentful), use `alt: { readOnly: true, readOnlyReason }` on the image
73
+ field: the image stays swappable, the alt is shown disabled with the reason.
65
74
  - **`localized: false`** marks a field that has one value for every language —
66
75
  an anchor, a slug fragment, an icon name. It is not decoration: without it the
67
76
  lens looks for a per-language value and writes one.
@@ -84,7 +93,13 @@ registerLens(lens)
84
93
  ```
85
94
 
86
95
  Sanity is the same with `sanityPrimitives()` and `sanityLocale(…)` from
87
- `@avocadostudio-ai/site-sdk/lens/sanity`.
96
+ `@avocadostudio-ai/site-sdk/lens/sanity`. Contentful uses `contentfulPrimitives()`
97
+ and `contentfulLocale(…)` from `@avocadostudio-ai/site-sdk/lens/contentful`, and
98
+ needs more than a lens: read section 8 before writing any of it.
99
+
100
+ The rich-text converters (`fromContentful`, `toPortableText`, `fromStoryblok`, …)
101
+ are re-exported from `@avocadostudio-ai/site-sdk/lens` and from each pack. Do not
102
+ add `@avocadostudio-ai/richtext` as a direct dependency just for them.
88
103
 
89
104
  **Prefer `registerLens(lens)`.** The older two-call form —
90
105
  `registerFieldTable(TABLE, { primitives })` beside `createLens({ …, primitives })`
@@ -187,6 +202,111 @@ right slot, and never let two languages of the same document share a page id.
187
202
  Read `https://docs.avocadostudio.dev/integration/multilingual` before writing a
188
203
  single CMS write path. A wrong projection corrupts content invisibly.
189
204
 
205
+ ## 7. Develop against a read-only fixture
206
+
207
+ Credentials usually arrive after the work starts. Do not wait for them, and do
208
+ not point a half-written write path at a real space to find out whether it works.
209
+ Give the adapter a **read-only fixture source** behind one env var: the CMS's own
210
+ export format (a Contentful `space export`, a Sanity `dataset export` NDJSON, a
211
+ Storyblok stories dump), read in place.
212
+
213
+ ```ts
214
+ export const source = process.env.CONTENTFUL_EXPORT_FILE
215
+ ? contentfulExportSource(async () => JSON.parse(await readFile(process.env.CONTENTFUL_EXPORT_FILE!, "utf8")))
216
+ : createContentfulDelivery({ spaceId, accessToken, preview: true })
217
+ ```
218
+
219
+ It must have **no write path**. A fixture that accepts a publish lets a broken
220
+ publish look like it worked. On one integration this fixture reached 100%
221
+ `editableCoverage` and a clean `roundTrip` on every entry before any space
222
+ existed. Everything in section 4 and in Verify can run against it. The only
223
+ thing it cannot prove is the publish itself.
224
+
225
+ A starter template's own export (`export.json`, `seed.ndjson`) is the usual first
226
+ fixture. Replace it with an export of the real space as soon as there is one:
227
+ fixtures are how the bugs in section 4 got shipped.
228
+
229
+ ## 8. Contentful
230
+
231
+ Use the pack; do not write it again. Two integrations each wrote the same
232
+ ~190 lines before it existed. `@avocadostudio-ai/site-sdk/lens/contentful`
233
+ provides:
234
+
235
+ - `contentfulPrimitives()` and `contentfulLocale(default, locales, { localized, contentTypeOf })`.
236
+ - `localizedFields(contentTypes)`: which fields are localised, read from the space's
237
+ `/content_types` **at runtime**. Do not set `localized` in a Contentful table.
238
+ That would be a second copy of the content model, and it goes wrong the day
239
+ someone toggles localisation in the web app.
240
+ - `readEntry(entry, { defaultLocale, localized, includes })`: a `locale=*` entry as a
241
+ lens document. Non-localised fields (which Contentful stores under the default
242
+ locale) are unwrapped to the bare key, and Links are annotated with the asset
243
+ URL and title or the target's slug.
244
+ - `entryPatch(source, merged, { defaultLocale, localized, contentType })`: only
245
+ the locale slots that changed, wrapped again.
246
+ - `createContentfulDelivery({ …, preview })` and `contentfulExportSource(json)`
247
+ for reads, both always `locale=*`. `createContentfulManagement(…).publishEntries(writes, opts)`
248
+ for writes: it fetches the live entry, applies the patch on top of it, sends an
249
+ update only if something differs, and publishes.
250
+
251
+ What the codecs do:
252
+
253
+ - **Images** project the asset's URL and title. A new URL is written as an upload
254
+ sentinel, which the publisher turns into a new asset and a Link. **Alt text is
255
+ the asset title, shared by every entry that uses the asset**, so an alt edit on
256
+ its own is refused with a warning. Say so to the user; do not work around it.
257
+ - **References** project the target's slug. A new slug is a lookup sentinel. At
258
+ publish it resolves to a Link, or the whole publish is refused if no published
259
+ entry has that slug.
260
+ - **Rich text** goes through `fromContentful` / `toContentful`, and "unchanged" is
261
+ judged against the stored value's own canonical round trip. Without that, every
262
+ publish rewrites every body.
263
+
264
+ **Publishing in Contentful is per entry, not per field.** `PUT …/published`
265
+ makes the entry's whole current draft live, including anything a person has
266
+ saved in the web app and not yet published. `publishEntries` therefore
267
+ **refuses the whole publish, before writing anything**, when any entry in it has
268
+ unpublished changes (`onUnpublishedChanges: "refuse"`, the default). `"draft"`
269
+ writes into the existing draft without publishing it. `"publish"` takes the
270
+ pending changes live, and is only for a space where Avocado is the only writer.
271
+ Never switch the policy yourself; ask the user and tell them what it means.
272
+
273
+ Content-model and seeding traps, each of which has broken a real integration:
274
+
275
+ - **Limit embedded entries in rich text.** Put `size: { max: 10 }` (or another
276
+ realistic number) on every rich-text field's `embedded-entry-block` validation.
277
+ Without it, Contentful's GraphQL API prices the field at the maximum. On one
278
+ site every post page returned 500 `TOO_COMPLEX_QUERY` (cost 101,700 against a
279
+ limit of 11,000) while the home page rendered fine.
280
+ - **Seed in two passes.** An entry cannot be published while it links to
281
+ unpublished entries. Create and publish the linked entries first, then add the
282
+ links (related posts, authors) in a second pass and publish again.
283
+ - **Check that the space holds the content model** the site queries. Some
284
+ templates ship without one, because Contentful's sign-up flow creates it.
285
+ - **Render every page after seeding, not just the home page.** Both the query-cost
286
+ failure and a missing content type show up only on the pages that use them.
287
+
288
+ Map **one block per rendered section**, even when the sections are slices of one
289
+ entry: a blog post is `articleHero` + `articleBody`, not one `pageBlogPost` block.
290
+ Give `contentfulLocale` a `contentTypeOf` for the section types, and merge every
291
+ section of a page into the same document before taking one `entryPatch`.
292
+
293
+ **Preview by overlaying, not by re-rendering.** Keep the site's own data layer:
294
+ the public route runs its own query, then calls
295
+ `applyDraftBlocks(siteData, draft?.blocks, OVERLAY)` from
296
+ `@avocadostudio-ai/site-sdk/lens`. `OVERLAY` maps each block prop to its path in
297
+ that object (`featuredImageUrl: "featuredImage.url"`,
298
+ `body: { path: "content.json", to: toContentful }`). With no draft, which is
299
+ every public render, the call returns its input unchanged. Do not write a
300
+ second preview route that re-implements the page composition, because it drifts
301
+ from the public one. Do not rewrite the templates to read projected props
302
+ either, because that orphans the site's data layer. Either one needs the user's
303
+ say-so. The overlay works for any CMS, not only Contentful.
304
+
305
+ Locales: one page per entry × locale, following
306
+ `https://docs.avocadostudio.dev/integration/multilingual`. The full Contentful
307
+ walkthrough, including a complete publish handler, is
308
+ `https://docs.avocadostudio.dev/integration/contentful`.
309
+
190
310
  ## Verify
191
311
 
192
312
  Report all of these as numbers, not as "it works":
@@ -200,6 +320,8 @@ Report all of these as numbers, not as "it works":
200
320
  its own inverse.
201
321
  4. The coverage figures from `avocado-blocks` — the table gives the manifest,
202
322
  the renderers still have to carry the markers.
323
+ 5. On Contentful, whether `publishEntries` refused, and which entries it named.
324
+ A refusal there is the policy working, not a bug to route around.
203
325
 
204
326
  ## What not to do
205
327
 
@@ -210,3 +332,7 @@ Report all of these as numbers, not as "it works":
210
332
  - Do not skip `roundTrip` because the fixtures pass. Fixtures are how these bugs
211
333
  got shipped.
212
334
  - Do not report success on a publish diff nobody read.
335
+ - Do not set `onUnpublishedChanges: "publish"` without the user's say-so. It
336
+ takes other people's drafts live.
337
+ - Do not declare `localized` in a Contentful table; read `localizedFields` from
338
+ the space.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: avocado-integrate
3
- description: Wire Avocado Studio into a Next.js site that already exists — its own components, its own content or CMS, its own routes. Use when adding chat-driven editing to a real site rather than scaffolding a demo.
3
+ description: Wire Avocado Studio into a Next.js or Astro site that already exists — its own components, its own content or CMS, its own routes. Use when adding chat-driven editing to a real site rather than scaffolding a demo, on Next.js 15–16 (App Router) or Astro 5+.
4
4
  ---
5
5
 
6
6
  # Adding Avocado to a site that already exists
@@ -11,50 +11,161 @@ wrote without saying so.**
11
11
 
12
12
  Work on a branch. Produce a diff the user reviews.
13
13
 
14
- ## 1. Survey, and report before touching anything
14
+ The steps are the same for both frameworks. Where they differ, a step has a
15
+ **Next.js** and an **Astro** branch; follow the one step 1 found and skip the
16
+ other.
15
17
 
16
- Answer these from the repo, not from assumption, and tell the user the answers:
18
+ ## 1. Survey, and report before touching anything
17
19
 
18
- - **Next version and router.** 15 or 16; App Router or Pages. Pages Router is
19
- not supported — stop and say so.
20
- - **Where content lives.** A JSON file, a local CMS module, Contentful, Sanity,
20
+ Answer these from the repo, not from assumption, and tell the user the answers.
21
+
22
+ **The toolchain — record it before the first install.**
23
+
24
+ - **Framework and version.** Next.js 15 or 16 on the App Router, or Astro 5+.
25
+ Pages Router is not supported — stop and say so. An older major (Next 14,
26
+ Astro 3/4) is an upgrade *before* this job, not during it; an archived
27
+ starter template is the same, plus nobody upstream to ask. Say which, and
28
+ how big the upgrade looks.
29
+ - **Node version** (`node -v`) against the project's `engines` field.
30
+ - **Package manager**, from the lockfile, and **corepack state**: a
31
+ `packageManager` field here or in a parent directory's `package.json` pins
32
+ one, and with corepack strict every other manager's command fails. Write down
33
+ the exact install command that works *before* running it, including any
34
+ `COREPACK_ENABLE_STRICT=0` it needs.
35
+ - **Free disk** (`df -h .`) against what the install adds: `site-sdk` and its
36
+ dependencies are small; library mode's `@avocadostudio-ai/orchestrator-core`
37
+ adds about 66 MB. An install that fills the disk takes every tool down with
38
+ it, including the one you would use to recover — stop and say so if there is
39
+ not comfortably more free than that.
40
+
41
+ **The content.**
42
+
43
+ - **Where content lives.** A JSON file, a local module, Contentful, Sanity,
21
44
  Strapi, Storyblok, something bespoke. Name the module that reads it.
22
- - **What renders a page today.** Usually a catch-all route plus a renderer that
23
- switches on a block/section type. Name both files.
45
+ - **Whether the CMS already holds the content model** the site queries. A
46
+ template that ships queries but no model (Contentful's Next.js blog starter
47
+ is one) renders nothing until the model and seed exist, and that is a job of
48
+ its own. Check every page after seeding, not only the home page.
49
+ - **Locales.** If the CMS reports more than one locale, read
50
+ [Multilingual content](https://docs.avocadostudio.dev/integration/multilingual)
51
+ before designing anything — one editable page per (entry × locale), and four
52
+ rules that stop the round trip corrupting content. Either way, note the
53
+ locale list: step 3 declares it.
54
+
55
+ **What renders, section by section.**
56
+
57
+ - **The routes, and the components each one renders**, in order, for every
58
+ route that serves content. Name the files.
59
+ - **Map one block per rendered section by default** — even when several
60
+ sections read the same CMS entry. A blog post that is one entry rendered as
61
+ `ArticleHero`, `ArticleContent` and `ArticleTileGrid` is three blocks, not
62
+ one: a single `pageBlogPost` block makes a click anywhere select the whole
63
+ post and the panel show every field at once, and the first person to open the
64
+ Studio asks why. Merge sections only when they really are one visual unit.
65
+ Sections the template always draws in the same order get `fixed: true`.
66
+ - **Which sections are site-wide** — a header, footer, business details or CTA
67
+ rendered on every page from one source. Each becomes one `shared: true` block,
68
+ not one block per page.
24
69
  - **Which components are content-bearing**, and what their props are called.
25
- - **Which route files exist**, and which of them serve URLs the content also
26
- describes.
27
- - **The package manager**, from the lockfile.
28
70
 
29
- If the site already renders from a list of typed sections with props, this is a
30
- short job. If content is embedded in JSX, it is a long one — say that before
31
- starting, not halfway through.
71
+ **Known blockers — look for each one and report what you found.**
72
+
73
+ - **Frame headers.** `X-Frame-Options` or a CSP `frame-ancestors` that does not
74
+ include the editor's origin blocks the editor frame. On Next, `withAvocado`
75
+ writes framing headers for editor requests; if the site sets its own, pass
76
+ `withAvocado(config, { framing: false })` and add the editor origins to the
77
+ site's `frame-ancestors` yourself.
78
+ - **An existing CMS live-preview SDK.** Contentful's
79
+ `ContentfulLivePreviewProvider` throws "The current origin is not supported"
80
+ when framed by anything but `app.contentful.com` —
81
+ `enableInspectorMode={false}` does not skip that check. In the layout Avocado's
82
+ preview uses, pass `targetOrigin={[editorOrigin]}`, or do not mount the
83
+ provider on editor renders. Sanity's and Storyblok's bridges have the same
84
+ shape of problem.
85
+ - **Existing `draftMode()` use** (Next). Next has one draft cookie,
86
+ `__prerender_bypass`, and Avocado's rewrite keys on it by default — so every
87
+ preview request of the site's own CMS preview gets rewritten into Avocado's.
88
+ Stop keying on the shared cookie: `createEditorProxy({ draftCookie: false })`
89
+ (or `createEditorMiddleware` on Next 15) rewrites on `__editor=1` alone, or
90
+ pass Avocado's own session cookie, `draftCookie: "editor_draft_session"`, as
91
+ the Contentful blog integration did. Either way, internal links rendered on
92
+ an editor request must carry the editor query, since the shared cookie no
93
+ longer does it for them.
94
+ - **A CSS reset that strips rich text.** Tailwind's preflight sets
95
+ `list-style: none` and flattens headings, so a bulleted list renders as plain
96
+ paragraphs — invisible until real content has a list in it. If the site
97
+ renders rich text inside a reset, add a `.rich-text` class on the container
98
+ that restores list markers, heading sizes, link underlines and blockquote
99
+ styling, and use it wherever rich text renders.
100
+ - **Image components that assume the CMS.** An image the editor chooses can be
101
+ relative or on another host. A bare `new URL(url)`, a CDN-only loader or a
102
+ blur placeholder built from the CMS's image API crashes on it. Note every
103
+ such component; step 3 makes it tolerant.
32
104
 
33
- If the site is reachable at a URL, run `npx avocado-scope <url>` on two or
34
- three of its pages first. It reports how many sections each page has and what
35
- each would become as blocks, without installing or writing anything, and it is
36
- the fastest way to tell the user how large this job is before agreeing to it. A
105
+ If the site already renders from a list of typed sections with props, this is a
106
+ short job. If content is embedded in JSX or templates, it is a long one — say
107
+ that before starting, not halfway through — and read
108
+ [File-backed sites](https://docs.avocadostudio.dev/integration/file-backed-sites)
109
+ before step 3. It is the recipe for that case: the copy moves into one JSON
110
+ file per page plus a `global.json`, the site's own data files stay the source of
111
+ truth, the markup and styles stay as they are, and a publish is a clean git
112
+ diff.
113
+
114
+ If the site is reachable at a URL, run
115
+ `npx -p @avocadostudio-ai/migration-sdk avocado-scope <url>` on two or three of
116
+ its pages first (`--allow-localhost` when the URL is a local dev server). It
117
+ reports how many sections each page has and what each would become as blocks,
118
+ without installing or writing anything, and it is the fastest way to tell the
119
+ user how large this job is before agreeing to it. A
37
120
  page that comes back mostly `RichText` is telling you its structure lives in
38
- JSX rather than in data.
121
+ markup rather than in data.
122
+
123
+ ## 2. Choose the mode, install and mount
124
+
125
+ **Split mode or library mode.** Split mode runs the orchestrator as its own
126
+ process (`:4200` by default) and adds only `site-sdk` to the site. Library mode
127
+ mounts the orchestrator inside the site's own app — one process, one deploy —
128
+ and adds `@avocadostudio-ai/orchestrator-core`, about 66 MB of dependencies.
129
+ Library mode is Next.js only. Say which you chose and why; on a tight disk or a
130
+ site that must stay lean, split mode is the answer.
39
131
 
40
- ## 2. Install and mount
132
+ ### Next.js
41
133
 
42
134
  ```bash
43
135
  npm install @avocadostudio-ai/site-sdk # or the project's own manager
44
136
  ```
45
137
 
46
- Add `@avocadostudio-ai/orchestrator-core` as well if the orchestrator is to run
47
- inside this app ("library mode") rather than as a separate process.
138
+ Add `@avocadostudio-ai/orchestrator-core` as well for library mode.
48
139
 
49
- **Wrap the Next config.** Whatever shape it is in — `next.config.ts`,
50
- `next.config.js`, ESM or CommonJS — wrap the existing exported object; do not
51
- create a second config file beside it:
140
+ **Wrap the Next config — as its last expression, without re-indenting it.**
141
+ Whatever shape it is in — `next.config.ts`, `next.config.js`, ESM or CommonJS,
142
+ a `withPlugins(...)` or `withBundleAnalyzer(...)` chain — leave the body
143
+ exactly as it is and wrap the value that is exported:
52
144
 
53
145
  ```ts
54
146
  import { withAvocado } from "@avocadostudio-ai/site-sdk/next-config"
55
147
  export default withAvocado(existingConfig)
56
148
  ```
57
149
 
150
+ A CommonJS `next.config.js` cannot `require()` the ESM helper, so there the
151
+ last lines become an async export — Next accepts one:
152
+
153
+ ```js
154
+ const existingConfig = withPlugins([/* unchanged */], nextConfig) // what module.exports was
155
+
156
+ module.exports = async (phase, context) => {
157
+ const { withAvocado } = await import("@avocadostudio-ai/site-sdk/next-config")
158
+ const resolved = typeof existingConfig === "function" ? await existingConfig(phase, context) : existingConfig
159
+ return withAvocado(resolved)
160
+ }
161
+ ```
162
+
163
+ Bind whatever `module.exports` was assigned to a name, unchanged, and wrap that
164
+ name. `withAvocado` takes a config *object*; composers like `withPlugins` return
165
+ a function of the build phase, which is why it is resolved first. Never move `withAvocado` inside an existing chain and re-indent it: on one
166
+ integration that turned a 15-line change into a 121-line diff nobody could
167
+ review. Do not create a second config file beside the first.
168
+
58
169
  **The editor API, as one catch-all route** at
59
170
  `app/api/editor/[...path]/route.ts`:
60
171
 
@@ -65,6 +176,7 @@ import { registerBlocks } from "@/avocado/blocks"
65
176
 
66
177
  export const { GET, POST, OPTIONS } = createEditorApiHandler({
67
178
  getPages: () => getPages(),
179
+ getSiteConfig: () => ({ locales: ["en-US", "de-DE"], defaultLocale: "en-US" }),
68
180
  registerBlocks,
69
181
  blockTypes: ["PricingTier", "LogoWall"],
70
182
  onPublish: async (pages, config) => { await publishPages(pages, config); return { ok: true } },
@@ -97,13 +209,70 @@ renders that process's content.
97
209
  `createEditorMiddleware` from `@avocadostudio-ai/site-sdk/middleware`; Next 16
98
210
  uses `proxy.ts` with `createEditorProxy` from
99
211
  `@avocadostudio-ai/site-sdk/proxy`, and its `config` must be a static object
100
- literal.
212
+ literal. If the site already uses `draftMode()`, pass the `draftCookie` option
213
+ from the survey.
214
+
215
+ ### Astro
216
+
217
+ Follow [Astro integration](https://docs.avocadostudio.dev/integration/astro-integration)
218
+ for the full contract; this is the order and the traps.
219
+
220
+ ```bash
221
+ npm install @avocadostudio-ai/astro @avocadostudio-ai/site-sdk
222
+ ```
223
+
224
+ - Add the integration to `astro.config.*` — `avocado({ siteId, content,
225
+ editablePages })` in `integrations`. Wrap nothing else; leave the rest of the
226
+ config as it is.
227
+ - `content` is a path to a module whose default export is
228
+ `{ getPages, registerBlocks, blockTypes, onPublish }` (plus `getSiteConfig`
229
+ for locales). `registerBlocks` is a function called per request, not a module
230
+ side effect.
231
+ - `editablePages` names the page files the editor may preview. A page built
232
+ from `getStaticPaths` cannot render on demand; name the pages that can.
233
+ - Every editable page reads `Astro.locals.avocado.getDraftPage()` and falls back
234
+ to its published content. Skip it and the preview never updates while every
235
+ other check reports success.
236
+ - On editor renders: turn off `<ClientRouter />` (client-side navigation inside
237
+ the frame is not draft-aware), keep the editor query on internal links, and
238
+ set `devToolbar: { enabled: false }` so the toolbar does not cover the footer
239
+ inside the frame.
240
+ - Check how the installed version reads `DRAFT_MODE_SECRET`, `ORCHESTRATOR_URL`
241
+ and `PUBLISH_TOKEN`: Astro fills `import.meta.env` from `.env`, not
242
+ `process.env`. The docs page says what the integration does for you; do not
243
+ assume.
244
+ - Check the install did not pull `next` or `react` into an Astro project — they
245
+ are the SDK's Next-only peers. If they arrived, say so rather than shipping
246
+ them.
247
+ - On Astro 7, `astro dev` started without a terminal daemonises itself and
248
+ returns: read its output with `npx astro dev logs` and stop it with
249
+ `npx astro dev stop`, not by redirecting or killing the process you launched.
250
+ `astro check` there needs `@astrojs/check` and TypeScript 6; TypeScript 7
251
+ lacks the API it calls.
101
252
 
102
253
  ## 3. Declare the components
103
254
 
104
255
  Load `avocado-blocks` and follow it. On an existing site the block names are
105
256
  usually already decided by the stored content, which is the case that skill's
106
- "names that collide with the built-ins" section is about.
257
+ "names that collide with the built-ins" section is about. Use the section map
258
+ from step 1: one block per rendered section.
259
+
260
+ Declare what the site cannot store, so the editor never offers it:
261
+ `fixed: true` on a section block the template draws in a fixed position, and
262
+ `readOnly: true` with a `readOnlyReason` on a field the publisher refuses to
263
+ write — shared asset alt text, slugs, dates. `avocado-blocks` has the details.
264
+
265
+ **Site-wide content is one shared block, injected everywhere, written once.**
266
+ For each section step 1 found on every page (header, footer, business info, a
267
+ closing CTA): declare its type `shared: true`; have `getPages()` append the same
268
+ block — same fixed id such as `global-footer`, same props — to every `PageDoc`;
269
+ and have `onPublish` write page-owned blocks to their page and each shared id
270
+ once, from any page carrying it. The orchestrator keeps the draft copies
271
+ identical (an edit on one page reaches all of them, undo reverts all of them),
272
+ so there is nothing to reconcile; do not write per-page copies back. Keep the
273
+ block inside the element a preview refresh swaps (`<main>`, or
274
+ `data-avocado-root` when the footer sits outside it). Details:
275
+ [Site-wide content](https://docs.avocadostudio.dev/integration/cms-adapters#site-wide-content).
107
276
 
108
277
  **If step 1 found a CMS, load `avocado-cms` as well, and before writing any
109
278
  write path.** A CMS-backed site declares one field table and derives the schema,
@@ -114,7 +283,41 @@ perspective read feeding a write path must never be the CMS's visual-editing
114
283
  client, whose strings carry invisible stega markers that get written back into
115
284
  the dataset as real characters.
116
285
 
117
- ## 4. Decide about the page route — carefully
286
+ **Overlay the draft; do not replace the site's data layer.** Keep the site's
287
+ own queries and its components' own prop shapes, and replace only the fields
288
+ Avocado edits — fetch the page the way the site already does, then lay the
289
+ draft's block props over that object (`applyDraft(siteData, blockProps,
290
+ mapping)` from the SDK where the installed version exports it; the same few
291
+ lines by hand where it does not). Do not rewrite public templates to consume
292
+ Avocado's projected props, and do not leave the site's original data module
293
+ orphaned: that makes every production read depend on the lens, for a site whose
294
+ visitors never see the editor.
295
+
296
+ **On Next with a separate preview route**, that route re-implements the page
297
+ composition the public route already has. Say so in the report and keep the two
298
+ in step — a later change to the public page that is not mirrored makes the
299
+ editor preview a different page from the one visitors get. `npx avocado qa`
300
+ renders both with no draft applied and diffs them.
301
+
302
+ **Markers need no plumbing.** On Next, the marker helpers from
303
+ `@avocadostudio-ai/site-sdk/markers` emit nothing outside an editor render. On
304
+ Astro, take them from `editorMarkers(Astro)` in `@avocadostudio-ai/astro/markers`
305
+ — Astro's attribute spelling, read off the component's own `Astro`, and `{}` on
306
+ a visitor's render. Either way, call them directly in shared components. Do not
307
+ thread an `editable` prop, or a hand-written conditional `mark()` wrapper,
308
+ through the component tree.
309
+
310
+ **Make image components tolerate editor-chosen URLs** — relative paths and
311
+ other hosts. Parse defensively (no bare `new URL(url)`), and fall back to a
312
+ plain image when the CMS-specific loader or blur placeholder cannot apply.
313
+
314
+ **Declare the site's locales** in `getSiteConfig` —
315
+ `{ locales: ["en-US"], defaultLocale: "en-US" }`, as the CMS reports them, even
316
+ when there is only one. The editor forwards them to the orchestrator, and the
317
+ planner then asks before writing text in a language the page is not in, instead
318
+ of translating an `en-US` page into Russian because someone asked it to.
319
+
320
+ ## 4. Decide about the page route — carefully (Next.js)
118
321
 
119
322
  `createSitePage` from `@avocadostudio-ai/site-sdk/page` is a full page factory:
120
323
  it renders registered blocks, supplies `generateStaticParams` and
@@ -140,12 +343,16 @@ so those URLs go on serving the old component and the integration looks dead
140
343
  while being perfectly wired. List every route removed and every one left, with
141
344
  the reason.
142
345
 
346
+ On Astro the site always renders itself; this step is `getDraftPage()` in each
347
+ editable page, from step 2.
348
+
143
349
  ## 5. Keep the public bundle clean
144
350
 
145
- Import the attribute helpers from `@avocadostudio-ai/site-sdk/markers`. Check
146
- the First Load JS before and after: an unchanged number is the expected result.
147
- If it jumped by tens of kilobytes, something on a public page imported from
148
- `/editor`.
351
+ Import the attribute helpers from `@avocadostudio-ai/site-sdk/markers` (Next)
352
+ or `@avocadostudio-ai/astro/markers` (Astro). Check the public JavaScript before
353
+ and after: an unchanged number is the expected result. On Next, if the First
354
+ Load JS jumped by tens of kilobytes, something on a public page imported from
355
+ the SDK's editor entry — never import it there. On Astro, check the preview bridge is not in the script every visitor downloads.
149
356
 
150
357
  ## 6. Register the site with the orchestrator
151
358
 
@@ -160,22 +367,29 @@ npx avocado-register --name "My Site" --orchestrator http://localhost:3000/api/a
160
367
  single most common way this step goes wrong: it either cannot connect, or it
161
368
  registers against whatever else is on that port.
162
369
 
163
- The command does two separable things. Locally, it generates a
164
- `DRAFT_MODE_SECRET` into `.env.local` if there is not one and fills in
165
- `NEXT_PUBLIC_DEFAULT_SITE_ID`, `NEXT_PUBLIC_SITE_NAME` and
166
- `NEXT_PUBLIC_EDITOR_ORIGIN` — that half always runs. Then it POSTs the site
167
- config to `/sites/register` and writes `ORCHESTRATOR_URL` once that POST has
168
- been answered, because an address nothing replied at is a guess. Other flags:
169
- `--id`, `--port`, `--secret`, `--session`, `--purpose`, `--preview-url`,
170
- `--token`.
370
+ The command detects the framework and package manager. It POSTs the site
371
+ config and the draft secret (`--secret`, else `DRAFT_MODE_SECRET` from `.env` or
372
+ `.env.local`, else a generated one) to `/sites/register`, and the orchestrator
373
+ answers whether that secret matches its own. Then it writes the env file the
374
+ framework reads — `.env.local` with `DRAFT_MODE_SECRET`, `ORCHESTRATOR_URL` and
375
+ the `NEXT_PUBLIC_*` names on Next; `.env` with `DRAFT_MODE_SECRET`,
376
+ `ORCHESTRATOR_URL` and `AVOCADO_SITE_ID` on Astro. Other flags: `--id`,
377
+ `--port`, `--session`, `--purpose`, `--preview-url`, `--token`.
378
+
379
+ **If it stops on a secret mismatch, it wrote nothing.** The editor sends its
380
+ secret to the site with every preview, and a site holding a different one shows
381
+ published content. Get the orchestrator's value — for a standalone orchestrator
382
+ run from an Avocado checkout, `DRAFT_MODE_SECRET` in that checkout's `.env` —
383
+ ask the user for it if you cannot read it, and re-run with `--secret <value>`.
384
+ Do not generate or invent one.
171
385
 
172
386
  If it reports that it could not reach an orchestrator, that is a report and not
173
- a failure — it exits 0 and the local half has already happened. Re-run with the
174
- right `--orchestrator` to finish the registry entry. Either way, do not register
175
- by hand-editing `.env.local`.
387
+ a failure — it exits 0 and the env file has still been written, with a secret it
388
+ could not check. Re-run with the right `--orchestrator` to finish the registry
389
+ entry. Either way, do not register by hand-editing the env file.
176
390
 
177
- **One `siteId`, spelled identically in three places:** `createSitePage` (or
178
- whatever supplies the page), the `createOrchestrator` mount, and
391
+ **One `siteId`, spelled identically everywhere:** `createSitePage` or the Astro
392
+ integration's `siteId`, the `createOrchestrator` mount, and
179
393
  `avocado-register --id`. When they disagree the page asks for a draft session
180
394
  the orchestrator never seeded, which looks like the editor showing published
181
395
  content for no reason.
@@ -189,22 +403,26 @@ printed `The site is NOT registered`; say which URL it tried.
189
403
 
190
404
  Do all of these and report the figures:
191
405
 
192
- 1. `next build` succeeds, and the route list still shows the site's own pages.
406
+ 1. The production build succeeds (`next build` / `astro build`), and the route
407
+ list still shows the site's own pages.
193
408
  2. `GET /api/editor/blocks` lists exactly the site's types, with the expected
194
409
  `fields` and `listFields`.
195
410
  3. **Both coverage figures**, from `@avocadostudio-ai/site-sdk/coverage`:
196
411
  `editableCoverage` per page and `panelCoverage` overall. The target is 100%
197
412
  editable coverage on every page and zero panel findings. If you cannot reach
198
413
  it, list every remaining gap with the block type and the field path — do not
199
- quietly stop. Run it against `next dev` through the editor path: markers are
200
- emitted only on an editor render, so a production build reports zero and
201
- reads exactly like an integration that marks nothing.
414
+ quietly stop. Run it against the dev server through the editor path: markers
415
+ are emitted only on an editor render, so a production build reports zero and
416
+ reads exactly like an integration that marks nothing. That is `next dev` or
417
+ `astro dev` with `?__editor=1&siteId=…`, and each marked block's `props` set
418
+ from `GET /api/editor/pages` before `editableCoverage` runs — without them an
419
+ optional field that is empty is still counted as a gap.
202
420
  4. A page still renders the site's own markup — diff the HTML against the
203
421
  pre-integration build if you can.
204
- 5. Every migrated page is served by the catch-all and not by a leftover route
205
- file: its HTML under `next dev` carries `data-block-id`. "The URL still
206
- works" does not prove this — a shadowing route makes it work by serving the
207
- old component.
422
+ 5. Every migrated page is served by the route you meant and not by a leftover
423
+ route file: its HTML on an editor render carries `data-block-id`. "The URL
424
+ still works" does not prove this — a shadowing route makes it work by
425
+ serving the old component.
208
426
  6. An unknown slug still answers a real 404, and each page has its own
209
427
  `<title>` and description.
210
428
 
@@ -221,11 +439,48 @@ hand-written — check them rather than assuming the handler did:
221
439
 
222
440
  No `DRAFT_MODE_SECRET` or `PUBLISH_TOKEN` value appears in any committed file.
223
441
 
442
+ ## 8. QA — the integration is not done until this passes
443
+
444
+ Type-check, build and `curl` are where the checks above stop, and every problem
445
+ found after hand-off on the integrations this step comes from lived past them:
446
+ in a browser, inside the editor frame, with edited content, or across a schema
447
+ change. So the last step is the one that goes there.
448
+
449
+ ```bash
450
+ npx avocado qa
451
+ ```
452
+
453
+ It runs against the running site and a throwaway orchestrator session, never
454
+ the user's, and writes a JSON report plus a one-screen summary. **Do not report
455
+ the integration as done until it passes.** If it fails, fix what it names and
456
+ run it again; if a check cannot pass on this site, say which one and why, in
457
+ the report, in those words. If the installed version has no `qa` command yet,
458
+ say that too, and do its checks by hand: render every page × locale in an
459
+ iframe on the editor origin and look for uncaught errors, compare the preview
460
+ and public routes with no draft applied, render a rich-text fixture with every
461
+ node type, and publish one page to a scratch environment.
462
+
463
+ Then do the manual pass — ten minutes in the Studio, with the user if they are
464
+ there — and report each line as checked or not:
465
+
466
+ - [ ] Clicking each visible section selects a block with a sensible name and
467
+ only its fields
468
+ - [ ] Fields that cannot be written (asset alt text, slugs, dates) are not
469
+ offered as editable
470
+ - [ ] A chat edit and a panel edit both update the preview within a few seconds
471
+ - [ ] Rich text in the panel looks like rich text on the page (lists, headings,
472
+ links)
473
+ - [ ] Asking for a language the site does not have produces a question, not an
474
+ overwrite
475
+ - [ ] Publishing one page changes only that page in the CMS, and the public site
476
+ shows it after reload
477
+
224
478
  ## What not to do
225
479
 
226
- - Do not overwrite the user's `next.config`, page route or `globals.css`
480
+ - Do not overwrite the user's framework config, page routes or global styles
227
481
  wholesale. Wrap, extend, or ask.
228
482
  - Do not rename stored block types to avoid a collision. Register over the name.
483
+ - Do not map a whole CMS entry to one block because it is one entry.
229
484
  - Do not declare presentation props to make something editable.
230
485
  - Do not add `allowDelete` to get past the publish guard.
231
486
  - Do not report success on "it builds."