@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.
- package/dist/index.js +38 -27
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/references/astro.md +5 -1
- package/dist/skills/bettercms/references/bindings.md +47 -0
- package/dist/skills/bettercms/references/componentize.md +6 -4
- package/dist/skills/bettercms/references/components.md +38 -3
- package/dist/skills/bettercms/references/content-modeling.md +10 -7
- package/dist/skills/bettercms/references/detection.md +2 -2
- package/dist/skills/bettercms/references/editor-chrome.md +4 -0
- package/dist/skills/bettercms/references/forms-and-interactions.md +16 -4
- package/dist/skills/bettercms/references/frameworks.md +2 -2
- package/dist/skills/bettercms/references/next.md +2 -2
- package/dist/skills/bettercms/references/publishing.md +15 -0
- package/dist/skills/bettercms/references/site-blueprints.md +18 -4
- package/dist/skills/bettercms/references/worked-examples-advanced.md +17 -3
- package/dist/skills/bettercms/references/worked-examples.md +22 -9
- package/package.json +2 -2
|
@@ -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
|
|
29
|
-
- **Several different children**: the section's ordered children
|
|
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,
|
|
44
|
-
(a text field holding an image path; it plans
|
|
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
|
|
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
|
|
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` | `
|
|
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` | `
|
|
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:
|
|
330
|
-
per-breakpoint values
|
|
331
|
-
|
|
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.
|
|
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
|
|
52
|
-
component
|
|
53
|
-
|
|
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
|
|
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
|
|
133
|
-
|
|
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`
|
|
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
|
|
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
|
|
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
|
-
|
|
488
|
-
|
|
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
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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.
|
|
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.
|
|
35
|
+
"@bettercms-ai/sdk": "^1.28.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
37
37
|
"zod": "^4.3.6"
|
|
38
38
|
},
|