@bettercms-ai/mcp 0.69.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,7 +55,11 @@ build. Pass `apiUrl` and `workspace` as options, or let the snapshot supply them
55
55
 
56
56
  A section root your own code renders spreads `sectionRootAttrs(block)` from `@bettercms-ai/astro`
57
57
  (`data-bcms-block`, `data-bcms-style` and the block's style), so the editor can move it and offer
58
- its spacing handles ([components.md](components.md) §5).
58
+ its spacing handles ([components.md](components.md) §5). It adds `data-bcms-hide` when `style.hideOn` is set: put `RESPONSIVE_CSS` (exported here) in your global CSS.
59
+
60
+ `<BcmsBlocks>` does not render a block outside its `window`, read against the `now` prop (default:
61
+ render time). Pass `showScheduled` on a draft preview to draw it, marked `data-bcms-window`
62
+ ([publishing.md](publishing.md) §6).
59
63
 
60
64
  `bind` and `bindProps` from `@bettercms-ai/astro/bindings` produce the same attributes as the Next
61
65
  helpers. Use them inside component instances so addresses resolve to the instance
@@ -207,3 +207,50 @@ still renders hard-coded copy is not connected. Client components never receive
207
207
  - `get_binding_report`'s `help.percent` is 100: every bound field also tells its editor what it
208
208
  is for. Binding a leaf makes it editable; help text makes it usable
209
209
  ([content-modeling.md](content-modeling.md), "Writing help text").
210
+
211
+ ## Reading the binding report and the conversion receipt
212
+
213
+ `get_binding_report` per slot (`slot` is `current` or `staging`; on a promote-gated project read
214
+ `current`, the tree the editor frames). Every figure, `unaddressable` included, is measured per
215
+ slot. `sha` is the build the report describes; `refreshRequired` is true when that is not the
216
+ build the slot serves. `builtRoutes` lists the routes the live build has HTML for (null = cannot
217
+ say). `layoutIncomplete` lists Layout paths the last content apply could not write (their publish
218
+ waits); `layoutUnread` lists Layout fields no route reads (`layoutUnreadJudged` false = not
219
+ judged).
220
+
221
+ - **Before any release** there is no report: pages 0, mode null, `refreshRequired` true.
222
+ `refreshReason` `never-generated` means only a release BetterCMS builds and serves writes a
223
+ report, so a headless site never gets one, redeploy or not; `outdated` means the next release
224
+ rewrites it.
225
+ - **`unmatched` reasons**: `not-declared`, `ambiguous-text`, `no-element`, `declared-only`,
226
+ `needs-wrap`, `refused`, `layout-owned`, `page-owned`, `layout-locator-stale`,
227
+ `locale-unvarianted`, `svg-unavailable`, `partial`, `unstamped` (a page list the content apply
228
+ left as built; `kind` reads `list <bound>/<total>`), `mismatch` (a text-match stamp whose element
229
+ shows more than the value, or no longer the text it was matched on, `data-bcms-from`; never
230
+ written) and `report-outdated`.
231
+ - A clean report proves each non-empty field has an element carrying its path, nothing more. A Global
232
+ Section counts only on the routes that render it (`layoutRoutes`).
233
+ - **`unaddressable`** is the copy the report cannot see: visible live text no field owns, by
234
+ route and bucket, biggest first (null = unmeasured: diff the visible text against the entry
235
+ values). Over `count / visible > 0.02`, fix it in the source (wrap the run in an element the
236
+ codemod can bind, or declare `<BcmsField path=…>`), deploy and read again (playbook §11).
237
+ - `canvas.lane` is the editor's live-preview lane (`bridge`, `draft-route`; `none` sends you to
238
+ `get_next_steps`). `coverage.error` `nothing-bound` means no element of this build is bound, so
239
+ its percentage describes another site.
240
+
241
+ `submit_conversion_receipt` answers `complete` (`sourceComplete` is the receipt's own verdict;
242
+ `liveChecks` gives the last release's `structureUnstamped` route count and `reportSha`) and
243
+ `pendingTotal`, and echoes the first 40 pending rows with their `fix`:
244
+ `{ action, file, line, col?, snippet, why?, kind? }`. `action` is one of `wrap-span`,
245
+ `declare-attr`, `bind-expression`, `declare-richtext`, `bind-data`, `manual`; `snippet` and `why`
246
+ say what to change. On a `declare-richtext` with `kind: document`, bind the one element wrapping
247
+ every block of the Body, never the paragraph holding its first. A 404 `unknown-brief` means that
248
+ digest was never issued here. The same run owes the `--forms` receipt, submitted as a second call
249
+ under the same `briefDigest`, and the `styleEdits` of `convert --verify-styles` inside this
250
+ receipt; one listing a changed class, className or style is refused with 409 `style-edits`.
251
+
252
+ The complete brief (`get_conversion_brief { complete: true }`) also carries `forms` (id, status,
253
+ fields, submitUrl). After the binding pass, run the codemod's forms mode (`npx
254
+ @bettercms-ai/convert --forms`, with the brief and a receipt file) to wire them into the
255
+ repository's own `<form>` elements. Read `forms.pending`, and publish every form `forms.notes` names in the Forms tab: a draft
256
+ form rejects every submission with a 403.
@@ -25,8 +25,9 @@ Inside a section, pick the smallest shape that holds the children:
25
25
 
26
26
  - **Small identical items** (FAQ rows, feature bullets, logos): a `group` or `table` prop.
27
27
  - **One replaceable child with its own layout or variants** (a hero's media treatment): a `slot`
28
- prop. It holds exactly one instance, and never sits inside a `group` or `table` row.
29
- - **Several different children**: the section's ordered children, not a list of slots.
28
+ prop. It holds one instance, and never sits inside a `group` or `table` row.
29
+ - **Several different children**: the section's ordered children when the set is fixed; a `slot`
30
+ prop with `config.multiple` when the editor picks and orders them.
30
31
 
31
32
  Never let a component contain itself, directly or through a child.
32
33
 
@@ -40,8 +41,9 @@ Never let a component contain itself, directly or through a child.
40
41
  prose body and lists of object rows are sections. Read `pending` for what it will not offer:
41
42
  `NO_GROUP_ROOT` (keys are still the derive lane's own, such as `h1-welcome`: rename them into
42
43
  families first), `NOT_A_SECTION` (a lone scalar or page metadata; `detail: ENTRY_METADATA` is
43
- a template's frontmatter, `detail: DECLINED` a group the user declined) and `IMAGE_AS_TEXT`
44
- (a text field holding an image path; it plans once the field is an image). A nav, footer or
44
+ a template's frontmatter: author, images, tags and dates stay entry fields; `detail: DECLINED`
45
+ is a group the user declined) and `IMAGE_AS_TEXT` (a text field holding an image path; it plans
46
+ once the field is an image). A nav, footer or
45
47
  modal group is `NOT_A_SECTION` with `detail: LAYOUT_CHROME`: chrome is the Layout on every
46
48
  site, never a page band. Author the real sections among those with `create_component` and
47
49
  `set_page_content` instead.
@@ -41,11 +41,31 @@ defaultValue? }`.
41
41
  - A prop takes `showIf` (`{ match, rules: [{ field, op, value? }] }`): hidden until SIBLING props match. `op` is `eq`, `neq`, `in` or `isSet` (no `value`); model fields take the same four.
42
42
  - `update_component` replaces the whole `props` array. Read with `get_component`, change, write
43
43
  all props back.
44
- - `select` and `multiselect` need `config.options`; `slot` holds one nested component instance;
45
- `form` holds one Form document.
44
+ - `select` and `multiselect` need `config.options`; `slot` holds one nested component instance,
45
+ or a list of them with `config.multiple`; `form` holds one Form document.
46
46
  - `slot` and `form` are reference-shaped: `target.path` is `props` (the whole envelope) on an
47
47
  empty `component` or `form` block, and their values are `{ componentId, overrides? }` and
48
48
  `{ formId }`. Neither may appear inside a `group` or `table` row.
49
+ - **A `slot` with `config.multiple: true` holds a list.** Its value is
50
+ `[{ componentId, overrides? }, ...]` and its `target.path` is a container's child list:
51
+ `props.children` on a `section` block, or `props.columns.<n>` on a `columns` block (never
52
+ `props` on a `component` block). Each item is drawn there as a child component, in order.
53
+ Until a placement sets the slot, the children the definition holds in that list are shown;
54
+ `[]` shows none.
55
+ - `config.maxItems` defaults to 12 and cannot pass 50. `config.minItems` and
56
+ `config.componentIds` (which components may be listed) are checked on every write.
57
+ - An item may also carry `style` (use it for `hideOn`) and `window` with `from` / `until`
58
+ as UTC to the second (`2026-11-27T00:00:00Z`). A `defaultValue` must be an array and takes
59
+ no `window`.
60
+ - A write is a 400 whose message starts with the code: `slot_too_many`, `slot_too_few`,
61
+ `slot_item_invalid`, `slot_component_not_allowed`, `slot_component_unknown` (not a
62
+ component of this project), `component_cycle` (a component would end up inside itself,
63
+ through any chain), `component_too_deep` (more than 10 levels), `slot_target_invalid`,
64
+ `slot_not_multiple` (a list sent to a single slot). A value that is already stored is not
65
+ refused when the page is saved again.
66
+ - A child's copy is not click-to-edit on the canvas. Change it in the placement's
67
+ `overrides.<slot key>` list with `update_page`.
68
+ - Publish every listed component: a draft child renders nothing, like any draft component.
49
69
  - **A `form` prop stores an id, nothing more.** Modelling the questions again as props gives you
50
70
  two answers to "what does this form ask?", and they diverge at the first edit on either side.
51
71
  The save schema is `.strict()`, so posting the hydrated form object back is a 400, not a silent
@@ -81,6 +101,12 @@ source? } }`.
81
101
  component with a `sectionType`. A true one-off is a `section` block whose `props.children` hold
82
102
  its blocks.
83
103
  - Components that share a `sectionType` are one family: its variants.
104
+ - **A built-in section is a starting point, not a thing you place.** `list_components` shows the
105
+ locked `builtin:*` blueprints; `create_component` with `from: "builtin:hero-centered"` copies
106
+ one into the project as an ordinary draft component, then edit and publish it like any other.
107
+ Whatever you send (`name`, `slug`, `props`, `blockJson`, `sectionType`, `category`) wins over
108
+ the copy. `list_components` `sourceKey` says which built-in a component came from. A copy
109
+ never follows later changes to the built-in. An unknown id is 404 `BUILTIN_NOT_FOUND`.
84
110
  - **Give every component a library `category` as well.** Why: `sectionType` groups a family, the
85
111
  category is how the component library lists it — an uncategorised component is created fine and
86
112
  is then hard to find in the picker, which is where a human looks for it.
@@ -128,7 +154,7 @@ Handle by code, never by retrying the same call:
128
154
  |---|---|---|
129
155
  | `COMPONENT_BLUEPRINT_MATERIALIZATION_REQUIRED` | You tried to publish a platform built-in | Create a project-local component from it, publish that |
130
156
  | `COMPONENT_PROJECT_SCOPED_RELEASE_REQUIRED` | The component is workspace-global | Create a project-local variant, publish that |
131
- | `COMPONENT_IMPLEMENTATION_NOT_READY` | A REPO-managed component lacks exact evidence and human approval, or a variant-group member's family contract is unpublished. Never a `create_component` component outside a variant group | Report `readiness`; the owner resolves it in the dashboard |
157
+ | `COMPONENT_IMPLEMENTATION_NOT_READY` | A REPO-managed component lacks exact evidence and human approval, or a variant-group member (the provisioned navigation and footer) has a family contract that is unpublished. Never a `create_component` component outside a variant group: that one is dashboard-managed and publishes on the preflight alone | Report `readiness`; the owner resolves it in the dashboard |
132
158
  | `COMPONENT_ORCHESTRATION_V2_REQUIRED` | The project runs implementation in Agent Dock | Continue there; stop here |
133
159
  | `COMPONENT_PUBLISH_CONFLICT` | The component or its evidence changed mid-publish | Re-read with `get_component`, then publish once more |
134
160
  | `COMPONENT_PROJECT_CONTEXT_MISMATCH` | The component's `projectId` differs from the request's project scope | Call with the component's own project |
@@ -311,4 +337,13 @@ stored list.
311
337
  siblings.
312
338
  2. `get_site_composition`: every page is sections; every component you meant to publish is
313
339
  published.
340
+ - Per page: `kind` is `blocks`, `fields-only` or `empty`. `nonComponentBlocks` are loose
341
+ top-level bands the editor's section lane cannot outline.
342
+ - Per placement: the component's `status` is `published`, `draft` or `missing`. `inPicker` is
343
+ false for a component with no `sectionType`: "Add a section" does not offer it.
344
+ `emptyRequiredProps` is reported only here.
345
+ - Site-wide: `components` counts published and draft, `awaitingEvidence` and
346
+ `awaitingApproval`. A person approves in the dashboard, one component at a time. Then the
347
+ `notInPicker` and `unused` ids, and `layout.placements`: the chrome placed in the Global
348
+ Layout draft.
314
349
  3. `get_binding_report` on a page that uses the component: instance values bind.
@@ -301,7 +301,8 @@ production cannot store it. Translate before the first write.
301
301
  | `string` | `text` with `richText: false` | `text` |
302
302
  | `text` | `longtext` | `text` |
303
303
  | `richtext` | `richtext` for section copy; `document` for the one body | `richtext` |
304
- | `number`, `rating` | `number` | `number` |
304
+ | `number` | `number` | `number` |
305
+ | `rating` | `number` with `config.min`, `max`, `step` and `ui.control: "rating"` (stars) | `number` |
305
306
  | `boolean` | `boolean` | `boolean` |
306
307
  | `slug` | `slug` (an entry's own slug stays on the entry) | `text` |
307
308
  | `url`, `link` | `link` | `link`; `url` for a media or embed address |
@@ -309,10 +310,10 @@ production cannot store it. Translate before the first write.
309
310
  | `date`, `datetime` | `date`, `datetime` | `date` (`YYYY-MM-DD`); a datetime stays `text` |
310
311
  | `color` | `color` | `color` |
311
312
  | `select` | `select` | `select` with `config.options` |
312
- | `multiselect`, `tags` | `array` (`itemType: "text"`); a taxonomy is a collection plus `multi-reference` | `multiselect` with `config.options` |
313
+ | `multiselect`, `tags` | `multiselect` with `config.options` (a `string[]`; `config.allowCustom` for free tags, `min`/`max` count the choices); a taxonomy with its own pages is a collection plus `multi-reference` | `multiselect` with `config.options` |
313
314
  | `image` | `image` | `image` |
314
315
  | `file`, `video` | `file`; a hosted video URL is a `link` | `file`; `url` for a hosted video |
315
- | `icon` | `text` with `richText: false` | `icon` (an icon name or an image) |
316
+ | `icon` | `icon` (an icon name or an uploaded image) | `icon` (an icon name or an image) |
316
317
  | `button` | `button` | `button`: one prop targeting the `props` of a `button` block (§4) |
317
318
  | `reference` | `reference`; `multi-reference` for many | none: keep it on the model |
318
319
  | `code` | `longtext` | `text` |
@@ -323,12 +324,14 @@ production cannot store it. Translate before the first write.
323
324
  | `array` of objects | `repeater` | `table` |
324
325
  | `array` of strings | `array` (`itemType: "text"`) | `table` |
325
326
  | `array` of several item shapes | `modular` with `config.blockSlugs` (create the block models first) | separate components |
326
- | `slot` | `sections` or `component-ref` | `slot` (one instance) |
327
+ | `slot` | `sections` or `component-ref` | `slot` (one instance; a list with `config.multiple`) |
327
328
  | a form `reference` | a `form` block inside the section | `form` |
328
329
 
329
- V2 features with no production shape are dropped and reported, never faked: typed table columns,
330
- per-breakpoint values, per-field localization, a slot with several
331
- children (several sections or nested components instead).
330
+ V2 features with no production shape are dropped and reported, never faked: a per-cell table
331
+ type, per-breakpoint values (only `style.hideOn` differs by breakpoint) and per-field
332
+ localization. Two have a shape now: typed table columns are a `repeater` with one field per column
333
+ and `ui.layout: "table"`, and a slot with several children is a `slot` prop with `config.multiple`
334
+ ([components.md](components.md) §1).
332
335
 
333
336
  | The plan says | Production word |
334
337
  |---|---|
@@ -136,7 +136,7 @@ match when the sequence is equal and optional parts are missing only at the ends
136
136
  |---|---|
137
137
  | Two or more siblings of one shape (cards, tiers, logos, questions, steps) | A repeater. Inside a component: a `group` or `table` prop |
138
138
  | A list of plain strings (check-mark bullets) | An `array` of scalars. No icon field, no link field |
139
- | Siblings of different kinds in one grid (a card, a quote, a big number) | The section's ordered children. A `slot` prop holds ONE instance, so it is not a list |
139
+ | Siblings of different kinds in one grid (a card, a quote, a big number) | The section's ordered children. When the editor picks and orders them, a `slot` prop with `config.multiple`; a plain `slot` holds ONE instance |
140
140
  | Same shape, each item linking to its own page | Entries of a collection |
141
141
 
142
142
  Count content levels, not DOM nodes. Component props nest at most three levels; a deeper subtree
@@ -220,7 +220,7 @@ field itself.
220
220
  | Pricing | Calculator | Range sliders with listeners, a live total in `<output>`, no submit |
221
221
  | Conversion | CTA band | Full-width high-contrast band, one heading, at most one sentence, one or two buttons, no inputs |
222
222
  | Conversion | Newsletter | A form with exactly one `type="email"` input, a consent line, a mail-provider action URL |
223
- | Conversion | Lead capture | Copy and bullets beside a multi-field form; fieldsets with Next and Back for multi-step |
223
+ | Conversion | Lead capture | Copy and bullets beside a multi-field form; fieldsets with Next and Back for multi-step (`step` fields) |
224
224
  | Conversion | Contact | `mailto:` and `tel:` links by label, a form, a map iframe, hours |
225
225
  | Conversion | Popup | A hidden `role="dialog"` shown on a timer, scroll or mouse leave |
226
226
  | Listing | Blog index | `<article>` cards with image, heading link, `<time>`, excerpt; a shared href prefix |
@@ -82,6 +82,10 @@ Navigation, footer and dialogs live in the Global Layout (`get_layout`, `update_
82
82
  `get_next_steps` reports `chrome-duplicated`, declare the element in the Layout and push; the page
