@avocadostudio-ai/skills 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/skills/avocado/SKILL.md +1 -0
- package/skills/avocado-blocks/SKILL.md +112 -14
- package/skills/avocado-cms/SKILL.md +212 -0
- package/skills/avocado-integrate/SKILL.md +74 -3
package/README.md
CHANGED
|
@@ -18,7 +18,8 @@ CLAUDE.md only if absent
|
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Then ask your agent to add Avocado to the site. It will load `avocado`, which
|
|
21
|
-
routes to one of `avocado-integrate`, `avocado-demo
|
|
21
|
+
routes to one of `avocado-integrate`, `avocado-demo`, `avocado-blocks` or
|
|
22
|
+
`avocado-cms`.
|
|
22
23
|
|
|
23
24
|
## Why a package and not a docs page
|
|
24
25
|
|
package/dist/index.js
CHANGED
|
@@ -76,6 +76,7 @@ the job:
|
|
|
76
76
|
| Wiring Avocado into this site | \`avocado-integrate\` |
|
|
77
77
|
| A demo, or a fresh project | \`avocado-demo\` |
|
|
78
78
|
| Declaring components as editable blocks | \`avocado-blocks\` |
|
|
79
|
+
| Content that comes from a CMS | \`avocado-cms\` |
|
|
79
80
|
|
|
80
81
|
The rule those skills exist to protect: **a component's props are not editable
|
|
81
82
|
until a schema declares them.** An undeclared prop cannot be reached by any
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|
package/skills/avocado/SKILL.md
CHANGED
|
@@ -20,6 +20,7 @@ every path.
|
|
|
20
20
|
| A Next.js app that already exists, with its own components and content | `avocado-integrate` |
|
|
21
21
|
| Nothing yet, or wants to see it working before committing | `avocado-demo` |
|
|
22
22
|
| Either, and you are now declaring their components as editable blocks | `avocado-blocks` |
|
|
23
|
+
| Either, and the content those blocks render comes from a CMS | `avocado-cms` |
|
|
23
24
|
|
|
24
25
|
Before deciding, if the site is live, `npx avocado-scope <url>` will tell you
|
|
25
26
|
what one of its pages would become as blocks without installing or writing
|
|
@@ -102,34 +102,132 @@ Avocado ships twenty built-in types: `Hero`, `FeatureGrid`, `Testimonials`,
|
|
|
102
102
|
## Marking up the renderer
|
|
103
103
|
|
|
104
104
|
A declared field is editable in the panel. It is editable **in the preview**
|
|
105
|
-
only once the
|
|
105
|
+
only once the markup says where it is. Three attributes do that, all from
|
|
106
|
+
`@avocadostudio-ai/site-sdk/markers` — never from `/editor`, see the rule in the
|
|
107
|
+
`avocado` skill about the public bundle.
|
|
108
|
+
|
|
109
|
+
### The block boundary comes first
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import { getPreviewWrapperProps } from "@avocadostudio-ai/site-sdk/markers"
|
|
113
|
+
|
|
114
|
+
<section {...getPreviewWrapperProps(editorMode, blockId, "PricingTier")}>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Selection is built entirely on `[data-block-id]`: a click in the preview
|
|
118
|
+
resolves through `closest("[data-block-id]")`, and no match is read as "clicked
|
|
119
|
+
outside any block", which *clears* the selection. Mark fields without this and
|
|
120
|
+
the preview frames, renders and scrolls correctly and deselects on every click
|
|
121
|
+
— which looks exactly like selection mode being switched off. It returns `{}`
|
|
122
|
+
when `editorMode` is false, so the published page renders the markup it
|
|
123
|
+
rendered before.
|
|
124
|
+
|
|
125
|
+
### Then each field
|
|
106
126
|
|
|
107
127
|
```tsx
|
|
108
128
|
import { editableProps } from "@avocadostudio-ai/site-sdk/markers"
|
|
109
129
|
|
|
110
|
-
export function PricingTier({ blockId, name, blurb }) {
|
|
130
|
+
export function PricingTier({ blockId, name, blurb, photoUrl }) {
|
|
111
131
|
return (
|
|
112
|
-
<section>
|
|
113
|
-
<h3 {...editableProps(
|
|
114
|
-
<p {...editableProps(
|
|
132
|
+
<section {...getPreviewWrapperProps(editorMode, blockId, "PricingTier")}>
|
|
133
|
+
<h3 {...editableProps("name")}>{name}</h3>
|
|
134
|
+
<p {...editableProps("blurb", { kind: "richtext" })}>{blurb}</p>
|
|
135
|
+
<div {...editableProps("photoUrl", { kind: "image" })}>
|
|
136
|
+
<img src={photoUrl} alt="" />
|
|
137
|
+
</div>
|
|
115
138
|
</section>
|
|
116
139
|
)
|
|
117
140
|
}
|
|
118
141
|
```
|
|
119
142
|
|
|
120
|
-
|
|
121
|
-
|
|
143
|
+
The signature is `editableProps(path, { label?, kind? })` — **one path, then
|
|
144
|
+
options**. It is not `editableProps(blockId, field)`: the block id belongs to
|
|
145
|
+
the wrapper above, and passing it here marks a field literally named after the
|
|
146
|
+
id.
|
|
147
|
+
|
|
148
|
+
- **The path is the same grammar an operation uses** — `title`,
|
|
149
|
+
`cards[0].title`, `links[0].children[1].label` — because it is the same path.
|
|
150
|
+
What the overlay reads here is what it sends back as the field to patch.
|
|
151
|
+
- **Pass `kind`.** Without it the overlay guesses an image from the prop name
|
|
152
|
+
against Avocado's own naming (`imageUrl`, `*.src`), so a field called
|
|
153
|
+
`photoUrl` or `heroSrc` gets a picker in the property panel and no button in
|
|
154
|
+
the preview, with no error on either side. Pass the same word the manifest
|
|
155
|
+
uses and the guess never runs.
|
|
156
|
+
- **For an image, mark the wrapper, not the `<img>`.** The overlay appends its
|
|
157
|
+
Change button *into* the marked element, and nothing can be appended into an
|
|
158
|
+
`<img>`.
|
|
159
|
+
- **The marker goes on the element that renders the text**, not on its wrapper.
|
|
160
|
+
|
|
161
|
+
### Rows drawn by their own component need a scope
|
|
162
|
+
|
|
163
|
+
The path is scoped from the block down, which a renderer can only write if it
|
|
164
|
+
knows where it sits. That holds while one component draws the whole block and
|
|
165
|
+
stops the moment a list row is a component of its own: the child knows it has a
|
|
166
|
+
`question` and cannot know it is `items[3]`.
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import { editableScopeProps } from "@avocadostudio-ai/site-sdk/markers"
|
|
170
|
+
|
|
171
|
+
<div className="flex flex-col gap-6">
|
|
172
|
+
{props.items.map((item, i) => (
|
|
173
|
+
<div key={item.id} {...editableScopeProps(`items[${i}]`, { display: "contents" })}>
|
|
174
|
+
<FaqRow item={item} /> {/* marks a bare "question"; needs no prefix */}
|
|
175
|
+
</div>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Forgetting the scope is silent and **wrong**, not silent and absent: the child
|
|
181
|
+
marks a bare `question`, the overlay resolves it against the enclosing block,
|
|
182
|
+
and an edit to a headline inside a column patches a prop the section does not
|
|
183
|
+
have. Be systematic — every renderer, every field.
|
|
184
|
+
|
|
185
|
+
Pass `{ display: "contents" }` whenever the wrapper exists only to carry the
|
|
186
|
+
scope. Without it the wrapper becomes the flex or grid item and the layout the
|
|
187
|
+
rows had silently becomes the layout of a column of wrappers: gaps land in
|
|
188
|
+
different places, `align-items` applies to the wrong box, and a grid's rows stop
|
|
189
|
+
being the grid's children at all. Leave it off when the wrapper is one you were
|
|
190
|
+
going to render anyway — a row that already has an `<li>` or a card `<div>`
|
|
191
|
+
should carry the scope on that rather than gain a second element to hold it.
|
|
192
|
+
|
|
193
|
+
Scopes nest and compose outermost first. A block boundary ends the composition,
|
|
194
|
+
so a scope outside a block never reaches into it.
|
|
122
195
|
|
|
123
196
|
Two things this costs on a real site, so plan for them: subcomponents factored
|
|
124
|
-
for rendering often do not know their own position and need a
|
|
125
|
-
threaded in, and
|
|
126
|
-
|
|
197
|
+
for rendering often do not know their own position and need either a scope
|
|
198
|
+
wrapper or a path threaded in, and marking up a design system is the long pole
|
|
199
|
+
of the whole integration.
|
|
127
200
|
|
|
128
|
-
## Verify on
|
|
201
|
+
## Verify on numbers
|
|
202
|
+
|
|
203
|
+
`@avocadostudio-ai/site-sdk/coverage` exports both halves. Run both and report
|
|
204
|
+
the figures — "it builds" is not a result, because a site can build perfectly
|
|
205
|
+
with every field unreachable.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import {
|
|
209
|
+
extractMarkedBlocks, editableCoverage, formatEditableCoverage,
|
|
210
|
+
panelCoverage, formatPanelCoverage,
|
|
211
|
+
} from "@avocadostudio-ai/site-sdk/coverage"
|
|
212
|
+
```
|
|
129
213
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
214
|
+
- **`editableCoverage`** reports `marked/expected` — of the fields the manifest
|
|
215
|
+
declares, how many the rendered page actually carries a marker for.
|
|
216
|
+
- **`panelCoverage`** reports `rowsLabelled/rowsExamined` plus findings: list
|
|
217
|
+
rows nobody can tell apart, polymorphic branches that never narrow, props in
|
|
218
|
+
the content that nothing describes, and type names colliding with the
|
|
219
|
+
built-ins.
|
|
220
|
+
|
|
221
|
+
**Target: 100% editable coverage on every page, and zero panel findings.** If
|
|
222
|
+
you cannot reach it, do not quietly stop — list every remaining gap with the
|
|
223
|
+
block type, the field path and why.
|
|
224
|
+
|
|
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
|
|
230
|
+
`http://localhost:3000//` and measures nothing.
|
|
133
231
|
|
|
134
232
|
Cross-check the manifest directly too — `GET /api/editor/blocks` should list
|
|
135
233
|
exactly the site's types, with the expected `fields` and `listFields` on each.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Editing a CMS through Avocado
|
|
7
|
+
|
|
8
|
+
Read the `avocado` skill first. Load this once the survey in `avocado-integrate`
|
|
9
|
+
has established that content comes from a CMS rather than from files.
|
|
10
|
+
|
|
11
|
+
The job is one declaration, not four. A CMS-backed integration has to agree
|
|
12
|
+
about the same fields in four places — the Zod schema, the panel metadata, the
|
|
13
|
+
projection out of the CMS, and the merge back into it. Written by hand they
|
|
14
|
+
drift, and every way they drift is silent. Declare a **field table** and derive
|
|
15
|
+
all four.
|
|
16
|
+
|
|
17
|
+
**Writes go back into somebody's production content.** More of this page is
|
|
18
|
+
about not corrupting it than about making it work, and that ratio is correct.
|
|
19
|
+
|
|
20
|
+
## 1. The table
|
|
21
|
+
|
|
22
|
+
Key it by the CMS's own name for each type — `hero_section`, not
|
|
23
|
+
`SiteHeroSection`. `BlockType` is a free string, and one name across the CMS,
|
|
24
|
+
the manifest and a publish diff makes the adapter an identity map rather than a
|
|
25
|
+
translation nobody can grep for.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// avocado/table.ts
|
|
29
|
+
import type { FieldTable } from "@avocadostudio-ai/site-sdk/lens"
|
|
30
|
+
|
|
31
|
+
export const TABLE: FieldTable = {
|
|
32
|
+
hero_section: {
|
|
33
|
+
displayName: "Hero",
|
|
34
|
+
topLevel: true,
|
|
35
|
+
fields: {
|
|
36
|
+
title: { kind: "text" },
|
|
37
|
+
body: { kind: "richtext" },
|
|
38
|
+
background_image: { kind: "image", label: "Background" },
|
|
39
|
+
cta_link: { kind: "link" },
|
|
40
|
+
alignment: { kind: "enum", options: ["left", "center"] },
|
|
41
|
+
anchor: { kind: "text", localized: false },
|
|
42
|
+
buttons: { kind: "list", of: ["button"] },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
button: {
|
|
46
|
+
displayName: "Button",
|
|
47
|
+
topLevel: false,
|
|
48
|
+
fields: { label: { kind: "text" }, variant: { kind: "enum", options: ["solid", "outline"] } },
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- **`topLevel: false` for row types.** A card or a button is declared so the
|
|
54
|
+
panel can draw it and the merge can construct it, and must never appear in the
|
|
55
|
+
block picker.
|
|
56
|
+
- **A list is `of` or `itemFields`, never both.** `of` names child types that
|
|
57
|
+
are their own documents and carry their own type key; `itemFields` describes a
|
|
58
|
+
row that is a plain object. Every type named in `of` must itself be in the
|
|
59
|
+
table, and the pack must supply a `rowTypeKey`.
|
|
60
|
+
- **A field the table does not declare is invisible to Avocado and untouched by
|
|
61
|
+
it.** It never reaches the planner or the panel, and it survives every publish
|
|
62
|
+
because the merge patches the source document rather than replacing it. That
|
|
63
|
+
is the lever for scope: declare what an editor should change, leave the layout
|
|
64
|
+
and behaviour switches out.
|
|
65
|
+
- **`localized: false`** marks a field that has one value for every language —
|
|
66
|
+
an anchor, a slug fragment, an icon name. It is not decoration: without it the
|
|
67
|
+
lens looks for a per-language value and writes one.
|
|
68
|
+
|
|
69
|
+
## 2. Register it with `registerLens`, not `registerFieldTable`
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// avocado/lens.ts
|
|
73
|
+
import { createLens, registerLens } from "@avocadostudio-ai/site-sdk/lens"
|
|
74
|
+
import { storyblokPrimitives, storyblokLocale } from "@avocadostudio-ai/site-sdk/lens/storyblok"
|
|
75
|
+
import { TABLE } from "./table"
|
|
76
|
+
|
|
77
|
+
export const lens = createLens({
|
|
78
|
+
table: TABLE,
|
|
79
|
+
locale: storyblokLocale("de", ["de", "en", "fr"]),
|
|
80
|
+
primitives: storyblokPrimitives(),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
registerLens(lens)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Sanity is the same with `sanityPrimitives()` and `sanityLocale(…)` from
|
|
87
|
+
`@avocadostudio-ai/site-sdk/lens/sanity`.
|
|
88
|
+
|
|
89
|
+
**Prefer `registerLens(lens)`.** The older two-call form —
|
|
90
|
+
`registerFieldTable(TABLE, { primitives })` beside `createLens({ …, primitives })`
|
|
91
|
+
— is two declarations that have to agree, and when they disagreed about what an
|
|
92
|
+
image field's two props are called, the panel drew `image` / `image_alt` while
|
|
93
|
+
the projection emitted `imageUrl` / `imageAlt`, and every image on the site was
|
|
94
|
+
both invisible and uneditable with nothing erroring. `registerLens` takes the
|
|
95
|
+
table *and* the primitives off the lens, so it cannot disagree with itself.
|
|
96
|
+
|
|
97
|
+
`registerFieldTable` now refuses a table that declares an image when no image
|
|
98
|
+
naming was given, so the old shape fails loudly rather than silently. It remains
|
|
99
|
+
correct for a table with **no lens** — a site registering blocks without
|
|
100
|
+
projecting a CMS through them. If you are writing `createLens`, use
|
|
101
|
+
`registerLens`.
|
|
102
|
+
|
|
103
|
+
For a CMS with no pack, write the primitives yourself — do not skip the table
|
|
104
|
+
and hand-roll a projection. A pack supplies the codecs, the image naming, the
|
|
105
|
+
row id and type keys, and the locale lens.
|
|
106
|
+
|
|
107
|
+
## 3. Read and write through the lens
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const props = lens.project(doc, "hero_section", "fr")
|
|
111
|
+
const { doc: next, changed, warnings } = lens.merge(doc, editedProps, "hero_section", "fr")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**`merge` takes the LIVE CMS document as its source**, never a snapshot Avocado
|
|
115
|
+
holds. That is what makes every undeclared field survive by construction.
|
|
116
|
+
Preserve the property. If you find yourself writing a whole document out of
|
|
117
|
+
Avocado props, stop and ask the user.
|
|
118
|
+
|
|
119
|
+
Two write rules govern every field, and neither was reasoned out from the
|
|
120
|
+
shapes — both came from running a projection through its own inverse over a real
|
|
121
|
+
dataset:
|
|
122
|
+
|
|
123
|
+
- **Unchanged means untouched.** A CMS with per-language fallback resolves a
|
|
124
|
+
missing translation to the default language: correct on screen, a lie in
|
|
125
|
+
storage. Merge a projection back wholesale and every fallback becomes a real,
|
|
126
|
+
fabricated translation — dozens per publish, each identical to what the page
|
|
127
|
+
already showed, so nothing looks wrong.
|
|
128
|
+
- **Empty means absent.** Writing `""` into a slot that had no value for this
|
|
129
|
+
language is a no-op on screen and a diff in the document, so a publish that
|
|
130
|
+
touched one field reports every page as modified.
|
|
131
|
+
|
|
132
|
+
A **`reference`** field is a pointer, not a link. The href is a *render* of it —
|
|
133
|
+
the same stored reference serves `/faq` and `/fr/faq` — so a projection can
|
|
134
|
+
never be its own inverse, and writing the rendered href back replaces the
|
|
135
|
+
reference with a hard-coded URL that stops following renames. Declare it
|
|
136
|
+
`reference`, not `link`.
|
|
137
|
+
|
|
138
|
+
## 4. Prove the round trip before anyone publishes
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const { clean, fields, warnings } = lens.roundTrip(doc, "hero_section", "fr")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Project, merge the projection straight back, and report what moved. **Nothing
|
|
145
|
+
should.** A non-empty `fields` is a codec that is not the inverse of itself for
|
|
146
|
+
some value in *this* dataset — invisible in the editor, harmless in the preview,
|
|
147
|
+
and visible later as a publish wanting to rewrite documents nobody opened.
|
|
148
|
+
|
|
149
|
+
Run it over **real content, not fixtures**, across every type in the table and
|
|
150
|
+
every language. Every rule above exists because a fixture round-tripped and a
|
|
151
|
+
dataset did not. Report the count of types checked and the fields that moved.
|
|
152
|
+
|
|
153
|
+
## 5. Never read a write path through a visual-editing client
|
|
154
|
+
|
|
155
|
+
If the site already uses the CMS's visual-editing tooling — Sanity's
|
|
156
|
+
Presentation tool, Contentful's Content Source Maps, the Storyblok bridge — its
|
|
157
|
+
draft client **stega-encodes** every string: hundreds of zero-width characters
|
|
158
|
+
encoding which field produced the text. Invisible everywhere a human looks.
|
|
159
|
+
|
|
160
|
+
Avocado's path is adapter read → session draft → planner edit → publish diff →
|
|
161
|
+
**write back**, so an encoded string poisons four things at once: the planner
|
|
162
|
+
reasons over text that is mostly invisible padding, the panel shows text that
|
|
163
|
+
looks right and is not, the publish diff compares an encoded string against a
|
|
164
|
+
clean one and reports **every field on the site as changed**, and a real publish
|
|
165
|
+
writes the markers into the dataset as real characters.
|
|
166
|
+
|
|
167
|
+
The client an integrator reaches for first is the one already lying around, and
|
|
168
|
+
that is the wrong one. Give the adapter a **second, machine-read client** — same
|
|
169
|
+
token, same `perspective: "drafts"`, `stega: false` — and keep the encoded one
|
|
170
|
+
for rendering:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const avocadoClient = client.withConfig({ stega: false })
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The rule is category-level: **a perspective read that feeds a write path must be
|
|
177
|
+
the machine read, never the visual-editing read.**
|
|
178
|
+
|
|
179
|
+
## 6. One page per (document × language)
|
|
180
|
+
|
|
181
|
+
A field-level-i18n CMS stores every language inside one document; Avocado edits
|
|
182
|
+
one page at a time. So the adapter presents one `PageDoc` per document *and*
|
|
183
|
+
language, and the lens is told which language it is projecting. Keep the
|
|
184
|
+
language in the page id and slug so a publish can route the merge back to the
|
|
185
|
+
right slot, and never let two languages of the same document share a page id.
|
|
186
|
+
|
|
187
|
+
Read `https://docs.avocadostudio.dev/integration/multilingual` before writing a
|
|
188
|
+
single CMS write path. A wrong projection corrupts content invisibly.
|
|
189
|
+
|
|
190
|
+
## Verify
|
|
191
|
+
|
|
192
|
+
Report all of these as numbers, not as "it works":
|
|
193
|
+
|
|
194
|
+
1. `lens.roundTrip` clean over real content — how many types and languages, and
|
|
195
|
+
every field that moved.
|
|
196
|
+
2. `GET /api/editor/blocks` lists exactly the table's `topLevel` types, with the
|
|
197
|
+
`fields` and `listFields` the table implies.
|
|
198
|
+
3. A publish diff on an untouched page is **empty**. A diff that reports
|
|
199
|
+
everything changed is the stega symptom in section 5, or a codec that is not
|
|
200
|
+
its own inverse.
|
|
201
|
+
4. The coverage figures from `avocado-blocks` — the table gives the manifest,
|
|
202
|
+
the renderers still have to carry the markers.
|
|
203
|
+
|
|
204
|
+
## What not to do
|
|
205
|
+
|
|
206
|
+
- Do not hand-write the projection or the merge beside a table. Derive both.
|
|
207
|
+
- Do not pass the site's visual-editing client to the adapter.
|
|
208
|
+
- Do not write a whole document back from Avocado props.
|
|
209
|
+
- Do not declare a `reference` as a `link`.
|
|
210
|
+
- Do not skip `roundTrip` because the fixtures pass. Fixtures are how these bugs
|
|
211
|
+
got shipped.
|
|
212
|
+
- Do not report success on a publish diff nobody read.
|
|
@@ -105,6 +105,15 @@ Load `avocado-blocks` and follow it. On an existing site the block names are
|
|
|
105
105
|
usually already decided by the stored content, which is the case that skill's
|
|
106
106
|
"names that collide with the built-ins" section is about.
|
|
107
107
|
|
|
108
|
+
**If step 1 found a CMS, load `avocado-cms` as well, and before writing any
|
|
109
|
+
write path.** A CMS-backed site declares one field table and derives the schema,
|
|
110
|
+
the panel metadata, the projection and the merge from it, rather than
|
|
111
|
+
hand-writing four things that have to agree. That skill also carries the rules
|
|
112
|
+
that stop a publish corrupting the user's content — chief among them that a
|
|
113
|
+
perspective read feeding a write path must never be the CMS's visual-editing
|
|
114
|
+
client, whose strings carry invisible stega markers that get written back into
|
|
115
|
+
the dataset as real characters.
|
|
116
|
+
|
|
108
117
|
## 4. Decide about the page route — carefully
|
|
109
118
|
|
|
110
119
|
`createSitePage` from `@avocadostudio-ai/site-sdk/page` is a full page factory:
|
|
@@ -138,17 +147,79 @@ the First Load JS before and after: an unchanged number is the expected result.
|
|
|
138
147
|
If it jumped by tens of kilobytes, something on a public page imported from
|
|
139
148
|
`/editor`.
|
|
140
149
|
|
|
141
|
-
## 6.
|
|
150
|
+
## 6. Register the site with the orchestrator
|
|
151
|
+
|
|
152
|
+
With the user's dev server running, from the project directory:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx avocado-register --name "My Site" --orchestrator http://localhost:3000/api/avocado
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Pass `--orchestrator`.** The default is `http://localhost:4200`, which is the
|
|
159
|
+
*standalone* server and not what a library-mode project runs. Omitting it is the
|
|
160
|
+
single most common way this step goes wrong: it either cannot connect, or it
|
|
161
|
+
registers against whatever else is on that port.
|
|
162
|
+
|
|
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`.
|
|
171
|
+
|
|
172
|
+
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`.
|
|
176
|
+
|
|
177
|
+
**One `siteId`, spelled identically in three places:** `createSitePage` (or
|
|
178
|
+
whatever supplies the page), the `createOrchestrator` mount, and
|
|
179
|
+
`avocado-register --id`. When they disagree the page asks for a draft session
|
|
180
|
+
the orchestrator never seeded, which looks like the editor showing published
|
|
181
|
+
content for no reason.
|
|
182
|
+
|
|
183
|
+
Registration is optional in library mode — the mount already knows the one site
|
|
184
|
+
it is in and reports it from `GET /sites`. What registration adds is the name,
|
|
185
|
+
preview URL and purpose in the registry. Do not report the step as done if it
|
|
186
|
+
printed `The site is NOT registered`; say which URL it tried.
|
|
187
|
+
|
|
188
|
+
## 7. Verify on numbers
|
|
142
189
|
|
|
143
190
|
Do all of these and report the figures:
|
|
144
191
|
|
|
145
192
|
1. `next build` succeeds, and the route list still shows the site's own pages.
|
|
146
193
|
2. `GET /api/editor/blocks` lists exactly the site's types, with the expected
|
|
147
194
|
`fields` and `listFields`.
|
|
148
|
-
3.
|
|
195
|
+
3. **Both coverage figures**, from `@avocadostudio-ai/site-sdk/coverage`:
|
|
196
|
+
`editableCoverage` per page and `panelCoverage` overall. The target is 100%
|
|
197
|
+
editable coverage on every page and zero panel findings. If you cannot reach
|
|
198
|
+
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.
|
|
149
202
|
4. A page still renders the site's own markup — diff the HTML against the
|
|
150
203
|
pre-integration build if you can.
|
|
151
|
-
5.
|
|
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.
|
|
208
|
+
6. An unknown slug still answers a real 404, and each page has its own
|
|
209
|
+
`<title>` and description.
|
|
210
|
+
|
|
211
|
+
And the four security seams, which are the reason the routes are not
|
|
212
|
+
hand-written — check them rather than assuming the handler did:
|
|
213
|
+
|
|
214
|
+
- `GET /api/editor/draft?secret=<secret>&redirect=/` is a 307 and sets the
|
|
215
|
+
draft cookie.
|
|
216
|
+
- The same call with a wrong secret is rejected.
|
|
217
|
+
- The same call with `redirect=https://evil.example` does **not** redirect
|
|
218
|
+
off-site.
|
|
219
|
+
- `POST /api/editor/publish` with `{"pages":[]}` answers 409 and leaves the
|
|
220
|
+
content store untouched.
|
|
221
|
+
|
|
222
|
+
No `DRAFT_MODE_SECRET` or `PUBLISH_TOKEN` value appears in any committed file.
|
|
152
223
|
|
|
153
224
|
## What not to do
|
|
154
225
|
|