@bettercms-ai/mcp 0.69.0 → 0.71.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 +195 -75
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +10 -4
- package/dist/skills/bettercms/references/animation.md +235 -0
- package/dist/skills/bettercms/references/astro.md +7 -3
- package/dist/skills/bettercms/references/bindings.md +80 -3
- package/dist/skills/bettercms/references/componentize.md +6 -4
- package/dist/skills/bettercms/references/components.md +58 -11
- package/dist/skills/bettercms/references/content-modeling.md +17 -9
- package/dist/skills/bettercms/references/detection.md +4 -4
- package/dist/skills/bettercms/references/editor-chrome.md +4 -0
- package/dist/skills/bettercms/references/finish-site.md +80 -2
- 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/media-and-brand.md +22 -1
- package/dist/skills/bettercms/references/next.md +6 -5
- package/dist/skills/bettercms/references/publishing.md +46 -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 +26 -11
- package/package.json +2 -2
|
@@ -49,7 +49,7 @@ a product). One page cannot tell chrome from content.
|
|
|
49
49
|
|---|---|---|
|
|
50
50
|
| Real headings and paragraphs inside `<main>` | Server-rendered or static | Parse the raw HTML. Render once anyway to catch client-only bands |
|
|
51
51
|
| An empty `#root`, `#__next`, `#app`, and large script bundles | Client-rendered | Render in a browser and read the rendered DOM |
|
|
52
|
-
| `<astro-island>`, `__NEXT_DATA__`, `window.__NUXT__` | Hydrated islands | Parse the HTML, then render to read the islands. Their inline JSON often holds the data |
|
|
52
|
+
| `<astro-island>`, `__NEXT_DATA__`, `window.__NUXT__` | Hydrated islands | Parse the HTML, then render to read the islands. Their inline JSON often holds the data. Binding their copy: [animation.md](animation.md) §9 |
|
|
53
53
|
|
|
54
54
|
When you render:
|
|
55
55
|
|
|
@@ -136,10 +136,10 @@ 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
|
-
Count content levels, not DOM nodes. Component props nest at most
|
|
142
|
+
Count content levels, not DOM nodes. Component props nest at most ten levels, counting the prop; a deeper subtree
|
|
143
143
|
becomes its own component, placed as a child.
|
|
144
144
|
|
|
145
145
|
**Collections.** A set of records with one shape and their own URLs.
|
|
@@ -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`
|
|
@@ -31,8 +31,86 @@ answers `complete: true` (no path left in `paths.pending`), and after the deploy
|
|
|
31
31
|
`declared`. A build job works from `bettercms://playbook/schema` (the `build_site` prompt carries
|
|
32
32
|
the same flow on a local MCP) and is done when the site is deployed, `get_component_readiness`
|
|
33
33
|
is clean for every component it placed, and `get_next_steps` lists nothing left to do from code.
|
|
34
|
-
Both end in the verify loop of SKILL.md
|
|
35
|
-
|
|
34
|
+
Both end in the verify loop of SKILL.md, and an imported site ends only on the setup gate below.
|
|
35
|
+
One converted page, or a receipt with paths still pending, is not a finished site.
|
|
36
|
+
|
|
37
|
+
## The setup gate: what "finished" is measured by
|
|
38
|
+
|
|
39
|
+
`get_next_steps` returns `receipt.complete` and `receipt.gate`. The platform computes the gate
|
|
40
|
+
from the last release, one verdict per route of the live build. The CLI that launched you reads
|
|
41
|
+
it again when you exit and prints it to the user, so the gate is the report, not your summary.
|
|
42
|
+
While the gate is incomplete that command exits 3 (0 once it is complete; 2 is a wrong command
|
|
43
|
+
line), which is what a script waiting on the setup reads.
|
|
44
|
+
|
|
45
|
+
| Check | Passes when | Where it shows |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `page` | a CMS page, or a collection entry, serves the route | `routes.rows[].page` |
|
|
48
|
+
| `published` | that page, and its entry, is published | `routes.rows[].page` |
|
|
49
|
+
| `twin` | no second page claims the same route | `blocking.rows` |
|
|
50
|
+
| `text` | 2% or less of the visible characters sit outside a field | `routes.rows[].text` |
|
|
51
|
+
| `image` | each image a visitor sees belongs to a field, and the release named no picture it could not make one | `routes.rows[].image`, `imageGaps` |
|
|
52
|
+
| `link` | 95% of the links have a field for the label or the destination | `routes.rows[].link` |
|
|
53
|
+
|
|
54
|
+
`pages` counts routes built, created and published. `coverage` sums `editable` of `total` across
|
|
55
|
+
the site. `routes.rows` is ordered worst first, with `failing` per route and the reasons the
|
|
56
|
+
deriver `skipped` copy there. `blocking.rows` is the work list: each row carries a `detail` with
|
|
57
|
+
its number and a `fix`, the single call to run next. Run the first `fix`, call `get_next_steps`,
|
|
58
|
+
repeat until `complete`. A `fix` of `null` means no tool closes that row (a collection's listing
|
|
59
|
+
route, a URL that cannot be placed, two pages nobody but the owner can choose between): put it to
|
|
60
|
+
the user and stop retrying. Treat a row that its fix did not move the same way.
|
|
61
|
+
|
|
62
|
+
A `page` row on a route setup left without a page carries the pages lane's reason in its
|
|
63
|
+
`detail` (`invalid-path`, `html-too-large`, `page-deleted`, `slug-taken`, `folder-conflict`,
|
|
64
|
+
`entry-not-registered`, `authored-content` and the rest), with what closes it: a change in the
|
|
65
|
+
site's code, another page to move, or the user's decision. Only `unreadable`, `write-failed`
|
|
66
|
+
and `not-registered` carry a `fix`. An `image` row with a `null` fix names a picture by its code
|
|
67
|
+
([media-and-brand.md](media-and-brand.md) Where images hide): the change is in the site's code.
|
|
68
|
+
`imageGaps` also lists the decoration the import declined, which blocks nothing. On a
|
|
69
|
+
collection's listing route the rows that link to its entries are counted as the collection's,
|
|
70
|
+
so the listing page is judged on its own heading, intro and calls to action.
|
|
71
|
+
|
|
72
|
+
### Exceptions go on the record
|
|
73
|
+
|
|
74
|
+
A check you are leaving on purpose is recorded, one row per route and check, with
|
|
75
|
+
`editor_setup` and `exempt: [{ route, check, reason, note }]`. `note` states what is being left
|
|
76
|
+
in a sentence a person can verify. `reason: null` withdraws one.
|
|
77
|
+
|
|
78
|
+
| `reason` | May excuse |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `form-validation-copy` | `text` |
|
|
81
|
+
| `legal-boilerplate` | `text` |
|
|
82
|
+
| `third-party-widget` | `text`, `image`, `link` |
|
|
83
|
+
| `decorative` | `text`, `image` |
|
|
84
|
+
| `duplicate-track` | `text`, `image`, `link` (the cloned track of a marquee or slider) |
|
|
85
|
+
| `client-only-island` | nothing: it lists an island the build cannot show, and the route still meets the bar outside it |
|
|
86
|
+
| `user-declined` | every check except `twin`, and only with the user's own words quoted in `note` (`User: "leave pricing in code"`) |
|
|
87
|
+
|
|
88
|
+
A row excuses what its check read when it was recorded. When the route reads differently
|
|
89
|
+
later (more copy nobody owns, another picture), the row stops counting and the check blocks
|
|
90
|
+
again: record it again if it still stands. The CLI prints every `user-declined` row marked for
|
|
91
|
+
the user to confirm.
|
|
92
|
+
|
|
93
|
+
Refused whatever the note says: `out-of-time`, `out-of-context`, `too-many-routes`,
|
|
94
|
+
`tool-failed`, `not-asked`, `good-enough`. A sentence to the user about a gap is not an
|
|
95
|
+
exception either: the gate stays incomplete, and the CLI exits non-zero with that gap listed.
|
|
96
|
+
|
|
97
|
+
### What setup derives on a collection's item pages
|
|
98
|
+
|
|
99
|
+
Setup reads a few entry pages of each collection and makes one field of everything they render
|
|
100
|
+
at the same place: text, each image (an `<img>` with only a `srcset`, a `<picture>`, a
|
|
101
|
+
background its stylesheet paints), a call to action (one `button`, or a label and its `-href`
|
|
102
|
+
where the project derives pairs), a plain list, and a gallery or spec table as a repeater whose
|
|
103
|
+
rows each entry fills from its own page. The article's container becomes the place the entry's
|
|
104
|
+
`body` is read from and bound, so `body` holds the article alone. An element the site's code
|
|
105
|
+
already declares keeps its declaration and gets no second field. A collection from before this
|
|
106
|
+
gains the fields add-only; its `body` is bound only while no entry of it was edited. Check the
|
|
107
|
+
result per entry route in the gate, never by adding the same fields by hand.
|
|
108
|
+
|
|
109
|
+
Copy the template shows unchanged on every entry (a "Related posts" heading, an "Add to cart"
|
|
110
|
+
button) is one field marked `config.shared`, and its value lives on that field, once: no entry
|
|
111
|
+
holds it. Edit it as a field of any one entry and publish that entry: the value moves onto the
|
|
112
|
+
field and every item page is repainted. Until then it is that entry's draft, shown on no page.
|
|
113
|
+
A translated entry keeps its own words for it. Never write the same value into each entry.
|
|
36
114
|
|
|
37
115
|
Git belongs to the owner. Verification releases go out from the working tree, so the job never
|
|
38
116
|
needs a commit to prove itself. After the final report, ask whether they want the changes
|
|
@@ -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
|
|
|
@@ -72,7 +72,8 @@ H1. Expose typography variants only where the design allows a real choice.
|
|
|
72
72
|
not in each page.
|
|
73
73
|
- **Images.** A stored image value is an object (`url`, `altText`, asset id), never a bare
|
|
74
74
|
string; read it as one. Alt text, decorative state and caption belong to the usage. Keep
|
|
75
|
-
`srcset`/`sizes`, poster, lazy/preload intent and art direction
|
|
75
|
+
`srcset`/`sizes`, poster, lazy/preload intent and art direction (a publish keeps them too:
|
|
76
|
+
[bindings.md](bindings.md) Images). Decorative images render an
|
|
76
77
|
empty `alt`; informative ones need text that fits the context. `update_media` with
|
|
77
78
|
`decorative: true` marks a library asset decorative, so the audit stops asking for its alt text.
|
|
78
79
|
- **Request the size you render.** Mint resize URLs with `@bettercms-ai/image-url`
|
|
@@ -112,6 +113,26 @@ H1. Expose typography variants only where the design allows a real choice.
|
|
|
112
113
|
every video into the media library by default; keep a streaming provider reference where the
|
|
113
114
|
site depends on one.
|
|
114
115
|
|
|
116
|
+
## Where images hide
|
|
117
|
+
|
|
118
|
+
Setup makes every picture an image field backed by a library asset, in code, with no step for
|
|
119
|
+
you: the release adopts each media file the build ships (alt text from the markup, its size, and
|
|
120
|
+
a default focal point you can move with `update_media`), derives the fields, and links each
|
|
121
|
+
value to its asset. Check it, never redo it: the binding report's `elementCoverage.image` is
|
|
122
|
+
`editable / total`, and `imageGaps` names what is not a field, misses first.
|
|
123
|
+
|
|
124
|
+
| Code | The picture | What to do |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `IMG_DATA_URI`, `IMG_NO_SOURCE` | An `<img>` inlined as `data:`, or filled in by a script | Ship it as a file with a real `src` |
|
|
127
|
+
| `BG_PSEUDO_ELEMENT` | A background on `::before` / `::after` | Move it onto the element, or render an `<img>` |
|
|
128
|
+
| `BG_ON_TEXT` | A background on an element that is itself a text field | Wrap the text, so the box and the words are two elements |
|
|
129
|
+
| `SVG_TOO_LARGE` | An inline `<svg>` illustration past the icon size | Ship it as an `.svg` file in an `<img>` |
|
|
130
|
+
| `SVG_SPRITE`, `SVG_NOT_ICON`, `SVG_DECORATIVE` | A `<use>` sprite, a chart, a nameless glyph | Decoration: leave it, or give a logo `role="img"` and an `aria-label` |
|
|
131
|
+
| `BG_SPRITE`, `BG_TINY`, `BG_PATTERN`, `BG_DATA_URI`, `BG_STATE` | A sprite, an icon-sized box, a tile, an inlined texture, a hover image | Decoration: leave it |
|
|
132
|
+
|
|
133
|
+
The first four rows count against the image coverage; the last two do not. A gap you cannot
|
|
134
|
+
close is reported with its code, never described as done.
|
|
135
|
+
|
|
115
136
|
## Media host
|
|
116
137
|
|
|
117
138
|
Use `mediaBaseUrl` from `get_project`: media is served from the CDN origin or the API origin of
|
|
@@ -38,16 +38,17 @@ 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
|
-
| `BcmsField` | `path`, `kind` (`richtext`/`image`), `as`; server-safe |
|
|
44
|
-
| `BcmsButton` | A `button` field: `path`, `value`, `className`; one `<a>` with `data-variant` / `data-size
|
|
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) |
|
|
43
|
+
| `BcmsField` | `path`, `kind` (`richtext`/`image`), `as`; server-safe; no wrapper around a single DOM element ([animation.md](animation.md) §10) |
|
|
44
|
+
| `BcmsButton` | A `button` field: `path`, `value`, `className`; one `<a>` with `data-variant` / `data-size`. Style only the looks the site has ([components.md](components.md) §5); 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, 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 |
|
|
49
49
|
| `llmsTxtRoute(config)` | An `llms.txt` route handler |
|
|
50
|
-
| `imageUrlBuilder({ baseUrl })` | Resize URLs
|
|
50
|
+
| `imageUrlBuilder({ baseUrl })` | Resize URLs; same media-host rules as Astro |
|
|
51
|
+
| `BcmsImage` | An image field as one `<img>`: `path`, `src` (the value), `sizes` or `width`; declares the field and keeps the value's alt, crop and focal point; server-safe |
|
|
51
52
|
|
|
52
53
|
Rules:
|
|
53
54
|
|
|
@@ -79,6 +79,51 @@ 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
|
+
### A section change that is held at the write
|
|
83
|
+
|
|
84
|
+
`update_page` and `compose_pages` save the draft and answer with `holds` when a `blockJson` adds,
|
|
85
|
+
moves or removes a section the live page cannot show yet. Each hold has a `reason`, a `message`,
|
|
86
|
+
the `sections` it is about and the one `action`. Never report such a write as done:
|
|
87
|
+
|
|
88
|
+
- `CODE_FIXED_SECTIONS`: the site's code fixes this page's sections, so no publish ships the
|
|
89
|
+
change. The action is `componentize_sections` with `codemod: true`; after its build, publish.
|
|
90
|
+
- `SECTION_NEEDS_BUILD`: the build draws the page's sections from BetterCMS. Publish: the build
|
|
91
|
+
starts by itself, and a move or a removal needs none.
|
|
92
|
+
- `STRUCTURE_UNMEASURED`: nothing has measured the page. Publish, then read the publish's outcome
|
|
93
|
+
in `get_deploy_status`.
|
|
94
|
+
|
|
95
|
+
### Reading `get_deploy_status`
|
|
96
|
+
|
|
97
|
+
- `release` is the newest release: `id`, `sha`, `status`, `phaseMarks`, `error`, `createdAt`.
|
|
98
|
+
While an uploaded build is ingested or released the state is `building`, `release.sha` is the
|
|
99
|
+
new sha and `lastLiveSha` the old one. Sending the live sha again does nothing. A failed deploy
|
|
100
|
+
has `release.status` `failed` and its `error`.
|
|
101
|
+
- `held` means the release waits until the hold is cleared; `held.by` is `staff`, `archived` or
|
|
102
|
+
`no-subdomain`.
|
|
103
|
+
- `slot` is where the bytes are: `current` is production, `staging` the staging handle.
|
|
104
|
+
`productionSha` is what production serves. `awaitingPromote: true` is a build that is verified
|
|
105
|
+
and ready on staging while production keeps the older sha, until `promote_project` runs.
|
|
106
|
+
- The poll is finished only when `slot` is `current`, or when `awaitingPromote` is false and
|
|
107
|
+
`productionSha` equals the sha you deployed.
|
|
108
|
+
- `publishes` is the newest publishes, value by value. `status` `live` means every published value
|
|
109
|
+
was found in the file the site serves that page from. `partial` means some were not: `summary`
|
|
110
|
+
counts them ("3 of 4 live · 1 held") and `notLive` lists each with its `route`, `address`,
|
|
111
|
+
`state`, `reason` and the one `action` that fixes it. Never tell the user a publish is live
|
|
112
|
+
while it reads `partial`: relay the reason and do the action, then poll again.
|
|
113
|
+
|
|
114
|
+
| `reason` | `state` | The one action |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `NOT_STAMPED` | held | Rebuild: the built page has no element for the field and does not show it |
|
|
117
|
+
| `VALUE_REFUSED` | held | Bind the field to an element that can hold its value |
|
|
118
|
+
| `APP_DOES_NOT_READ` | held | Convert in code: make the page's code read the published field |
|
|
119
|
+
| `ROWS_NEED_BUILD`, `SECTION_NEEDS_BUILD`, `NEEDS_BUILD` | held | Wait for the build the publish started |
|
|
120
|
+
| `STRUCTURE_UNBOUND` | refused | Let BetterCMS control section order (componentize with the codemod) |
|
|
121
|
+
| `SECTION_MISSING` | held | The same: the served page does not render the section |
|
|
122
|
+
| `STYLE_NOT_PAINTABLE` | held | Run editor setup so section roots carry `sectionRootAttrs` |
|
|
123
|
+
| `WINDOW_NOT_OPEN` | held | Nothing: it goes live when its window opens |
|
|
124
|
+
| `ROUTE_NOT_OWNED` | held | Change one of the two page paths that share the URL |
|
|
125
|
+
| `APP_UNREACHABLE`, `NOT_CHECKED`, `EDGE_PENDING`, `IMAGE_UNAVAILABLE`, `STRUCTURE_UNMEASURED` | unverified | Poll again; publish again if it stays |
|
|
126
|
+
|
|
82
127
|
### Deploy to Vercel or Netlify
|
|
83
128
|
|
|
84
129
|
To serve a `managed` or `hybrid` site from the owner's own Vercel or Netlify account, call
|
|
@@ -156,6 +201,7 @@ for a content value the sync got wrong is §4.
|
|
|
156
201
|
| 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
202
|
| Send form submissions somewhere | Per-form integrations: Google Sheets, Slack, a webhook, email | Dashboard only |
|
|
158
203
|
| 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 |
|
|
204
|
+
| 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
205
|
| 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
206
|
| 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
207
|
| 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
|
```
|
|
@@ -108,8 +109,8 @@ Value (`set_page_content`):
|
|
|
108
109
|
"hero_eyebrow": "Freight visibility for mid-size shippers",
|
|
109
110
|
"hero_title": "Know where every container is before your customer asks.",
|
|
110
111
|
"hero_lede": "Live ETAs from 140 carriers, delays flagged before they cost you, and one shared view for ops and sales.",
|
|
111
|
-
"hero_primary": { "label": "Book a demo", "href": "/demo", "
|
|
112
|
-
"hero_secondary": { "label": "Start free trial", "href": "https://app.kestrelfreight.com/signup"
|
|
112
|
+
"hero_primary": { "label": "Book a demo", "href": "/demo", "arrow": true },
|
|
113
|
+
"hero_secondary": { "label": "Start free trial", "href": "https://app.kestrelfreight.com/signup" },
|
|
113
114
|
"hero_image": {
|
|
114
115
|
"url": "/img/hero-harbor-2560.jpg",
|
|
115
116
|
"altText": "Container cranes over a quiet harbor at dusk",
|
|
@@ -148,6 +149,8 @@ Bound template (classes unchanged, shortened here):
|
|
|
148
149
|
- The image binding sits on the `<img>`, not the `<picture>`. The alt text travels in the image
|
|
149
150
|
value (`altText`): a publish writes it to the bound element's `alt`. When `get_conversion_brief`
|
|
150
151
|
lists a separate alt path, declare that path in `data-bcms-props` with kind `text`.
|
|
152
|
+
- Neither button value names a `variant`: the anchors keep the site's own `btn-primary` and
|
|
153
|
+
`btn-ghost`, and the release reads those two looks into the project's Button family.
|
|
151
154
|
- An `href` is kind `url`, never `text`. The label gets its own `<span>` so a save does not erase
|
|
152
155
|
the arrow; the arrow is drawn by `BcmsButton` ([components.md](components.md)), so remove the
|
|
153
156
|
hand-written `<svg>` once the button renders through it.
|
|
@@ -258,10 +261,9 @@ Bound template (first card):
|
|
|
258
261
|
- The icon `<svg>` is not bound: no field is read from text inside an SVG. The name is edited in
|
|
259
262
|
the side panel and the template turns it into the sprite reference.
|
|
260
263
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
BetterCMS icon name or an image ([components.md](components.md) §1).
|
|
264
|
+
To make the icon editable, add a field of type `icon`: its value is a BetterCMS icon name or an
|
|
265
|
+
uploaded icon, and a name off the set is refused. A component prop of type `icon` takes the same
|
|
266
|
+
value ([components.md](components.md) §1). An inline SVG stays an `image` with `media: "icon"`.
|
|
265
267
|
|
|
266
268
|
## 3. Pricing plans with a comparison table
|
|
267
269
|
|
|
@@ -446,10 +448,19 @@ Bound template (the Growth card and one comparison row; `pricing-01` is the inst
|
|
|
446
448
|
- A comparison written as prose instead belongs in a `richtext` prop as a Portable Text `table`
|
|
447
449
|
([portable-text.md](portable-text.md)); then the wrapper of that one value is the bound element.
|
|
448
450
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
451
|
+
A typed table is available on both sides. On a component each `table` column is one sub-field
|
|
452
|
+
with its own type (`number`, `boolean`, `url`, ...), as `monthly` and `ctaUrl` are above. On a
|
|
453
|
+
model it is a `repeater` (stored as an `array` with `config.zones.repeatable.fields`): one field
|
|
454
|
+
per column, of any field type, `minItems` / `maxItems` on the repeatable zone, and
|
|
455
|
+
`ui.layout: "table"` to edit the rows as a grid. To key rows to plan ids, give the model a `text`
|
|
456
|
+
column with `unique: true`. A value only a developer may change is a model field with
|
|
457
|
+
`readOnly: "developer"` and a `lockReason`.
|
|
458
|
+
|
|
459
|
+
Not available today: a per-cell type (a column has one type for every row), and a caption,
|
|
460
|
+
footnotes or row groups as parts of the table (model them as fields beside it, as `group` and
|
|
461
|
+
`fineprint` are here). Offer markup: no JSON-LD is built from prices. Paste it as `schema` on
|
|
462
|
+
`update_page` when the page needs it. A fourth plan in the comparison is a new sub-field, a schema
|
|
463
|
+
change.
|
|
453
464
|
|
|
454
465
|
## 4. Testimonials kept as a collection
|
|
455
466
|
|
|
@@ -576,6 +587,10 @@ Bound template (the section's own values; one card shown):
|
|
|
576
587
|
for every page that shows them. The picked list is changed in the side panel.
|
|
577
588
|
- Draw the stars from `rating`; never store the glyphs.
|
|
578
589
|
|
|
590
|
+
One chosen entry on a hosted page is a `collection` block, not a prop: `source: { modelId }`,
|
|
591
|
+
`pinnedIds: ["<entry id>"]`, `limit: 1` and `cardComponentId` naming the card that draws it. Pinned
|
|
592
|
+
entries come first, so the limit keeps exactly that one.
|
|
593
|
+
|
|
579
594
|
Not available today: a component prop that references entries (there is no reference prop type; it
|
|
580
595
|
would save and render nothing). A listing chosen by tag or filter on a route the repo's code
|
|
581
596
|
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.71.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.29.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
37
37
|
"zod": "^4.3.6"
|
|
38
38
|
},
|