83
83
  copies are stripped for you. Never delete them by hand, and never give them dock chrome.
84
84
 
85
+ A Global field with `overridable: false` and a `lockReason` is not offered on a page: a page write
86
+ of it answers 403 **LAYOUT_FIELD_NOT_OVERRIDABLE** with the reason. A page value written earlier is
87
+ kept but the Global value renders; `get_layout` scope page lists it under `shadowed`.
88
+
85
89
  ## Acceptance
86
90
 
87
91
  Read back with `get_content_model`, `get_page` or `get_component`. Done means `get_next_steps`
@@ -48,9 +48,10 @@ change it safely.
48
48
 
49
49
  For a complex one-off component, keep the working code and expose only an intentional prop
50
50
  contract. A nested child is edited through a `slot` prop or its own instance; the edit must
51
- address the child instance, never the parent definition. A `slot` holds exactly one nested
52
- component; for several heterogeneous children use the section's ordered children. Prevent
53
- circular nesting.
51
+ address the child instance, never the parent definition. A `slot` holds one nested
52
+ component, or a list of them with `config.multiple`; a fixed set of different children is the
53
+ section's ordered children. A write that would nest a component inside itself is refused
54
+ (`component_cycle`).
54
55
 
55
56
  ## Forms: the rules
56
57
 
