@bettercms-ai/mcp 0.83.0 → 0.83.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +86 -86
  2. package/SKILL.md +160 -160
  3. package/dist/index.js +38 -9
  4. package/dist/index.js.map +1 -1
  5. package/dist/kit-catalog.json +1509 -1509
  6. package/dist/skills/bettercms/SKILL.md +206 -206
  7. package/dist/skills/bettercms/references/accessibility.md +171 -171
  8. package/dist/skills/bettercms/references/animation.md +296 -296
  9. package/dist/skills/bettercms/references/astro.md +148 -148
  10. package/dist/skills/bettercms/references/bindings.md +366 -366
  11. package/dist/skills/bettercms/references/componentize.md +185 -185
  12. package/dist/skills/bettercms/references/components.md +415 -415
  13. package/dist/skills/bettercms/references/content-experiments.md +62 -62
  14. package/dist/skills/bettercms/references/content-modeling.md +382 -382
  15. package/dist/skills/bettercms/references/detection.md +465 -465
  16. package/dist/skills/bettercms/references/dock-input.md +121 -121
  17. package/dist/skills/bettercms/references/editor-chrome.md +92 -92
  18. package/dist/skills/bettercms/references/external-content.md +78 -78
  19. package/dist/skills/bettercms/references/finish-site.md +377 -377
  20. package/dist/skills/bettercms/references/forms-and-interactions.md +245 -245
  21. package/dist/skills/bettercms/references/frameworks.md +159 -159
  22. package/dist/skills/bettercms/references/getting-started.md +156 -156
  23. package/dist/skills/bettercms/references/integrations.md +364 -364
  24. package/dist/skills/bettercms/references/kit-catalog.md +3259 -3259
  25. package/dist/skills/bettercms/references/kit-scenarios.md +712 -712
  26. package/dist/skills/bettercms/references/localization.md +85 -85
  27. package/dist/skills/bettercms/references/media-and-brand.md +181 -181
  28. package/dist/skills/bettercms/references/next.md +189 -189
  29. package/dist/skills/bettercms/references/other-frameworks.md +85 -85
  30. package/dist/skills/bettercms/references/portable-text.md +213 -213
  31. package/dist/skills/bettercms/references/programmatic-seo.md +98 -98
  32. package/dist/skills/bettercms/references/publishing.md +289 -289
  33. package/dist/skills/bettercms/references/seo.md +357 -357
  34. package/dist/skills/bettercms/references/shopify-headless.md +52 -52
  35. package/dist/skills/bettercms/references/site-blueprints.md +895 -895
  36. package/dist/skills/bettercms/references/site-review.md +74 -74
  37. package/dist/skills/bettercms/references/worked-examples-advanced.md +509 -509
  38. package/dist/skills/bettercms/references/worked-examples.md +885 -885
  39. package/dist/skills/bettercms/references/write-limits.md +217 -217
  40. package/package.json +1 -1
