@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.
- package/README.md +86 -86
- package/SKILL.md +160 -160
- package/dist/index.js +38 -9
- package/dist/index.js.map +1 -1
- package/dist/kit-catalog.json +1509 -1509
- package/dist/skills/bettercms/SKILL.md +206 -206
- package/dist/skills/bettercms/references/accessibility.md +171 -171
- package/dist/skills/bettercms/references/animation.md +296 -296
- package/dist/skills/bettercms/references/astro.md +148 -148
- package/dist/skills/bettercms/references/bindings.md +366 -366
- package/dist/skills/bettercms/references/componentize.md +185 -185
- package/dist/skills/bettercms/references/components.md +415 -415
- package/dist/skills/bettercms/references/content-experiments.md +62 -62
- package/dist/skills/bettercms/references/content-modeling.md +382 -382
- package/dist/skills/bettercms/references/detection.md +465 -465
- package/dist/skills/bettercms/references/dock-input.md +121 -121
- package/dist/skills/bettercms/references/editor-chrome.md +92 -92
- package/dist/skills/bettercms/references/external-content.md +78 -78
- package/dist/skills/bettercms/references/finish-site.md +377 -377
- package/dist/skills/bettercms/references/forms-and-interactions.md +245 -245
- package/dist/skills/bettercms/references/frameworks.md +159 -159
- package/dist/skills/bettercms/references/getting-started.md +156 -156
- package/dist/skills/bettercms/references/integrations.md +364 -364
- package/dist/skills/bettercms/references/kit-catalog.md +3259 -3259
- package/dist/skills/bettercms/references/kit-scenarios.md +712 -712
- package/dist/skills/bettercms/references/localization.md +85 -85
- package/dist/skills/bettercms/references/media-and-brand.md +181 -181
- package/dist/skills/bettercms/references/next.md +189 -189
- package/dist/skills/bettercms/references/other-frameworks.md +85 -85
- package/dist/skills/bettercms/references/portable-text.md +213 -213
- package/dist/skills/bettercms/references/programmatic-seo.md +98 -98
- package/dist/skills/bettercms/references/publishing.md +289 -289
- package/dist/skills/bettercms/references/seo.md +357 -357
- package/dist/skills/bettercms/references/shopify-headless.md +52 -52
- package/dist/skills/bettercms/references/site-blueprints.md +895 -895
- package/dist/skills/bettercms/references/site-review.md +74 -74
- package/dist/skills/bettercms/references/worked-examples-advanced.md +509 -509
- package/dist/skills/bettercms/references/worked-examples.md +885 -885
- package/dist/skills/bettercms/references/write-limits.md +217 -217
- 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.
|