@@ -127,7 +128,7 @@ The pipeline with the code that enforces each hop.
127
128
 
128
129
  ### 2. Field types and server checks
129
130
 
130
- Thirteen types exist (`src/lib/validation/schemas/forms.ts:61`, `:62`, `:65`). No file upload.
131
+ Thirteen input types exist (`src/lib/validation/schemas/forms.ts`), plus `step`, a divider. No file upload.
131
132
 
132
133
  | Type | The server checks |
133
134
  |---|---|
@@ -140,6 +141,7 @@ Thirteen types exist (`src/lib/validation/schemas/forms.ts:61`, `:62`, `:65`). N
140
141
  | `url` | Starts with `http://` or `https://` |
141
142
  | `date` | `YYYY-MM-DD` |
142
143
  | `checkbox`, `hidden` | Nothing beyond `required` |
144
+ | `step` | Nothing: a divider is never required and never submitted; a value sent under its key is refused with rule `step` |
143
145
 
144
146
  Any field with `required` is checked for presence. Every value must be a plain value or a flat
145
147
  list of them; a nested object is refused with rule `scalar`. Each refusal is
@@ -159,6 +161,16 @@ Rules:
159
161
  - Read `issues[].field` and `issues[].rule` to fix a refused submission; retrying unchanged returns