@@ -1,366 +1,366 @@
1
- ---
2
- title: "Bindings and inline editing"
3
- description: "Click-to-edit attributes and addresses, declared mode and the binding report, stega in preview strings, and classifying an unfamiliar codebase."
4
- ---
5
-
6
- # Bindings, inline editing and codebase classification
7
-
8
- Doctrine (what should be editable, the conversion loop): §11 and §13 of
9
- `bettercms://playbook/schema`, and the `convert_site` prompt. This is the template side. Rich-text
10
- values: [portable-text.md](portable-text.md). Adapter wiring and env: [frameworks.md](frameworks.md).
11
-
12
- ## 1. Why the attributes matter
13
-
14
- A `data-bcms-*` attribute is the only thing that locates a stored value on a rendered page. Five
15
- readers resolve the same address: the Visual Editor canvas, the component Output pane and live
16
- preview, publish-time value injection, `get_binding_report`, and componentization. A reader that
17
- cannot resolve an address guesses, which works once.
18
-
19
- ## 2. Bind the leaf, and know which vocabulary you are writing in
20
-
21
- **Put the attributes on the element that renders the value and nothing else: the `<img>` itself,
22
- the `<h1>` itself. Never a wrapper, never an element whose children you need.** A declared write
23
- owns what is inside the element it names. Bind a `<section>` or a card and the band's own
24
- markup is what a rich text or image write overwrites; the page comes back as bare text where a
25
- layout used to be. A plain-text write keeps the inline elements inside its element (a highlight
26
- `<span>`, split-text spans, a `<br>`) on a project in the text-node publish mode, and replaces
27
- them on an older one: [animation.md](animation.md) §1 has both, and the recipe per animation
28
- library.
29
-
30
- `data-bcms-kind` does not choose the editor; the MODEL does. The attribute says **"this element is
31
- the declared CONTAINER for the field"**, so the canvas repaints it whole. Leave it off and the
32
- canvas treats the element as one text-matched FRAGMENT of the value and injects an inline render
33
- instead, which for a Body means the article does not come back until the next rebuild.
34
-
35
- Two separate lists, and mixing them is the way to break a page:
36
-
37
- | Attribute | The only values it takes |
38
- |---|---|
39
- | `data-bcms-kind` | `text` (or omit it) · `richtext` · `image` · `document` |
40
- | the kind inside `data-bcms-props` | `text` · `richtext` · `image` · `url` · `number` · `array` |
41
-
42
- A model's field TYPES (`select`, `boolean`, `date`, `number`) are **not** kinds. Stamping one
43
- names a value no renderer mints, and the preview writer refuses it with `UNKNOWN_KIND`.
44
-
45
- A `document` body: the WRAPPER carries `data-bcms-kind="document"`, and the per-block elements
46
- inside carry their own `data-bcms-kind="richtext"`. Never `"richtext"` on the wrapper: that takes
47
- the rich-text arm, which repaints the container from the field's derived PLAIN TEXT and loses the
48
- article's blocks. Omitting the kind is legal (the hosted renderer emits it that way) and costs the
49
- container repaint, so a declared template states it.
50
-
51
- What the author gets, from the field's type: text is edited in place, an image opens the media
52
- picker and patches the element's `src`, richtext and document open a Portable Text editor in the
53
- frame, and a `url` never takes a caret: it is edited in the element toolbar, because a malformed
54
- URL typed onto the page has nowhere to report itself.
55
-
56
- ### Lists, labels and icons
57
-
58
- An element bound as text or richtext may hold only inline markup (`span`, `a`, `strong`, `em`,
59
- `b`, `i`, `small`, `sup`, `code`, `br`), classes and inline styles included. A publish writes the
60
- value over everything else inside it, so:
61
-
62
- - **A list item with a marker.** In `<li><span>✓</span>{item}</li>` the `<li>` renders the tick
63
- and the item; bind it and the first save erases the tick. Wrap the value in its own `<span>`
64
- and bind that. An `<li>` that renders nothing but the value is itself the leaf.
65
- - **A link or button label beside an icon.** In `<a href="…"><svg/> Sign up</a>`, put the label
66
- in its own `<span>` with the field, and the `href` on the anchor through `data-bcms-props`
67
- (`cta.href|url|href`). A binding on the anchor's text deletes the icon. A call to action is a
68
- `button` field rendered with `BcmsButton` ([components.md](components.md)).
69
- - **SVG text is never bound; the icon is.** No field is derived from the text inside an `<svg>`,
70
- and no binding there is measured. Each inline icon IS an editable `image` field (`config.media:
71
- "icon"`, an svg value): an editor swaps it. Never remove one to tidy a form; group it. So is a
72
- sprite icon (`<svg><use href="#id">`) whose `<symbol>` is in the same page, a wordmark set in
73
- `<text>`, and an illustration up to 64 KB. A swap keeps the element's classes and size and
74
- stays inline, so `currentColor` still applies; scripts and remote references are stripped.
75
- - **Copy in an attribute is a field of its own.** Every release derives a text field for an
76
- `aria-label`, a `title`, a `placeholder` outside a form, and the number a count-up script
77
- counts to (`data-target`, `data-count`, `data-value` and their kin). Its `config.attr` names the
78
- attribute; it is bound on `data-bcms-props` as `<path>|text|<attribute>` and published into that
79
- attribute alone, never matched against the page's words. A label that only repeats the words
80
- on its own element gets none. To declare one in code, write the same `data-bcms-props` entry.
81
- - **Text beside an icon in a plain element** (`<span>Pages<svg/></span>`) is a field when it is
82
- the element's one text node: the save writes that node and leaves the icon. Split over several
83
- text nodes it stays `BARE_TEXT`, so wrap the label in its own `<span>` where you can.
84
- - **Icon-only links in the nav and footer** take their field name from what a screen reader
85
- reads: the anchor's `aria-label`, its `sr-only` text, a `title`, the SVG `<title>` or an `alt`
86
- (never `<style>` or `<script>` text). Give each one an `aria-label` or `sr-only` label, or every
87
- social link is named `link` and an editor cannot tell them apart. A row of them is a "Links" or
88
- "Social links" list; each row keeps its own name in its values.
89
-
90
- ### The site's design is not yours to change
91
-
92
- **Never edit a `class`, `className` or `style`, and never rewrite markup to suit the CMS.** A rich
93
- text value keeps inline markup exactly as the site wrote it: `Equip <span class="text-yellow-500
94
- dark:text-yellow-400">ScrewFast</span>` is stored and published with that span and its classes.
95
- Do not turn it into `<em>` with an `[&_em]:` selector on the parent, and do not turn
96
- `<span class="font-bold">` into `<b>`. Before submitting a conversion receipt, run
97
- `npx @bettercms-ai/convert --verify-styles --base <commit before the conversion> --root .` and put
98
- its `styleEdits` in the receipt; any token it lists is a design change to restore.
99
-
100
- ### Images
101
-
102
- An image the page imports and renders is bound by the codemod, including one the page hands to a
103
- section component (`<Hero src={heroImage} />` → the component's `<Image src={src}>`): the call site
104
- reads `bcmsImage(...)` and passes `bcmsBindings`, and the `<Image>` gets
105
- `data-bcms-field={bcmsBindings?.src} data-bcms-kind="image"`. Keep the `astro:assets` import as
106
- the fallback; the build renders it and the publish writes the editor's image. Frontmatter images
107
- (`cardImage: '@/images/blog/post-1.avif'`) become image fields on their own once the build's
108
- images are in the Media Library, matched by file name. An image rendered inside a loop over a
109
- list is not bound automatically: bind it in the list's data file.
110
-
111
- The codemod knows an image component by its IMPORT, not its name: `Image` and `Picture` from
112
- `astro:assets` (aliased, or through `import * as assets`), and the default export of
113
- `next/image` under any name. An import that reaches the page through `getImage()`, `hero.src` or
114
- a style is refused as `IMAGE_ASSET_UNRESOLVED`, and the row's message names the import: declare
115
- the element that shows it by hand (`data-bcms-field` + `data-bcms-kind="image"`).
116
-
117
- Images need no code at all on a text-matched site. Every release derives an image field for each
118
- picture a page shows: an `<img>` (its `src`, or where that is a placeholder a lazy loader's
119
- `data-src`, then the largest `srcset` candidate), a whole `<picture>` as ONE field, a
120
- `<video poster>`, an inline or STYLESHEET `background-image` (Tailwind `bg-[url(...)]` included)
121
- and an inline `<svg>`. What it declines is listed with a reason in the binding report's
122
- `imageGaps`, and everything else it declines in `elementGaps`
123
- ([media-and-brand.md](media-and-brand.md) Where images hide).
124
-
125
- Publishing an image keeps the page responsive. A library image re-points every `srcset`
126
- candidate and each `<source>` at the new picture at that candidate's own width and format, and
127
- leaves `sizes`, `width`, `loading`, classes, ids and `data-*` as written; never hand-edit those
128
- after a swap. A stylesheet background gets one inline `background-image` over the rule, removed
129
- again when the value returns to the sheet's own picture.
130
-
131
- To keep an image editable in code you write yourself, render it with `BcmsImage` and its `path`
132
- (Astro: `@bettercms-ai/astro/components/BcmsImage.astro`; Next: `BcmsImage` from
133
- `@bettercms-ai/next`): one `<img>` that declares the field, reads the value's alt, crop and focal
134
- point, and offers a width ladder when given `sizes`.
135
-
136
- ## 3. Addresses
137
-
138
- The grammar is identical in `@bettercms-ai/next` and `@bettercms-ai/astro`; emit it with the SDK
139
- helpers rather than by hand.
140
-
141
- - `data-bcms-field="<address>"` + `data-bcms-kind` on the element whose content is the value.
142
- - `data-bcms-props="<address>|<kind>|<domAttribute>;…"` for values that live in an attribute
143
- (`alt`, `href`, a video `src`). One element carries one field address, so attribute values ride
144
- this list. An `href`, an `src` and a `poster` are kind `url` (`cta.href|url|href`, not
145
- `|text|`); an `alt` is `text`. The same address on two elements is edited as two copies of one
146
- value (numbered `data-node-index`).
147
- - `<BcmsField path kind as>` binds hand-rendered entry values. Around ONE element it adds no
148
- wrapper: the attributes go on that element. Around text or several elements it renders a
149
- `display:contents` span, and `as` names a real wrapper. `path` is dotted keys with any
150
- number of `[n]` indices: `title`, `hero.title`, `columns[0].links[2].href`.
151
- - Rich text is addressed per paragraph: `<base>#<_key>`. No write tool takes an address:
152
- `update_content_entry` replaces the whole field (it merges by default, keeping the other
153
- top-level fields, never other blocks of the same field). The platform's own path writer refuses a
154
- `#<_key>` path (`KEYED_PATH_NOT_WRITABLE`), and only the editor patches one block. A `_key` is
155
- part of an address, so never regenerate keys of blocks that already exist.
156
- - **Inside a component instance** the address is `<instanceId>__overrides.<key>`. Block ids
157
- inside a component belong to the definition and are shared by every instance, so a
158
- definition-scoped address binds to nothing and two instances collide. Use the package `bind` /
159
- `bindProps` helpers inside instances; they resolve the instance address for you.
160
- - **The placement root carries `data-bcms-block="<blockId>"`.** `BcmsBlocks` emits it; without it
161
- the Output pane and the section overlay fall back to an ancestor guess.
162
- - **Chrome is its own lane.** A nav or footer value is
163
- `data-bcms-layout-field="layout:<sectionId>:<fieldId>"`, never `data-bcms-field`; the page lane
164
- cannot reach a Layout value.
165
- - **Draft markers come from the SDK.** `BcmsDraftMarker` and the Astro middleware stamp
166
- `<html data-bcms-draft="1">`; `BcmsDraftBridge` stamps `data-bcms-canvas="bridge"`. Mount them;
167
- never write them by hand. Binding attributes are always rendered and inert for visitors.
168
-
169
- ## 4. Declared mode, publish, and the report
170
-
171
- - Until `set_binding_mode` `{ declaredBindings: true }` the platform text-matches, which holds
172
- only while CMS values equal the built copy. Flip it only when the stamped build's report is
173
- clean on every route: push, release, read `get_binding_report`, flip, release again, then
174
- confirm `mode: "declared"` and `coverage.pending` empty.
175
- - On a declared site every published value is written into the element that declares it, so the
176
- bytes your template renders at build time are replaced on publish; never put markup you need to
177
- keep inside a bound element. A value with no element never reaches the page, and one that
178
- would corrupt its element is refused and named.
179
- - Read the report, not the screen. Clean = `coverage.pending` empty and the mode you expect. Each
180
- `unmatched` reason names a fix in the source. A field the stamped build renders on no route is flagged `stale: true` on its schema — kept, hidden in the editor, cleared by the release that binds it again — and is not listed. A Global Layout Section is reported only on the
181
- routes that render it (`layoutRoutes` lists each route's Sections): a docs page on its own layout
182
- is not missing the nav, so never add one there. A `fallback` flag means the build carried no
183
- `bcms-brief` meta and was measured against the latest pin. Only a release creates the report;
184
- a content apply later updates its Layout rows.
185
-
186
- ## 5. Preview strings (stega)
187
-
188
- Preview responses requested with `stega=1` hide invisible characters inside prose strings so the
189
- editor can map text back to fields. Two filters decide what is skipped, and you need both:
190
-
191
- - by field NAME: `slug`, `url`, `href`, `src`, `id`, `email`, `color`, `icon`, `date`,
192
- `datetime`, `locale`, `type`, `target`, `rel`, `_type`, `_key`;
193
- - by VALUE shape, for fields it cannot recognise by name: anything starting `http:`, `https:`,
194
- `mailto:`, `tel:`, `#` or `/`, and ISO dates.
195
-
196
- Portable Text arrays and richtext/image envelopes are skipped whole. Never compare, slugify or
197
- parse a preview string as-is, and never put one in an attribute or SEO output. Compare published
198
- values, or strip it first with `stripStega` from `@bettercms-ai/sdk`. The editor strips it before
199
- it saves.
200
-
201
- | The string goes to | Strip first? | Why |
202
- |---|---|---|
203
- | Visible text a template renders | Never | The markers are how a click finds its field |
204
- | A Portable Text renderer, `BcmsField`, an image helper | Never | Those values carry no markers; pass them as-is |
205
- | `===`, `switch`, `includes`, sort order, a lookup key | Always | The hidden characters make equal strings unequal |
206
- | A React/Astro `key`, an element `id`, a slug, a URL you build | Always | A marked id never matches its published twin |
207
- | Any HTML attribute (`class`, `href`, `alt`, `aria-label`) | Always | Attributes are not text the editor maps |
208
- | `<head>`: title, meta, JSON-LD | Always | Crawlers and share cards read the characters |
209
- | A third-party call (analytics, search, maps, a date parser) | Always | They store or reject what they cannot read |
210
-
211
- ## 6. Classify an unfamiliar codebase before binding it
212
-
213
- Inspect `package.json`, the framework config, route tree, build command, output mode, content
214
- loaders, deployment config and any existing BetterCMS adapter. Then classify:
215
-
216
- 1. **First-class adapter**: Astro or Next.js with its BetterCMS package ([astro.md](astro.md),
217
- [next.md](next.md)).
218
- 2. **React/Vite or another SDK integration**: find who owns routing and data fetching, call the
219
- SDK on the correct side of the server/client boundary, keep an explicit component registry,
220
- and state what preview supports. The Astro and Next helpers do not transfer unchanged.
221
- 3. **Static HTML/CSS/JS**: keep the working markup and add a safe content path where supported.
222
- 4. **Other or opaque generated output**: preserve behaviour and produce an adapter plan (fetch
223
- typed content, resolve sections and components, render Portable Text, bind editable values,
224
- serve media, preview drafts, refresh on publish), marking every unimplemented capability. A
225
- source crawl alone never proves full editability.
226
-
227
- Use syntax-aware parsing and the build output, never blind string replacement; for bulk stamping
228
- use the codemod (`npx @bettercms-ai/convert`). Trace the real data flow: where values are
229
- fetched, normalised, passed as props and rendered. A component that looks like its CMS record but
230
- still renders hard-coded copy is not connected. Client components never receive server keys.
231
-
232
- ## 7. Edit-to-render test
233
-
234
- 1. Pick a safe draft field and record its old value.
235
- 2. Edit it through the dashboard or the MCP; confirm in preview that the intended leaf or
236
- attribute changed.
237
- 3. Confirm sibling instances, global layout and other routes behave according to their scope.
238
- 4. Restore the value, or keep the change the user asked for.
239
- 5. After an authorised publish, fetch the public URL cache-busted and check status and body.
240
- Repeat for a dynamic entry route and a global element.
241
-
242
- ## Acceptance
243
-
244
- - The site builds with the repository's build command; routes and assets load.
245
- - Every claimed editable field has a path: schema, reader, component prop, DOM binding, preview,
246
- public output.
247
- - No public secrets, wrong media hosts, unscoped instance ids or hard-coded shadow values.
248
- - `get_binding_report` for each route is clean, and one value of each bound kind opens the right
249
- editor in the Visual Editor.
250
- - `get_binding_report`'s `help.percent` is 100: every bound field also tells its editor what it
251
- is for. Binding a leaf makes it editable; help text makes it usable
252
- ([content-modeling.md](content-modeling.md), "Writing help text").
253
-
254
- ## Reading the binding report and the conversion receipt
255
-
256
- `get_binding_report` per slot (`slot` is `current` or `staging`; on a promote-gated project read
257
- `current`, the tree the editor frames). Every figure, `unaddressable` included, is measured per
258
- slot. `sha` is the build the report describes; `refreshRequired` is true when that is not the
259
- build the slot serves. `builtRoutes` lists the routes the live build has HTML for (null = cannot
260
- say). `layoutIncomplete` lists Layout paths the last content apply could not write (their publish
261
- waits); `layoutUnread` lists Layout fields no route reads (`layoutUnreadJudged` false = not
262
- judged).
263
-
264
- - **Before any release** there is no report: pages 0, mode null, `refreshRequired` true.
265
- `refreshReason` `never-generated` means only a release BetterCMS builds and serves writes a
266
- report, so a headless site never gets one, redeploy or not; `outdated` means the next release
267
- rewrites it.
268
- - **`unmatched` reasons**: `not-declared`, `ambiguous-text`, `no-element`, `declared-only`,
269
- `needs-wrap`, `refused`, `layout-owned`, `page-owned`, `layout-locator-stale`,
270
- `locale-unvarianted`, `svg-unavailable`, `partial`, `unstamped` (a page list the content apply
271
- left as built; `kind` reads `list <bound>/<total>`), `mismatch` (a text-match stamp whose element
272
- shows more than the value, or no longer the text it was matched on, `data-bcms-from`; never
273
- written) and `report-outdated`.
274
- - A clean report proves each non-empty field has an element carrying its path, nothing more. A Global
275
- Section counts only on the routes that render it (`layoutRoutes`).
276
- - **`unaddressable`** is the copy the report cannot see: visible live text no field owns, by
277
- route and bucket, biggest first (null = unmeasured: diff the visible text against the entry
278
- values). Over `count / visible > 0.02`, fix it in the source (wrap the run in an element the
279
- codemod can bind, or declare `<BcmsField path=…>`), deploy and read again (playbook §11).
280
- - `canvas.lane` is the editor's live-preview lane (`bridge`, `draft-route`; `none` sends you to
281
- `get_next_steps`). `coverage.error` `nothing-bound` means no element of this build is bound, so
282
- its percentage describes another site.
283
-
284
- `submit_conversion_receipt` answers `complete` (`sourceComplete` is the receipt's own verdict;
285
- `liveChecks` gives the last release's `structureUnstamped` route count and `reportSha`) and
286
- `pendingTotal`, and echoes the first 40 pending rows with their `fix`:
287
- `{ action, file, line, col?, snippet, why?, kind? }`. `action` is one of `wrap-span`,
288
- `declare-attr`, `bind-expression`, `declare-richtext`, `bind-data`, `manual`; `snippet` and `why`
289
- say what to change. On a `declare-richtext` with `kind: document`, bind the one element wrapping
290
- every block of the Body, never the paragraph holding its first. A 404 `unknown-brief` means that
291
- digest was never issued here. The same run owes the `--forms` receipt, submitted as a second call
292
- under the same `briefDigest`, and the `styleEdits` of `convert --verify-styles` inside this
293
- receipt; one listing a changed class, className or style is refused with 409 `style-edits`.
294
-
295
- The complete brief (`get_conversion_brief { complete: true }`) also carries `forms` (id, status,
296
- fields, submitUrl). After the binding pass, run the codemod's forms mode (`npx
297
- @bettercms-ai/convert --forms`, with the brief and a receipt file) to wire them into the
298
- repository's own `<form>` elements. Read `forms.pending`, and publish every form `forms.notes` names in the Forms tab: a draft
299
- form rejects every submission with a 403.
300
-
301
- ## Editor setup, proposals and binding decisions
302
-
303
- `editor_setup` derives what a build no longer imports: a build brings in SEO/meta, media, brand and
304
- the models `bcms-content.json` declares; editor setup adds pages, collections, forms, bindings,
305
- button and section components (when the authoring preference is components), Component Groups and
306
- the Content sidebar. `get_deploy_status` `editorSetup` reports its state.
307
-
308
- The complete brief's `proposals` (page 1, at most 50; `proposalsMore` when there are more) list
309
- what the last convert found and no field owns: `{ id, kind, target, text, suggested?, evidence? }`.
310
- `id` is `pr_` and 16 hex and survives re-runs; `target` is a `file:line:col` locator only. Kinds:
311
- `unaddressable` and `code-candidate` (copy no field owns), `inert` (declared, nothing reads it),
312
- `mismatch` (declares one path, reads another), `flatten` (two leaves in one element).
313
-
314
- Answer them with `editor_setup { declare: [item, …] }` (at most 100 items, one result each;
315
- `check: true` with `declare` is a dry run that stores nothing):
316
-
317
- - `{ op: "accept", ids }` takes each proposal's `suggested` op. A proposal with none refuses.
318
- - `{ op: "bind", id, path, owner? }` binds the copy to an existing field; `owner` is a route or
319
- `layout`. The next convert writes it into the source (a mismatch's declaration is rewritten; an
320
- attribute or an expression is reported `manual` in `receipt.decisionsApplied`). A `bind` waits as
321
- `decision-pending` until a build proves the new read; one still pending after the next release
322
- blocks the gate. A declared element is corrected through its `mismatch` proposal id.
323
- - `{ op: "code", id, why, system? }` keeps it in code. `why` is one of `runtime`, `external`,
324
- `derived`, `stylesheet`, `decorative`, `embed`, `legal`, and the system checks its evidence;
325
- without it the item refuses `evidence-missing` (bind it, pick the reason that fits, or a person
326
- marks it in Overview). The next convert writes `data-bcms-code-owned="text why:<id>"`; the
327
- `code` counts once a build carries it. Each convert re-checks the
328
- facts: one whose reason no longer holds becomes `evidence-lost`, blocks the gate, and returns to
329
- the proposals to decide again. Facts are same-file only: a value from another file needs a person.
330
- - `{ op: "reject", ids }` drops the suggestion; the copy stays unaddressable. A person's reject of
331
- an AI `code` is its Undo: the next convert removes the token.
332
- - `{ op: "clear", decisionId }` undoes a decision.
333
-
334
- A person's decision beats yours: add `force: true` and a `reason` (shown to them) only when they
335
- asked for it. Refusals per item: `target-unknown`, `path-not-in-schema`, `owner-mismatch`,
336
- `locked-by-person`, `slot-mismatch`, `evidence-missing`, `declaration-conflict`, `invalid-item`.
337
- Until a convert that emits proposals has submitted its receipt, every id refuses `target-unknown`.
338
- `get_next_steps` `receipt.decisions` counts `stored`, `materialized`, `proven`, `pending` (open
339
- proposals) and `lost`; stop when `pending` is 0.
340
-
341
- ### Archive (ZIP) sites
342
-
343
- A project with no connected repository and no receipt carrying proposals is an ARCHIVE site: no
344
- codemod runs, so each release scan mints the proposals itself. `target` is then
345
- `template|owner|textHash` (owner `page`, `layout` or `entry:<collection>`; Layout copy has template
346
- `*`, an entry route its collection's `/blog/*`), and translations of a page share one template.
347
- Decisions are not written into source: every release stamps them into the built HTML wherever
348
- their anchors (structure, text, nearest declared field) agree 2 of 3, else the row is `lost`
349
- (`anchor-lost`). A `bind` stamps only when the element's text equals the field's stored value; an
350
- uploaded copy change loses it (`anchor-text-changed`) and its copy is proposed again. An AI `code`
351
- for `runtime`, `external`, `derived` or `stylesheet` refuses `evidence-missing` here (there is no
352
- source to check); `decorative`, `embed` and `legal` are checked on the page, and a person may mark
353
- any reason. Each such decision lives in BetterCMS, not in the site's code: recommend connecting a
354
- repository.
355
-
356
- Archive sites also take `{ op: "rule", template, match, then }`, one row for every element it
357
- matches on every route of `template`:
358
- - `template`: `/`-segments literal or `*`, a trailing `/**`, or `*` alone (every route). No regex.
359
- - `match`: at least one of `struct` (a proposal's structure anchor), `cls` (one class token),
360
- `text` (exact copy); `owner` (`page`, `layout`, `entry`) narrows.
361
- - `then`: `{ op: "code", why }` or `{ op: "bind", path }`. A bind rule names a `layout` or `entry`
362
- owner, and matching more than one element on a route is `ambiguous` there.
363
- A person beats an AI, then a per-id decision beats a rule; two rules saying different things about
364
- one element are `declaration-conflict`. On a git site a `rule` refuses `invalid-item`. A lane change
365
- (a repository connected, or a convert's receipt arriving) loses the other lane's decisions
366
- (`lane-changed`): decide again on the new proposals.
1
+ ---
2
+ title: "Bindings and inline editing"
3
+ description: "Click-to-edit attributes and addresses, declared mode and the binding report, stega in preview strings, and classifying an unfamiliar codebase."
4
+ ---
5
+
6
+ # Bindings, inline editing and codebase classification
7
+
8
+ Doctrine (what should be editable, the conversion loop): §11 and §13 of
9
+ `bettercms://playbook/schema`, and the `convert_site` prompt. This is the template side. Rich-text
10
+ values: [portable-text.md](portable-text.md). Adapter wiring and env: [frameworks.md](frameworks.md).
11
+
12
+ ## 1. Why the attributes matter
13
+
14
+ A `data-bcms-*` attribute is the only thing that locates a stored value on a rendered page. Five
15
+ readers resolve the same address: the Visual Editor canvas, the component Output pane and live
16
+ preview, publish-time value injection, `get_binding_report`, and componentization. A reader that
17
+ cannot resolve an address guesses, which works once.
18
+
19
+ ## 2. Bind the leaf, and know which vocabulary you are writing in
20
+
21
+ **Put the attributes on the element that renders the value and nothing else: the `<img>` itself,
22
+ the `<h1>` itself. Never a wrapper, never an element whose children you need.** A declared write
23
+ owns what is inside the element it names. Bind a `<section>` or a card and the band's own
24
+ markup is what a rich text or image write overwrites; the page comes back as bare text where a
25
+ layout used to be. A plain-text write keeps the inline elements inside its element (a highlight
26
+ `<span>`, split-text spans, a `<br>`) on a project in the text-node publish mode, and replaces
27
+ them on an older one: [animation.md](animation.md) §1 has both, and the recipe per animation
28
+ library.
29
+
30
+ `data-bcms-kind` does not choose the editor; the MODEL does. The attribute says **"this element is
31
+ the declared CONTAINER for the field"**, so the canvas repaints it whole. Leave it off and the
32
+ canvas treats the element as one text-matched FRAGMENT of the value and injects an inline render
33
+ instead, which for a Body means the article does not come back until the next rebuild.
34
+
35
+ Two separate lists, and mixing them is the way to break a page:
36
+
37
+ | Attribute | The only values it takes |
38
+ |---|---|
39
+ | `data-bcms-kind` | `text` (or omit it) · `richtext` · `image` · `document` |
40
+ | the kind inside `data-bcms-props` | `text` · `richtext` · `image` · `url` · `number` · `array` |
41
+
42
+ A model's field TYPES (`select`, `boolean`, `date`, `number`) are **not** kinds. Stamping one
43
+ names a value no renderer mints, and the preview writer refuses it with `UNKNOWN_KIND`.
44
+
45
+ A `document` body: the WRAPPER carries `data-bcms-kind="document"`, and the per-block elements
46
+ inside carry their own `data-bcms-kind="richtext"`. Never `"richtext"` on the wrapper: that takes
47
+ the rich-text arm, which repaints the container from the field's derived PLAIN TEXT and loses the
48
+ article's blocks. Omitting the kind is legal (the hosted renderer emits it that way) and costs the
49
+ container repaint, so a declared template states it.
50
+
51
+ What the author gets, from the field's type: text is edited in place, an image opens the media
52
+ picker and patches the element's `src`, richtext and document open a Portable Text editor in the
53
+ frame, and a `url` never takes a caret: it is edited in the element toolbar, because a malformed
54
+ URL typed onto the page has nowhere to report itself.
55
+
56
+ ### Lists, labels and icons
57
+
58
+ An element bound as text or richtext may hold only inline markup (`span`, `a`, `strong`, `em`,
59
+ `b`, `i`, `small`, `sup`, `code`, `br`), classes and inline styles included. A publish writes the
60
+ value over everything else inside it, so:
61
+
62
+ - **A list item with a marker.** In `<li><span>✓</span>{item}</li>` the `<li>` renders the tick
63
+ and the item; bind it and the first save erases the tick. Wrap the value in its own `<span>`
64
+ and bind that. An `<li>` that renders nothing but the value is itself the leaf.
65
+ - **A link or button label beside an icon.** In `<a href="…"><svg/> Sign up</a>`, put the label
66
+ in its own `<span>` with the field, and the `href` on the anchor through `data-bcms-props`
67
+ (`cta.href|url|href`). A binding on the anchor's text deletes the icon. A call to action is a
68
+ `button` field rendered with `BcmsButton` ([components.md](components.md)).
69
+ - **SVG text is never bound; the icon is.** No field is derived from the text inside an `<svg>`,
70
+ and no binding there is measured. Each inline icon IS an editable `image` field (`config.media:
71
+ "icon"`, an svg value): an editor swaps it. Never remove one to tidy a form; group it. So is a
72
+ sprite icon (`<svg><use href="#id">`) whose `<symbol>` is in the same page, a wordmark set in
73
+ `<text>`, and an illustration up to 64 KB. A swap keeps the element's classes and size and
74
+ stays inline, so `currentColor` still applies; scripts and remote references are stripped.
75
+ - **Copy in an attribute is a field of its own.** Every release derives a text field for an
76
+ `aria-label`, a `title`, a `placeholder` outside a form, and the number a count-up script
77
+ counts to (`data-target`, `data-count`, `data-value` and their kin). Its `config.attr` names the
78
+ attribute; it is bound on `data-bcms-props` as `<path>|text|<attribute>` and published into that
79
+ attribute alone, never matched against the page's words. A label that only repeats the words
80
+ on its own element gets none. To declare one in code, write the same `data-bcms-props` entry.
81
+ - **Text beside an icon in a plain element** (`<span>Pages<svg/></span>`) is a field when it is
82
+ the element's one text node: the save writes that node and leaves the icon. Split over several
83
+ text nodes it stays `BARE_TEXT`, so wrap the label in its own `<span>` where you can.
84
+ - **Icon-only links in the nav and footer** take their field name from what a screen reader
85
+ reads: the anchor's `aria-label`, its `sr-only` text, a `title`, the SVG `<title>` or an `alt`
86
+ (never `<style>` or `<script>` text). Give each one an `aria-label` or `sr-only` label, or every
87
+ social link is named `link` and an editor cannot tell them apart. A row of them is a "Links" or
88
+ "Social links" list; each row keeps its own name in its values.
89
+
90
+ ### The site's design is not yours to change
91
+
92
+ **Never edit a `class`, `className` or `style`, and never rewrite markup to suit the CMS.** A rich
93
+ text value keeps inline markup exactly as the site wrote it: `Equip <span class="text-yellow-500
94
+ dark:text-yellow-400">ScrewFast</span>` is stored and published with that span and its classes.
95
+ Do not turn it into `<em>` with an `[&_em]:` selector on the parent, and do not turn
96
+ `<span class="font-bold">` into `<b>`. Before submitting a conversion receipt, run
97
+ `npx @bettercms-ai/convert --verify-styles --base <commit before the conversion> --root .` and put
98
+ its `styleEdits` in the receipt; any token it lists is a design change to restore.
99
+
100
+ ### Images
101
+
102
+ An image the page imports and renders is bound by the codemod, including one the page hands to a
103
+ section component (`<Hero src={heroImage} />` → the component's `<Image src={src}>`): the call site
104
+ reads `bcmsImage(...)` and passes `bcmsBindings`, and the `<Image>` gets
105
+ `data-bcms-field={bcmsBindings?.src} data-bcms-kind="image"`. Keep the `astro:assets` import as
106
+ the fallback; the build renders it and the publish writes the editor's image. Frontmatter images
107
+ (`cardImage: '@/images/blog/post-1.avif'`) become image fields on their own once the build's
108
+ images are in the Media Library, matched by file name. An image rendered inside a loop over a
109
+ list is not bound automatically: bind it in the list's data file.
110
+
111
+ The codemod knows an image component by its IMPORT, not its name: `Image` and `Picture` from
112
+ `astro:assets` (aliased, or through `import * as assets`), and the default export of
113
+ `next/image` under any name. An import that reaches the page through `getImage()`, `hero.src` or
114
+ a style is refused as `IMAGE_ASSET_UNRESOLVED`, and the row's message names the import: declare
115
+ the element that shows it by hand (`data-bcms-field` + `data-bcms-kind="image"`).
116
+
117
+ Images need no code at all on a text-matched site. Every release derives an image field for each
118
+ picture a page shows: an `<img>` (its `src`, or where that is a placeholder a lazy loader's
119
+ `data-src`, then the largest `srcset` candidate), a whole `<picture>` as ONE field, a
120
+ `<video poster>`, an inline or STYLESHEET `background-image` (Tailwind `bg-[url(...)]` included)
121
+ and an inline `<svg>`. What it declines is listed with a reason in the binding report's
122
+ `imageGaps`, and everything else it declines in `elementGaps`
123
+ ([media-and-brand.md](media-and-brand.md) Where images hide).
124
+
125
+ Publishing an image keeps the page responsive. A library image re-points every `srcset`
126
+ candidate and each `<source>` at the new picture at that candidate's own width and format, and
127
+ leaves `sizes`, `width`, `loading`, classes, ids and `data-*` as written; never hand-edit those
128
+ after a swap. A stylesheet background gets one inline `background-image` over the rule, removed
129
+ again when the value returns to the sheet's own picture.
130
+
131
+ To keep an image editable in code you write yourself, render it with `BcmsImage` and its `path`
132
+ (Astro: `@bettercms-ai/astro/components/BcmsImage.astro`; Next: `BcmsImage` from
133
+ `@bettercms-ai/next`): one `<img>` that declares the field, reads the value's alt, crop and focal
134
+ point, and offers a width ladder when given `sizes`.
135
+
136
+ ## 3. Addresses
137
+
138
+ The grammar is identical in `@bettercms-ai/next` and `@bettercms-ai/astro`; emit it with the SDK
139
+ helpers rather than by hand.
140
+
141
+ - `data-bcms-field="<address>"` + `data-bcms-kind` on the element whose content is the value.
142
+ - `data-bcms-props="<address>|<kind>|<domAttribute>;…"` for values that live in an attribute
143
+ (`alt`, `href`, a video `src`). One element carries one field address, so attribute values ride
144
+ this list. An `href`, an `src` and a `poster` are kind `url` (`cta.href|url|href`, not
145
+ `|text|`); an `alt` is `text`. The same address on two elements is edited as two copies of one
146
+ value (numbered `data-node-index`).
147
+ - `<BcmsField path kind as>` binds hand-rendered entry values. Around ONE element it adds no
148
+ wrapper: the attributes go on that element. Around text or several elements it renders a
149
+ `display:contents` span, and `as` names a real wrapper. `path` is dotted keys with any
150
+ number of `[n]` indices: `title`, `hero.title`, `columns[0].links[2].href`.
151
+ - Rich text is addressed per paragraph: `<base>#<_key>`. No write tool takes an address:
152
+ `update_content_entry` replaces the whole field (it merges by default, keeping the other
153
+ top-level fields, never other blocks of the same field). The platform's own path writer refuses a
154
+ `#<_key>` path (`KEYED_PATH_NOT_WRITABLE`), and only the editor patches one block. A `_key` is
155
+ part of an address, so never regenerate keys of blocks that already exist.
156
+ - **Inside a component instance** the address is `<instanceId>__overrides.<key>`. Block ids
157
+ inside a component belong to the definition and are shared by every instance, so a
158
+ definition-scoped address binds to nothing and two instances collide. Use the package `bind` /
159
+ `bindProps` helpers inside instances; they resolve the instance address for you.
160
+ - **The placement root carries `data-bcms-block="<blockId>"`.** `BcmsBlocks` emits it; without it
161
+ the Output pane and the section overlay fall back to an ancestor guess.
162
+ - **Chrome is its own lane.** A nav or footer value is
163
+ `data-bcms-layout-field="layout:<sectionId>:<fieldId>"`, never `data-bcms-field`; the page lane
164
+ cannot reach a Layout value.
165
+ - **Draft markers come from the SDK.** `BcmsDraftMarker` and the Astro middleware stamp
166
+ `<html data-bcms-draft="1">`; `BcmsDraftBridge` stamps `data-bcms-canvas="bridge"`. Mount them;
167
+ never write them by hand. Binding attributes are always rendered and inert for visitors.
168
+
169
+ ## 4. Declared mode, publish, and the report
170
+
171
+ - Until `set_binding_mode` `{ declaredBindings: true }` the platform text-matches, which holds
172
+ only while CMS values equal the built copy. Flip it only when the stamped build's report is
173
+ clean on every route: push, release, read `get_binding_report`, flip, release again, then
174
+ confirm `mode: "declared"` and `coverage.pending` empty.
175
+ - On a declared site every published value is written into the element that declares it, so the
176
+ bytes your template renders at build time are replaced on publish; never put markup you need to
177
+ keep inside a bound element. A value with no element never reaches the page, and one that
178
+ would corrupt its element is refused and named.
179
+ - Read the report, not the screen. Clean = `coverage.pending` empty and the mode you expect. Each
180
+ `unmatched` reason names a fix in the source. A field the stamped build renders on no route is flagged `stale: true` on its schema — kept, hidden in the editor, cleared by the release that binds it again — and is not listed. A Global Layout Section is reported only on the
181
+ routes that render it (`layoutRoutes` lists each route's Sections): a docs page on its own layout
182
+ is not missing the nav, so never add one there. A `fallback` flag means the build carried no
183
+ `bcms-brief` meta and was measured against the latest pin. Only a release creates the report;
184
+ a content apply later updates its Layout rows.
185
+
186
+ ## 5. Preview strings (stega)
187
+
188
+ Preview responses requested with `stega=1` hide invisible characters inside prose strings so the
189
+ editor can map text back to fields. Two filters decide what is skipped, and you need both:
190
+
191
+ - by field NAME: `slug`, `url`, `href`, `src`, `id`, `email`, `color`, `icon`, `date`,
192
+ `datetime`, `locale`, `type`, `target`, `rel`, `_type`, `_key`;
193
+ - by VALUE shape, for fields it cannot recognise by name: anything starting `http:`, `https:`,
194
+ `mailto:`, `tel:`, `#` or `/`, and ISO dates.
195
+
196
+ Portable Text arrays and richtext/image envelopes are skipped whole. Never compare, slugify or
197
+ parse a preview string as-is, and never put one in an attribute or SEO output. Compare published
198
+ values, or strip it first with `stripStega` from `@bettercms-ai/sdk`. The editor strips it before
199
+ it saves.
200
+
201
+ | The string goes to | Strip first? | Why |
202
+ |---|---|---|
203
+ | Visible text a template renders | Never | The markers are how a click finds its field |
204
+ | A Portable Text renderer, `BcmsField`, an image helper | Never | Those values carry no markers; pass them as-is |
205
+ | `===`, `switch`, `includes`, sort order, a lookup key | Always | The hidden characters make equal strings unequal |
206
+ | A React/Astro `key`, an element `id`, a slug, a URL you build | Always | A marked id never matches its published twin |
207
+ | Any HTML attribute (`class`, `href`, `alt`, `aria-label`) | Always | Attributes are not text the editor maps |
208
+ | `<head>`: title, meta, JSON-LD | Always | Crawlers and share cards read the characters |
209
+ | A third-party call (analytics, search, maps, a date parser) | Always | They store or reject what they cannot read |
210
+
211
+ ## 6. Classify an unfamiliar codebase before binding it
212
+
213
+ Inspect `package.json`, the framework config, route tree, build command, output mode, content
214
+ loaders, deployment config and any existing BetterCMS adapter. Then classify:
215
+
216
+ 1. **First-class adapter**: Astro or Next.js with its BetterCMS package ([astro.md](astro.md),
217
+ [next.md](next.md)).
218
+ 2. **React/Vite or another SDK integration**: find who owns routing and data fetching, call the
219
+ SDK on the correct side of the server/client boundary, keep an explicit component registry,
220
+ and state what preview supports. The Astro and Next helpers do not transfer unchanged.
221
+ 3. **Static HTML/CSS/JS**: keep the working markup and add a safe content path where supported.
222
+ 4. **Other or opaque generated output**: preserve behaviour and produce an adapter plan (fetch
223
+ typed content, resolve sections and components, render Portable Text, bind editable values,
224
+ serve media, preview drafts, refresh on publish), marking every unimplemented capability. A
225
+ source crawl alone never proves full editability.
226
+
227
+ Use syntax-aware parsing and the build output, never blind string replacement; for bulk stamping
228
+ use the codemod (`npx @bettercms-ai/convert`). Trace the real data flow: where values are
229
+ fetched, normalised, passed as props and rendered. A component that looks like its CMS record but
230
+ still renders hard-coded copy is not connected. Client components never receive server keys.
231
+
232
+ ## 7. Edit-to-render test
233
+
234
+ 1. Pick a safe draft field and record its old value.
235
+ 2. Edit it through the dashboard or the MCP; confirm in preview that the intended leaf or
236
+ attribute changed.
237
+ 3. Confirm sibling instances, global layout and other routes behave according to their scope.
238
+ 4. Restore the value, or keep the change the user asked for.
239
+ 5. After an authorised publish, fetch the public URL cache-busted and check status and body.
240
+ Repeat for a dynamic entry route and a global element.
241
+
242
+ ## Acceptance
243
+
244
+ - The site builds with the repository's build command; routes and assets load.
245
+ - Every claimed editable field has a path: schema, reader, component prop, DOM binding, preview,
246
+ public output.
247
+ - No public secrets, wrong media hosts, unscoped instance ids or hard-coded shadow values.
248
+ - `get_binding_report` for each route is clean, and one value of each bound kind opens the right
249
+ editor in the Visual Editor.
250
+ - `get_binding_report`'s `help.percent` is 100: every bound field also tells its editor what it
251
+ is for. Binding a leaf makes it editable; help text makes it usable
252
+ ([content-modeling.md](content-modeling.md), "Writing help text").
253
+
254
+ ## Reading the binding report and the conversion receipt
255
+
256
+ `get_binding_report` per slot (`slot` is `current` or `staging`; on a promote-gated project read
257
+ `current`, the tree the editor frames). Every figure, `unaddressable` included, is measured per
258
+ slot. `sha` is the build the report describes; `refreshRequired` is true when that is not the
259
+ build the slot serves. `builtRoutes` lists the routes the live build has HTML for (null = cannot
260
+ say). `layoutIncomplete` lists Layout paths the last content apply could not write (their publish
261
+ waits); `layoutUnread` lists Layout fields no route reads (`layoutUnreadJudged` false = not
262
+ judged).
263
+
264
+ - **Before any release** there is no report: pages 0, mode null, `refreshRequired` true.
265
+ `refreshReason` `never-generated` means only a release BetterCMS builds and serves writes a
266
+ report, so a headless site never gets one, redeploy or not; `outdated` means the next release
267
+ rewrites it.
268
+ - **`unmatched` reasons**: `not-declared`, `ambiguous-text`, `no-element`, `declared-only`,
269
+ `needs-wrap`, `refused`, `layout-owned`, `page-owned`, `layout-locator-stale`,
270
+ `locale-unvarianted`, `svg-unavailable`, `partial`, `unstamped` (a page list the content apply
271
+ left as built; `kind` reads `list <bound>/<total>`), `mismatch` (a text-match stamp whose element
272
+ shows more than the value, or no longer the text it was matched on, `data-bcms-from`; never
273
+ written) and `report-outdated`.
274
+ - A clean report proves each non-empty field has an element carrying its path, nothing more. A Global
275
+ Section counts only on the routes that render it (`layoutRoutes`).
276
+ - **`unaddressable`** is the copy the report cannot see: visible live text no field owns, by
277
+ route and bucket, biggest first (null = unmeasured: diff the visible text against the entry
278
+ values). Over `count / visible > 0.02`, fix it in the source (wrap the run in an element the
279
+ codemod can bind, or declare `<BcmsField path=…>`), deploy and read again (playbook §11).
280
+ - `canvas.lane` is the editor's live-preview lane (`bridge`, `draft-route`; `none` sends you to
281
+ `get_next_steps`). `coverage.error` `nothing-bound` means no element of this build is bound, so
282
+ its percentage describes another site.
283
+
284
+ `submit_conversion_receipt` answers `complete` (`sourceComplete` is the receipt's own verdict;
285
+ `liveChecks` gives the last release's `structureUnstamped` route count and `reportSha`) and
286
+ `pendingTotal`, and echoes the first 40 pending rows with their `fix`:
287
+ `{ action, file, line, col?, snippet, why?, kind? }`. `action` is one of `wrap-span`,
288
+ `declare-attr`, `bind-expression`, `declare-richtext`, `bind-data`, `manual`; `snippet` and `why`
289
+ say what to change. On a `declare-richtext` with `kind: document`, bind the one element wrapping
290
+ every block of the Body, never the paragraph holding its first. A 404 `unknown-brief` means that
291
+ digest was never issued here. The same run owes the `--forms` receipt, submitted as a second call
292
+ under the same `briefDigest`, and the `styleEdits` of `convert --verify-styles` inside this
293
+ receipt; one listing a changed class, className or style is refused with 409 `style-edits`.
294
+
295
+ The complete brief (`get_conversion_brief { complete: true }`) also carries `forms` (id, status,
296
+ fields, submitUrl). After the binding pass, run the codemod's forms mode (`npx
297
+ @bettercms-ai/convert --forms`, with the brief and a receipt file) to wire them into the
298
+ repository's own `<form>` elements. Read `forms.pending`, and publish every form `forms.notes` names in the Forms tab: a draft
299
+ form rejects every submission with a 403.
300
+
301
+ ## Editor setup, proposals and binding decisions
302
+
303
+ `editor_setup` derives what a build no longer imports: a build brings in SEO/meta, media, brand and
304
+ the models `bcms-content.json` declares; editor setup adds pages, collections, forms, bindings,
305
+ button and section components (when the authoring preference is components), Component Groups and
306
+ the Content sidebar. `get_deploy_status` `editorSetup` reports its state.
307
+
308
+ The complete brief's `proposals` (page 1, at most 50; `proposalsMore` when there are more) list
309
+ what the last convert found and no field owns: `{ id, kind, target, text, suggested?, evidence? }`.
310
+ `id` is `pr_` and 16 hex and survives re-runs; `target` is a `file:line:col` locator only. Kinds:
311
+ `unaddressable` and `code-candidate` (copy no field owns), `inert` (declared, nothing reads it),
312
+ `mismatch` (declares one path, reads another), `flatten` (two leaves in one element).
313
+
314
+ Answer them with `editor_setup { declare: [item, …] }` (at most 100 items, one result each;
315
+ `check: true` with `declare` is a dry run that stores nothing):
316
+
317
+ - `{ op: "accept", ids }` takes each proposal's `suggested` op. A proposal with none refuses.
318
+ - `{ op: "bind", id, path, owner? }` binds the copy to an existing field; `owner` is a route or
319
+ `layout`. The next convert writes it into the source (a mismatch's declaration is rewritten; an
320
+ attribute or an expression is reported `manual` in `receipt.decisionsApplied`). A `bind` waits as
321
+ `decision-pending` until a build proves the new read; one still pending after the next release
322
+ blocks the gate. A declared element is corrected through its `mismatch` proposal id.
323
+ - `{ op: "code", id, why, system? }` keeps it in code. `why` is one of `runtime`, `external`,
324
+ `derived`, `stylesheet`, `decorative`, `embed`, `legal`, and the system checks its evidence;
325
+ without it the item refuses `evidence-missing` (bind it, pick the reason that fits, or a person
326
+ marks it in Overview). The next convert writes `data-bcms-code-owned="text why:<id>"`; the
327
+ `code` counts once a build carries it. Each convert re-checks the
328
+ facts: one whose reason no longer holds becomes `evidence-lost`, blocks the gate, and returns to
329
+ the proposals to decide again. Facts are same-file only: a value from another file needs a person.
330
+ - `{ op: "reject", ids }` drops the suggestion; the copy stays unaddressable. A person's reject of
331
+ an AI `code` is its Undo: the next convert removes the token.
332
+ - `{ op: "clear", decisionId }` undoes a decision.
333
+
334
+ A person's decision beats yours: add `force: true` and a `reason` (shown to them) only when they
335
+ asked for it. Refusals per item: `target-unknown`, `path-not-in-schema`, `owner-mismatch`,
336
+ `locked-by-person`, `slot-mismatch`, `evidence-missing`, `declaration-conflict`, `invalid-item`.
337
+ Until a convert that emits proposals has submitted its receipt, every id refuses `target-unknown`.
338
+ `get_next_steps` `receipt.decisions` counts `stored`, `materialized`, `proven`, `pending` (open
339
+ proposals) and `lost`; stop when `pending` is 0.
340
+
341
+ ### Archive (ZIP) sites
342
+
343
+ A project with no connected repository and no receipt carrying proposals is an ARCHIVE site: no
344
+ codemod runs, so each release scan mints the proposals itself. `target` is then
345
+ `template|owner|textHash` (owner `page`, `layout` or `entry:<collection>`; Layout copy has template
346
+ `*`, an entry route its collection's `/blog/*`), and translations of a page share one template.
347
+ Decisions are not written into source: every release stamps them into the built HTML wherever
348
+ their anchors (structure, text, nearest declared field) agree 2 of 3, else the row is `lost`
349
+ (`anchor-lost`). A `bind` stamps only when the element's text equals the field's stored value; an
350
+ uploaded copy change loses it (`anchor-text-changed`) and its copy is proposed again. An AI `code`
351
+ for `runtime`, `external`, `derived` or `stylesheet` refuses `evidence-missing` here (there is no
352
+ source to check); `decorative`, `embed` and `legal` are checked on the page, and a person may mark
353
+ any reason. Each such decision lives in BetterCMS, not in the site's code: recommend connecting a
354
+ repository.
355
+
356
+ Archive sites also take `{ op: "rule", template, match, then }`, one row for every element it
357
+ matches on every route of `template`:
358
+ - `template`: `/`-segments literal or `*`, a trailing `/**`, or `*` alone (every route). No regex.
359
+ - `match`: at least one of `struct` (a proposal's structure anchor), `cls` (one class token),
360
+ `text` (exact copy); `owner` (`page`, `layout`, `entry`) narrows.
361
+ - `then`: `{ op: "code", why }` or `{ op: "bind", path }`. A bind rule names a `layout` or `entry`
362
+ owner, and matching more than one element on a route is `ambiguous` there.
363
+ A person beats an AI, then a per-id decision beats a rule; two rules saying different things about
364
+ one element are `declaration-conflict`. On a git site a `rule` refuses `invalid-item`. A lane change
365
+ (a repository connected, or a convert's receipt arriving) loses the other lane's decisions
366
+ (`lane-changed`): decide again on the new proposals.