@avocadostudio-ai/skills 0.21.1 → 0.23.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 +8 -0
- package/package.json +4 -2
- package/skills/avocado/SKILL.md +25 -10
- package/skills/avocado-blocks/SKILL.md +74 -7
- package/skills/avocado-cms/SKILL.md +128 -2
- package/skills/avocado-integrate/SKILL.md +309 -54
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.
|
|
3
|
+
"version": "0.23.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",
|
package/skills/avocado/SKILL.md
CHANGED
|
@@ -17,14 +17,18 @@ every path.
|
|
|
17
17
|
|
|
18
18
|
| The user has | Load |
|
|
19
19
|
|---|---|
|
|
20
|
-
| A Next.js
|
|
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,
|
|
26
|
-
what one of
|
|
27
|
-
anything
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
- **
|
|
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
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
## 1. Survey, and report before touching anything
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
- **
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
50
|
-
`next.config.js`, ESM or CommonJS
|
|
51
|
-
|
|
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
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
174
|
-
right `--orchestrator` to finish the registry
|
|
175
|
-
by hand-editing
|
|
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
|
|
178
|
-
|
|
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`
|
|
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
|
|
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
|
|
205
|
-
file: its HTML
|
|
206
|
-
works" does not prove this — a shadowing route makes it work by
|
|
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
|
|
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."
|