160
162
  the same refusal.
161
163
 
164
+ #### Steps
165
+
166
+ A multi-step form is one form with `step` fields in `fields`: `{ key, type: "step", label }` opens
167
+ a page named `label`, and the fields before the first `step` are page 1. A step takes only `key`
168
+ and `label` (`step_has_input_keys`), needs a field after it (`step_empty`), and a form has at most
169
+ ten (`step_limit`). The visitor sees one page at a time with Back and Next, and the form posts
170
+ once, at the end, as one submission whose body never carries a step key. Without JavaScript every
171
+ page shows at once, as one long form. Map a source form's `fieldset` pages, or a "Next" button that
172
+ only reveals the next group, to `step` fields; keep one `required` per real question, not per page.
173
+
162
174
  ### 3. MCP versus dashboard
163
175
 
164
176
  | Operation | Where |
@@ -129,8 +129,8 @@ for a reader before calling a site wired.
129
129
 
130
130
  Read it through one of those, the converted-site helper (`bcms`, `bcmsRows`, `bcmsImage`) or
131
131
  `publishedEntry(slug)`. Never hand-write a parser of the file: each row holds its field values
132
- under `data` (`row.data.title`), so code that reads `row.title` gets `undefined` and renders its
133
- fallback on every publish. An editable image is `bcmsImage(page, path, importedImage)`: the bare
132
+ under `data` (`row.data.title`), so code that reads `row.title` gets `undefined` and the page shows
133
+ its in-code copy after every publish. For an editable image call `bcmsImage(page, path, importedImage)`: the bare
134
134
  import with only a stamp never shows a published image on a route the app renders. An old
135
135
  `src/utils/bcms.ts` with `pageEntry()` is that parser: replace its reads with `publishedEntry`.
136
136
 
@@ -38,11 +38,11 @@ webhook. Use the project's webhook secret from the dashboard (the package docs n
38
38
 
39
39
  | Export | Use |
40
40
  |---|---|
41
- | `BcmsBlocks` | Render a block tree; pass `components` from `readComponents()` |
41
+ | `BcmsBlocks` | Render a block tree; pass `components` from `readComponents()`. A block outside its `window` is not rendered, read against `now` (default: render time); `showScheduled` draws it marked `data-bcms-window`, for a draft preview |
42
42
  | `readComponents()` | Component definitions from `bcms-content.json` |
43
43
  | `BcmsField` | `path`, `kind` (`richtext`/`image`), `as`; server-safe |
44
44
  | `BcmsButton` | A `button` field: `path`, `value`, `className`; one `<a>` with `data-variant` / `data-size` for your CSS; server-safe |
45
- | `sectionRootAttrs(block)` | Spread on a section root your own code renders: `data-bcms-block`, `data-bcms-style` and the block's style ([components.md](components.md) §5) |
45
+ | `sectionRootAttrs(block)` | Spread on a section root your own code renders: `data-bcms-block`, `data-bcms-style` and the block's style, plus `data-bcms-hide` when `style.hideOn` is set: ship `RESPONSIVE_CSS` (exported here) in your global CSS ([components.md](components.md) §5) |
46
46
  | `bind`, `bindProps` | Binding attributes; instance-aware inside components |
47
47
  | `BcmsForm` + `getForm(idOrName)` | Forms from the snapshot ([forms-and-interactions.md](forms-and-interactions.md)) |
48
48
  | `buildMetadata(page, siteDefaults, location?)` | Next `Metadata` from native SEO fields |
@@ -79,6 +79,20 @@ Builds run `build:bcms` instead of `build` when `package.json` declares it. Keep
79
79
  of it and every step the build needs in: `"build": "astro check && astro build"` beside
80
80
  `"build:bcms": "astro build"`. The convert codemod adds one to a repo whose build chains a check.
81
81
 
82
+ ### Reading `get_deploy_status`
83
+
84
+ - `release` is the newest release: `id`, `sha`, `status`, `phaseMarks`, `error`, `createdAt`.
85
+ While an uploaded build is ingested or released the state is `building`, `release.sha` is the
86
+ new sha and `lastLiveSha` the old one. Sending the live sha again does nothing. A failed deploy
87
+ has `release.status` `failed` and its `error`.
88
+ - `held` means the release waits until the hold is cleared; `held.by` is `staff`, `archived` or
89
+ `no-subdomain`.
90
+ - `slot` is where the bytes are: `current` is production, `staging` the staging handle.
91
+ `productionSha` is what production serves. `awaitingPromote: true` is a build that is verified
92
+ and ready on staging while production keeps the older sha, until `promote_project` runs.
93
+ - The poll is finished only when `slot` is `current`, or when `awaitingPromote` is false and
94
+ `productionSha` equals the sha you deployed.
95
+
82
96
  ### Deploy to Vercel or Netlify
83
97
 
84
98
  To serve a `managed` or `hybrid` site from the owner's own Vercel or Netlify account, call
@@ -156,6 +170,7 @@ for a content value the sync got wrong is §4.
156
170
  | Notify another system of an event | Event webhooks, `list_webhooks` / `create_webhook`: `content.publish`, `content.unpublish`, `content.stage_changed` (workflow stage), `content.created`, `content.updated`, `content.deleted` and `deployment.triggered`; HMAC-SHA256 signed in `X-BetterCMS-Signature`; payloads carry ids, never content. The signing `secret` is returned ONCE, on create. A receiver that writes back on `content.updated` loops unless it skips its own writes. Per-subscription payload filters: dashboard | You |
157
171
  | Send form submissions somewhere | Per-form integrations: Google Sheets, Slack, a webhook, email | Dashboard only |
158
172
  | Go live or retire at a time | `scheduledAt` (publish a draft at a time; needs publish permission) and `unpublishAt` (return a published row to draft) on `update_content_entry` and `update_page`; `null` cancels either. Campaigns: `publish_campaign` takes both | You. An edit that omits `scheduledAt` CANCELS the pending publish, so resend it with every edit before it fires; omitting `unpublishAt` never cancels it |
173
+ | Show one section only for a period | `window` `{ from?, until? }` on a page block (`update_page` `blockJson`) or on a Layout Section (`update_layout` `add-section`, `update-section`). Shown from `from` until `until`; each is an ISO 8601 datetime with an offset (`2026-11-27T00:00:00+01:00`), stored as UTC, and either may be left out. `null` clears it. Refusals: `invalid_window` (no offset, or not a date), `window_inverted` (`from` not before `until`), `window_in_component` (inside a component's `blockJson`: put it on the placement), `window_in_entry` (on a block inside an entry's sections value: put it on the page block or Layout Section that shows the entry). A Section a page adds or customizes takes a window like a Global one | You. Outside its window the block is in no published output: the page, its Markdown, the delivery API and search. A draft preview draws it, marked. A page BetterCMS renders per request changes at the boundary; a static site catches up within about a minute, in place when a section goes and with a build when one appears. Site code that renders its own header ignores a Section's window |
159
174
  | Load many entries | `create_content_entries`: up to 200 drafts of one collection per call, each row `created` or `failed` (with `externalKey` also `updated` or `unchanged`, so a re-run is safe); then `publish_entries` | You |
160
175
  | React to edits made elsewhere | Poll `list_activity` with `since` set to the time of your last read (1–100 events per call) | You |
161
176
  | Generate or rewrite in bulk with a human gate | AI jobs: `list_ai_jobs`, `get_ai_job`, then `approve_ai_job` or `reject_ai_job` on the user's word | User approves |
@@ -102,7 +102,7 @@ Topics, FAQs, Testimonials) once. Drop any collection the source does not have.
102
102
 
103
103
  | Piece | Where it lives |
104
104
  |---|---|
105
- | Header, footer, announcement bar | Global Layout via `update_layout`. Never a page section. Footer link columns are a repeater of groups; the newsletter box points at a form |
105
+ | Header, footer, announcement bar | Global Layout via `update_layout`. Never a page section. Footer link columns are a repeater of groups; the newsletter box points at a form. A bar or promo that runs for a period takes a `window` on its Section, and a seasonal band one on its page block ([publishing.md](publishing.md) §6) |
106
106
  | Company name, address, phone, social profiles | One place in the Global Layout, read by the footer and the contact page. Site JSON-LD repeats it once in `siteSchema` (`update_seo`) |
107
107
  | Cookie consent | A third-party consent tool stays code. Report it; do not rebuild it as content |
108
108
  | 404, legal, thank-you pages | Static pages in a `System` Content sidebar folder. Thank-you pages are noindex |
@@ -126,7 +126,7 @@ every entry and are not repeated in the tables. SEO fields are never listed.
126
126
  | One or many other entries | `reference`, `multi-reference`. A missing target is refused on write |
127
127
  | Call to action | `button` (label and link in one value). A bare link is `link`; also `email`, `phone` |
128
128
  | Date, date and time | `date`, `datetime`. Keep the zone as a `text` IANA name beside it when the site shows times |
129
- | A spec or comparison table | `repeater` of label and value rows. Models have no typed table field |
129
+ | A spec or comparison table | `repeater` with one field per column (any field type) and `ui.layout: "table"`. There is no separate table field type |
130
130
 
131
131
  An external system's id (`"store_product_id"`, `"ats_job_id"`, `"feed_id"`, `"lms_course_id"`)
132
132
  is a `text` field kept as migration metadata and locked: `readOnly: "developer"` with a
@@ -194,7 +194,7 @@ collection-wide default in `collectionSchemaTypes`. Never a self-serving `Aggreg
194
194
  ### Forms
195
195
 
196
196
  Thirteen field types: `text`, `textarea`, `email`, `phone`, `number`, `url`, `date`, `select`,
197
- `radio`, `checkbox`, `checkboxes`, `consent`, `hidden`. No file upload, so CVs, floor plans and
197
+ `radio`, `checkbox`, `checkboxes`, `consent`, `hidden`, plus the `step` divider for multi-step forms. No file upload, so CVs, floor plans and
198
198
  attachments go to the external system. Payment details never go in a form. Submissions land in
199
199
  the inbox; an editor turns one into an entry. Full pipeline:
200
200
  [forms-and-interactions.md](forms-and-interactions.md).
@@ -208,6 +208,19 @@ the inbox; an editor turns one into an entry. Full pipeline:
208
208
  - Every collection has a `urlPattern`; every changed URL has a 301.
209
209
  - Structured data is built from visible fields, with the type its catalog names.
210
210
 
211
+ ### Built-in sections
212
+
213
+ The section library ships 23 platform blueprints in 17 families, so a standard band needs no new
214
+ component: Hero, Feature grid (three, two, bento), Feature rows, Steps, Timeline, Stats, Logo cloud,
215
+ Testimonial (quote, three cards), Team, Case study, Comparison, Pricing, FAQ, Call to action,
216
+ Capture (newsletter band) and Listing (latest, related). Read one with `list_components`; inline its
217
+ `blockJson` as a `section` block for a one-off band, or copy it with `create_component` `from:
218
+ "builtin:<key>"` for a recurring one (see [components.md](components.md)). Two ship unbound on purpose: the Capture form has no form until you
219
+ set its `form` prop, and both Listing layouts list the page's own entries until you give them a
220
+ `source`; each shows its empty text meanwhile. Not available as a section, so build it in the repo
221
+ or leave it out: mega menu, docs sidebar, cookie consent, exit intent, store locator, map, billing
222
+ toggle, countdown and anything commerce.
223
+
211
224
  ## Portfolio website
212
225
 
213
226
  Use this when the primary job is to show selected work and make an individual or small studio credible. First determine whether the work is private, client-approved, dated, or still under embargo.
@@ -674,7 +687,8 @@ countdown.
674
687
  | Sponsors | `"logo"`, `"tier"` `select`, `"website"` `link` |
675
688
 
676
689
  - **Chrome:** header with a tickets button; an announcement bar whose countdown end is one
677
- `datetime` with its zone. Never migrate a countdown that resets per visitor.
690
+ `datetime` with its zone, and whose Section `window` ends with it. Never migrate a countdown
691
+ that resets per visitor.
678
692
  - **Lists:** a `collection` block with `source` sorts by `"starts_at"`, but its filter values
679
693
  are fixed: none means "today". Upcoming and past lists, and an agenda grouped by day and
680
694
  track, are the site's code filtering on `"starts_at"`. State that in the report; do not fake
@@ -484,10 +484,24 @@ the attributes. What it writes:
484
484
  - Without the definitions passed to the renderer, every instance draws empty: check that before
485
485
  debugging a card.
486
486
 
487
- Not available today: a slot with several children. A `slot` prop holds one instance, so "a column
488
- of mixed blocks" is a `columns` block, not a slot. A tab icon (a tab has a label only) and a
487
+ A slot can hold several children. Give the `slot` prop `config: { multiple: true }` and point its
488
+ target at a container's child list (`props.children` on a `section`, `props.columns.<n>` on a
489
+ `columns` block); a placement then lists the children, each a published component of this project:
490
+
491
+ ```json
492
+ { "type": "component", "id": "tour-cards", "props": { "componentId": "<card grid id>", "overrides": {
493
+ "cards": [
494
+ { "componentId": "<tour card id>", "overrides": { "title": "Plan" } },
495
+ { "componentId": "<stat id>", "overrides": { "value": "3.4x" }, "style": { "hideOn": ["mobile"] } }
496
+ ] } } }
497
+ ```
498
+
499
+ A fixed layout of different blocks that the editor never reorders is still a `columns` block, as
500
+ in the tabs above. Rules and refusal codes: [components.md](components.md).
501
+
502
+ Not available today: a tab icon (a tab has a label only) and a
489
503
  "first open tab" setting. Per-column widths: columns are equal, and `gap` is a whole number of
490
- pixels, not a named size. Per-breakpoint values on any of these.
504
+ pixels, not a named size. Per-breakpoint sizes are not available; `style.hideOn` (`["mobile"]`, `["tablet","desktop"]`; never all three) hides a block at up to two breakpoints, under 768, 768-1023 and 1024 and up, as `display:none` that keeps it in the HTML.
491
505
 
492
506
  ## Checks
493
507
 
@@ -94,7 +94,8 @@ Schema (`create_page` `fields`, or one `add_field` with `pageId` per field):
94
94
  { "key": "hero_secondary", "label": "Secondary button", "type": "button" },
95
95
  { "key": "hero_image", "label": "Background image", "type": "image",
96
96
  "helpText": "Fills the whole band behind the text. Use a photo at least 2560 px wide." },
97
- { "key": "hero_rating", "label": "Rating", "type": "number", "config": { "min": 0, "max": 5, "step": 0.1 } },
97
+ { "key": "hero_rating", "label": "Rating", "type": "number", "config": { "min": 0, "max": 5, "step": 0.1 },
98
+ "ui": { "control": "rating" } },
98
99
  { "key": "hero_rating_label", "label": "Rating source", "type": "text", "richText": false }
99
100
  ]
100
101
  ```
@@ -258,10 +259,9 @@ Bound template (first card):
258
259
  - The icon `<svg>` is not bound: no field is read from text inside an SVG. The name is edited in
259
260
  the side panel and the template turns it into the sprite reference.
260
261
 
261
- Not available today: an `icon` FIELD type with a picker over a named icon set. In a model or
262
- page field a sprite icon is a plain name; only an inline SVG stored as an `image` with
263
- `media: "icon"` gets the editor's icon picker. A component prop does have the `icon` type: a
264
- BetterCMS icon name or an image ([components.md](components.md) §1).
262
+ To make the icon editable, add a field of type `icon`: its value is a BetterCMS icon name or an
263
+ uploaded icon, and a name off the set is refused. A component prop of type `icon` takes the same
264
+ value ([components.md](components.md) §1). An inline SVG stays an `image` with `media: "icon"`.
265
265
 
266
266
  ## 3. Pricing plans with a comparison table
267
267
 
@@ -446,10 +446,19 @@ Bound template (the Growth card and one comparison row; `pricing-01` is the inst
446
446
  - A comparison written as prose instead belongs in a `richtext` prop as a Portable Text `table`
447
447
  ([portable-text.md](portable-text.md)); then the wrapper of that one value is the bound element.
448
448
 
449
- Not available today: typed table columns (money, boolean) and a per-cell type; every cell is text
450
- or one sub-field type for the whole column. Rows keyed to plan ids, and a field only a developer
451
- can change. Offer markup: no JSON-LD is built from prices. Paste it as `schema` on `update_page`
452
- when the page needs it. A fourth plan in the comparison is a new sub-field, a schema change.
449
+ A typed table is available on both sides. On a component each `table` column is one sub-field
450
+ with its own type (`number`, `boolean`, `url`, ...), as `monthly` and `ctaUrl` are above. On a
451
+ model it is a `repeater` (stored as an `array` with `config.zones.repeatable.fields`): one field
452
+ per column, of any field type, `minItems` / `maxItems` on the repeatable zone, and
453
+ `ui.layout: "table"` to edit the rows as a grid. To key rows to plan ids, give the model a `text`
454
+ column with `unique: true`. A value only a developer may change is a model field with
455
+ `readOnly: "developer"` and a `lockReason`.
456
+
457
+ Not available today: a per-cell type (a column has one type for every row), and a caption,
458
+ footnotes or row groups as parts of the table (model them as fields beside it, as `group` and
459
+ `fineprint` are here). Offer markup: no JSON-LD is built from prices. Paste it as `schema` on
460
+ `update_page` when the page needs it. A fourth plan in the comparison is a new sub-field, a schema
461
+ change.
453
462
 
454
463
  ## 4. Testimonials kept as a collection
455
464
 
@@ -576,6 +585,10 @@ Bound template (the section's own values; one card shown):
576
585
  for every page that shows them. The picked list is changed in the side panel.
577
586
  - Draw the stars from `rating`; never store the glyphs.
578
587
 
588
+ One chosen entry on a hosted page is a `collection` block, not a prop: `source: { modelId }`,
589
+ `pinnedIds: ["<entry id>"]`, `limit: 1` and `cardComponentId` naming the card that draws it. Pinned
590
+ entries come first, so the limit keeps exactly that one.
591
+
579
592
  Not available today: a component prop that references entries (there is no reference prop type; it
580
593
  would save and render nothing). A listing chosen by tag or filter on a route the repo's code
581
594
  renders: the `collection` block with `source`, `filters` and `sort` is drawn by the hosted
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettercms-ai/mcp",
3
- "version": "0.69.0",
3
+ "version": "0.70.0",
4
4
  "type": "module",
5
5
  "description": "BetterCMS MCP server — schema, content, forms + components authoring tools over the Management API, authed via OAuth device-code.",
6
6
  "bin": {
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@bettercms-ai/device-auth": "^0.2.0",
35
- "@bettercms-ai/sdk": "^1.27.0",
35
+ "@bettercms-ai/sdk": "^1.28.0",
36
36
  "@modelcontextprotocol/sdk": "^1.30.0",
37
37
  "zod": "^4.3.6"
38
38
  },