@bettercms-ai/mcp 0.71.0 → 0.72.1
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 +365 -46
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +9 -6
- package/dist/skills/bettercms/references/animation.md +21 -5
- package/dist/skills/bettercms/references/astro.md +72 -0
- package/dist/skills/bettercms/references/bindings.md +15 -2
- package/dist/skills/bettercms/references/componentize.md +73 -6
- package/dist/skills/bettercms/references/components.md +54 -11
- package/dist/skills/bettercms/references/content-modeling.md +6 -1
- package/dist/skills/bettercms/references/finish-site.md +116 -9
- package/dist/skills/bettercms/references/forms-and-interactions.md +29 -0
- package/dist/skills/bettercms/references/getting-started.md +2 -0
- package/dist/skills/bettercms/references/media-and-brand.md +20 -3
- package/dist/skills/bettercms/references/next.md +87 -0
- package/dist/skills/bettercms/references/publishing.md +75 -7
- package/dist/skills/bettercms/references/seo.md +2 -1
- package/package.json +1 -1
|
@@ -9,6 +9,13 @@ description: "Take an imported or converted site to fully editable: the gap ledg
|
|
|
9
9
|
verified with evidence. "Fully editable" is a measured result, never an inference from imported
|
|
10
10
|
schemas. New to the project, or no repo on disk yet: [getting-started.md](getting-started.md).
|
|
11
11
|
|
|
12
|
+
## Contents
|
|
13
|
+
|
|
14
|
+
- [Inputs and baseline](#inputs-and-baseline) · [The finish line for a convert or build job](#the-finish-line-for-a-convert-or-build-job)
|
|
15
|
+
- [The setup gate: what "finished" is measured by](#the-setup-gate-what-finished-is-measured-by)
|
|
16
|
+
- [The gap ledger](#the-gap-ledger-bettercmsledgerjson) · [Repair order](#repair-order-and-the-reference-for-each-step)
|
|
17
|
+
- [Proof loop](#proof-loop) · [Launch decision](#launch-decision)
|
|
18
|
+
|
|
12
19
|
## Inputs and baseline
|
|
13
20
|
|
|
14
21
|
Require the project, source repository or import snapshot, site URL, import manifest, hosting
|
|
@@ -50,15 +57,100 @@ line), which is what a script waiting on the setup reads.
|
|
|
50
57
|
| `text` | 2% or less of the visible characters sit outside a field | `routes.rows[].text` |
|
|
51
58
|
| `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
59
|
| `link` | 95% of the links have a field for the label or the destination | `routes.rows[].link` |
|
|
60
|
+
| `sections` | every section the page places is a stamped root on the built page, none twice, none published after the build, and a section added in BetterCMS has somewhere to show (`accepts` `list` or `in-place`) | `routes.rows[].sections`: `accepts`, `renderer` (`sdk`, `registry`, `annotator`), and when it fails `refusal` (why the release cannot write the page) and `wrap` (`wrapped`, or `refused:<REASON>` from the sections conversion) |
|
|
61
|
+
| `style` | every stamped section root carries the style marker (`data-bcms-style`, or `data-bcms-block-style` on a root that is a field's element) or is drawn by the route's block renderer, counted by setup on the served page, so a block's spacing, padding and background paint | `routes.rows[].style` |
|
|
62
|
+
| `svg` | every inline SVG a visitor sees (a logo, an icon, an illustration) belongs to a field; a hidden symbol sheet is not counted | `routes.rows[].svg` |
|
|
63
|
+
| `number` | every element whose whole text is a number (a stat, a price, a rating) belongs to a field | `routes.rows[].number` |
|
|
64
|
+
| `attribute` | every `alt`, `aria-label`, `title` and `placeholder` with words in it belongs to a field | `routes.rows[].attribute` |
|
|
65
|
+
| `names` | no field of the page, and no component, has a name a person cannot read: a slug, a hash, a structural word (`Section 2`, `wrap`), or a number beside the same name (`Icon 2`) | `blocking.rows` |
|
|
66
|
+
| `seo` | the page has a title of at most 70 characters and a description of at most 200, and a canonical where the build ships one for this site's origin; site-wide, every page has a share image, its own or the site's default | `blocking.rows` |
|
|
67
|
+
| `structuredData` | JSON-LD the build ships on the route is on the page or the entry | `blocking.rows` |
|
|
68
|
+
| `collections` | sibling routes that are one family (three under one parent, or two under `tags`, `category`, `author`) are a collection; reported on the family's listing route, built or not | `blocking.rows` |
|
|
69
|
+
| `buttons` | every button field is one of the site's live Button looks, the Button family is the looks setup measured on this build (the same looks, not the same count), and no look is named by a number or a build's class | `blocking.rows`, `route: null` |
|
|
70
|
+
| `components` | the authoring question is answered, and no section is left that a componentize run would place | `blocking.rows`, `route: null` |
|
|
71
|
+
| `forms` | every form of the customer's has an address to notify; a form setup ignored (sign-in, add to basket) is not counted | `blocking.rows`, `route: null` |
|
|
53
72
|
|
|
54
73
|
`pages` counts routes built, created and published. `coverage` sums `editable` of `total` across
|
|
55
|
-
the site
|
|
74
|
+
the site, `svg`, `number` and `attribute` included, and `open` counts the rows still open per
|
|
75
|
+
check. `routes.rows` is ordered worst first, with `failing` per route and the reasons the
|
|
56
76
|
deriver `skipped` copy there. `blocking.rows` is the work list: each row carries a `detail` with
|
|
57
|
-
its number and a `fix`, the
|
|
58
|
-
|
|
77
|
+
its number, a `fixCode` and a `fix`, the call that closes it.
|
|
78
|
+
|
|
79
|
+
`nextAction` is the one thing to do now: the fix a tool can make that closes the most rows
|
|
80
|
+
(`fixCode`, `count`, the `fix` of its first row, the first `routes`), two with the same count in a
|
|
81
|
+
fixed order. Run it on every row that carries that `fixCode`, call `get_next_steps`, and repeat
|
|
82
|
+
until `complete`: following it reaches `complete` with no step that changes nothing. A fix no
|
|
83
|
+
tool can make is named only when nothing else is left. A `fix` of `null` means no tool closes that row (a collection's listing
|
|
59
84
|
route, a URL that cannot be placed, two pages nobody but the owner can choose between): put it to
|
|
60
85
|
the user and stop retrying. Treat a row that its fix did not move the same way.
|
|
61
86
|
|
|
87
|
+
A `sections` or `style` row is a page nobody can add a section to, reorder or space, however
|
|
88
|
+
complete its copy is. Its `fixCode` names the one fix, and `fix` the call when a tool can make it:
|
|
89
|
+
|
|
90
|
+
| `fixCode` | The fix |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `run-sections-conversion` | Only the site's code can draw this page's sections: its app renders the route, a framework hydrates the page whole (`PAGE_HYDRATED`), or the release could not map its bands. In a checkout run `npx @bettercms-ai/convert@latest --sections --root .` ([componentize.md](componentize.md) §5), or call `componentize_sections` with `codemod: true` and the plan's `digest`; `--upgrade-sections` for a `style` row. Then push and release |
|
|
93
|
+
| `rebuild` | The build that serves is older than what makes the row pass: a published section, the wrap's commit (`wrap: "wrapped"` on a row that still fails), or the in-place switch. No tool starts one: ask the user to push and Rebuild |
|
|
94
|
+
| `enable-in-place-structure` | A static page the release can write by itself, and the project's in-place structure switch is off. Setup switches it on for a project it creates; on an existing one it is the operator's call. No repository change |
|
|
95
|
+
| `run-setup` | Nothing measured what the row reads, or a page is still to register: `editor_setup { force: true }`, then read again |
|
|
96
|
+
|
|
97
|
+
The other rows name their fix the same way:
|
|
98
|
+
|
|
99
|
+
| `fixCode` | The fix |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `bind-elements` | `get_conversion_brief { complete: true }` lists what no field owns on the route; bind each (an SVG as an image or icon field, a number as its own field, attribute copy with `data-bcms-props`) |
|
|
102
|
+
| `rename-fields` | `get_page`, then `update_field` with a name a person would say; `update_component` for a component |
|
|
103
|
+
| `fill-seo` | `update_page` with `metaTitle` and `metaDescription` (`update_content_entry` with `meta` for an entry; `generate_seo_meta` drafts one from text you give it); a missing canonical is setup's to copy from the built head, so that row carries `editor_setup { force: true }`; the site-wide row wants the site's default share image, `update_seo` with `seoDefaults.ogImage` |
|
|
104
|
+
| `import-structured-data` | `editor_setup { force: true }` imports the build's JSON-LD; `update_seo` when it has to be written by hand |
|
|
105
|
+
| `model-collection` | `create_content_model` with the family's `urlPattern` (`/tags/:slug`), then its entries: one item template for the family ([content-modeling.md](content-modeling.md)) |
|
|
106
|
+
| `derive-buttons` | `editor_setup { force: true }` mints a Button for each look the build draws, retires one it no longer draws and maps each button to its look; rename a look with `update_component` ([components.md](components.md)) |
|
|
107
|
+
| `componentize` | `set_authoring_preference` when nobody answered, then `get_componentize_plan` and `componentize_sections` ([componentize.md](componentize.md)) |
|
|
108
|
+
| `set-form-recipient` | ask the user where the form's submissions go, then `update_form` with `notifyEmails`; `update_form { ignored: true }` when it is not one of their forms |
|
|
109
|
+
|
|
110
|
+
A row nothing measured reads `unmeasured: true` with `run-setup`, and `gate.unmeasured` lists the
|
|
111
|
+
checks that have one: a release older than a count, or a project set up before setup recorded
|
|
112
|
+
what it found on the build (the style marker of each section root, the looks it measured, the
|
|
113
|
+
canonicals the build ships). It is not a failure of the site and not a pass either: run
|
|
114
|
+
`editor_setup { force: true }` once and read again.
|
|
115
|
+
|
|
116
|
+
Setup closes what is deterministic by itself, on every run, so your first receipt is already past
|
|
117
|
+
it and an open row of these means run setup again: `run-setup`, `derive-buttons` (the family and
|
|
118
|
+
the map), `import-structured-data`, `fill-seo` from the built head and the page's own copy,
|
|
119
|
+
`rename-fields` for a field still carrying a derived name, `componentize` once the authoring
|
|
120
|
+
answer is `components`, and `enable-in-place-structure` on a project it creates. What is left
|
|
121
|
+
takes judgement and is yours: `bind-elements`, `model-collection`, `set-form-recipient` (ask the
|
|
122
|
+
user), `run-sections-conversion`, a name a person gave, a title over its limit, a look named by a
|
|
123
|
+
number, and the authoring answer itself (the user's). `rebuild` is the user's.
|
|
124
|
+
|
|
125
|
+
Beside the gate, `data` lists what else is unfinished, each item by its `ruleId`: content
|
|
126
|
+
(`mcp-pages-draft`, `mcp-pages-missing-meta`, `mcp-model-empty`, `mcp-form-no-recipient`,
|
|
127
|
+
`mcp-pending-review`), bindings and structure (`conversion-pending`, `bindings-not-declared`,
|
|
128
|
+
`unaddressable-copy`, `upgrade-section-roots`, `upgrade-element-styles`, `componentize-pages`,
|
|
129
|
+
`components-unpublished`), the authoring answer (`authoring-undecided`), the Layout lane
|
|
130
|
+
(`layout-write-blocked`, `layout-field-unread`, `layout-locator-stale`, `layout-rebuild-required`,
|
|
131
|
+
`layout-unpublished`, `chrome-duplicated`), publishes (`publish-held`) and the Content sidebar
|
|
132
|
+
(`organise-content`). `setup-gate` is first while the gate is open and `publish-held` next.
|
|
133
|
+
|
|
134
|
+
Stamps are not enough to pass either check: the lane that serves a route must have PROVED it.
|
|
135
|
+
Each release adds a probe section to the middle of one built page of each kind (a page, a
|
|
136
|
+
listing, an item template) and a probe padding to one of its sections, in memory, finds both in
|
|
137
|
+
the result and takes them out again; the sections conversion proves each page it wraps the same
|
|
138
|
+
way. `routes.rows[].proof` is that record (`lane`, `route`, `insert`, `style`: `ok` or the
|
|
139
|
+
reason). A row whose proof was never run asks for it where it runs: `editor_setup { force: true }`
|
|
140
|
+
for the release, the conversion again for a wrapped page. `SLOTS_NOT_PLACED` on a wrapped page
|
|
141
|
+
means every slot is still `code:`: place the page's sections, then run the conversion again.
|
|
142
|
+
|
|
143
|
+
The gate picks the fix per route, in that order of evidence: a wrapped page waits for its build; a
|
|
144
|
+
page a script draws needs the conversion; a static page needs only the switch. Run the fix the
|
|
145
|
+
row names and no other: the conversion does nothing for a row that says `rebuild`.
|
|
146
|
+
|
|
147
|
+
Two rows have no `fixCode`. A page that places a section twice: remove the second placement with
|
|
148
|
+
`update_page`. A route neither lane serves: the release cannot write it (`refusal`) and the
|
|
149
|
+
conversion refused it (`wrap`); its `detail` names both. Put it to the user: write the wrap by
|
|
150
|
+
hand ([astro.md](astro.md), [next.md](next.md)) or change the page. No exception covers it
|
|
151
|
+
(`document-body` excuses only a route that places no section). Check a route's row again after
|
|
152
|
+
each release; never mark one done from the codemod's own output.
|
|
153
|
+
|
|
62
154
|
A `page` row on a route setup left without a page carries the pages lane's reason in its
|
|
63
155
|
`detail` (`invalid-path`, `html-too-large`, `page-deleted`, `slug-taken`, `folder-conflict`,
|
|
64
156
|
`entry-not-registered`, `authored-content` and the rest), with what closes it: a change in the
|
|
@@ -67,7 +159,17 @@ and `not-registered` carry a `fix`. An `image` row with a `null` fix names a pic
|
|
|
67
159
|
([media-and-brand.md](media-and-brand.md) Where images hide): the change is in the site's code.
|
|
68
160
|
`imageGaps` also lists the decoration the import declined, which blocks nothing. On a
|
|
69
161
|
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
|
|
162
|
+
so the listing page is judged on its own heading, intro and calls to action, and on the words
|
|
163
|
+
of a row the release bound to its entry (a field's there too). The release binds
|
|
164
|
+
each such row to its entry: the row carries `data-bcms-entry`, a part showing a field as stored
|
|
165
|
+
carries `data-bcms-entry-field` (`entry:<id>:<path>`, written when that entry publishes), and a
|
|
166
|
+
part the site's code made from a field (a cut excerpt, a reformatted date or price, a thumbnail,
|
|
167
|
+
the item link) carries `data-bcms-entry-from` with the transform, read-only: edit the entry.
|
|
168
|
+
Never add a field for a row to the listing page. A taxonomy's term routes (`/tags/<term>`, each
|
|
169
|
+
listing another collection's entries) are a small collection of terms, and their rows are bound
|
|
170
|
+
the same way. A term has a `body` only where its template has an article. Chrome the Layout
|
|
171
|
+
could not take (an icon with no Media asset) is template copy on item routes: one shared field
|
|
172
|
+
of the collection, as it is a page's own field on a page.
|
|
71
173
|
|
|
72
174
|
### Exceptions go on the record
|
|
73
175
|
|
|
@@ -79,11 +181,16 @@ in a sentence a person can verify. `reason: null` withdraws one.
|
|
|
79
181
|
|---|---|
|
|
80
182
|
| `form-validation-copy` | `text` |
|
|
81
183
|
| `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) |
|
|
184
|
+
| `third-party-widget` | `text`, `image`, `link`, `svg`, `number`, `attribute` |
|
|
185
|
+
| `decorative` | `text`, `image`, `svg` |
|
|
186
|
+
| `duplicate-track` | `text`, `image`, `link`, `svg`, `number`, `attribute` (the cloned track of a marquee or slider) |
|
|
85
187
|
| `client-only-island` | nothing: it lists an island the build cannot show, and the route still meets the bar outside it |
|
|
86
|
-
| `user-declined` |
|
|
188
|
+
| `user-declined` | `page`, `published`, `text`, `image`, `link`, `svg`, `number`, `attribute`, `seo`, `structuredData`, `collections`, and only with the user's own words quoted in `note` (`User: "leave pricing in code"`) |
|
|
189
|
+
| `document-body` | `sections`, and only on a route that places no section at all: a page that is one document (a markdown post, a legal page) |
|
|
190
|
+
|
|
191
|
+
Nothing else excuses `sections`, and nothing excuses `style`, `twin`, `names`, `buttons`,
|
|
192
|
+
`components` or `forms`: not the user's decision either. A name is fixed by renaming, and a form
|
|
193
|
+
that is not the customer's is marked ignored.
|
|
87
194
|
|
|
88
195
|
A row excuses what its check read when it was recorded. When the route reads differently
|
|
89
196
|
later (more copy nobody owns, another picture), the row stops counting and the check blocks
|
|
@@ -149,7 +256,7 @@ and add the new result. Update the file as each row moves, so a reset loses at m
|
|
|
149
256
|
|---|---|---|
|
|
150
257
|
| 0 | Editor setup: `editor_setup { check: true }` reads `ready` for the live build (run `editor_setup` when it does not) | [getting-started.md](getting-started.md) |
|
|
151
258
|
| 1 | Routing and data shape: page / collection / entry classification and folders; keep public URLs | [content-modeling.md](content-modeling.md) |
|
|
152
|
-
| 2 | Sections and reuse: every band a section; dedupe compatible patterns into variants and overrides; nav and footer as globals (`get_componentize_plan`, `componentize_sections` on a components project) | [components.md](components.md), [componentize.md](componentize.md) |
|
|
259
|
+
| 2 | Sections and reuse: every band a section; dedupe compatible patterns into variants and overrides; nav and footer as globals (`get_componentize_plan`, `componentize_sections` on a components project). `bettercms init` has already wrapped each page's sections in `<BcmsSections>` and committed it: fix only the pages it refused ([componentize.md](componentize.md) §5) | [components.md](components.md), [componentize.md](componentize.md) |
|
|
153
260
|
| 3 | Editor contract: field types, labels, help text on every element (`get_next_steps` shows no `help-text-missing`), validation, Portable Text accept lists, nested groups | [portable-text.md](portable-text.md) |
|
|
154
261
|
| 4 | Design: media, logo roles, fonts and brand tokens, without changing visual intent | [media-and-brand.md](media-and-brand.md) |
|
|
155
262
|
| 5 | Bindings: the site reads BetterCMS data and each intended leaf or attribute is bound; build, preview and runtime agree | [bindings.md](bindings.md), [frameworks.md](frameworks.md) |
|
|
@@ -184,6 +184,35 @@ only reveals the next group, to `step` fields; keep one `required` per real ques
|
|
|
184
184
|
|
|
185
185
|
`update_form` with `fields` replaces the whole array: send every field you want to keep.
|
|
186
186
|
|
|
187
|
+
Setup imports two kinds of form already ignored, each kept as a restorable row: the site's own
|
|
188
|
+
sign-in, register and password forms, and a shop's add-to-basket, buy-now or checkout form (it
|
|
189
|
+
posts to the cart and asks only a quantity or a variant). Both stay the site's code. An imported
|
|
190
|
+
form is named for what it is for: its `aria-label`, its one legend, a heading inside it or in its
|
|
191
|
+
own band, else its purpose ("Newsletter signup", "Contact"). Rename it with `update_form` if the
|
|
192
|
+
site calls it something else.
|
|
193
|
+
|
|
194
|
+
**An imported form's words are edited in the form, and written into the built page.** On a new
|
|
195
|
+
import, setup binds each part of the built `<form>` to its form row: field labels, placeholders,
|
|
196
|
+
help lines, select and radio options, and the submit button's text. Change one with `update_form`
|
|
197
|
+
(`fields[].label`, `placeholder`, `helpText`, `options`, or `submitLabel`) and the next content
|
|
198
|
+
apply writes it into every built page that draws the form, with no rebuild. The built form
|
|
199
|
+
carries the stamps:
|
|
200
|
+
|
|
201
|
+
| Stamp | On | Says |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `data-bcms-form-bound="<formId>"` | the `<form>` | Its parts are this form's. Never `data-bcms-form`: that one means the form posts to BetterCMS. |
|
|
204
|
+
| `data-bcms-form-part` | a part | `label`, `help`, `option` or `submit`, as `BcmsForm` names them. |
|
|
205
|
+
| `data-bcms-form-field="<key>"` | a part or a control | The field it belongs to. |
|
|
206
|
+
| `data-bcms-form-value="form:<formId>:<path>"` | a part shown as text | The address written: `fields.<key>.label`, `fields.<key>.helpText`, `fields.<key>.options.<i>`, `submitLabel`. A placeholder is a `data-bcms-props` entry (`form:<formId>:fields.<key>.placeholder`). |
|
|
207
|
+
|
|
208
|
+
Only words are written. The lane adds and removes no element and never touches `name`, `id`,
|
|
209
|
+
`for`, `action`, `value`, a validation attribute or a hidden input, so the site's own script and
|
|
210
|
+
endpoint keep working. An option added to the row has no element until the site's code is
|
|
211
|
+
rebuilt, and is reported as not on the page. A fieldset's `<legend>` and an
|
|
212
|
+
`<input type="submit">` stay the code's. A form setup ignored (sign-in, basket) and a form
|
|
213
|
+
already wired with `data-bcms-form` take no stamp. The submit button belongs to the form: its
|
|
214
|
+
text is `submitLabel`, never a page field.
|
|
215
|
+
|
|
187
216
|
### 4. Wiring a self-hosted site
|
|
188
217
|
|
|
189
218
|
- **Next**: `getForm('Name')` (or an id) reads the form from `bcms-content.json`; render
|
|
@@ -85,6 +85,8 @@ and `get_componentize_plan` proposes nothing more.
|
|
|
85
85
|
with `componentize_sections`, then the repository half (`componentize_sections { codemod: true }`
|
|
86
86
|
when there is no checkout) and `submit_componentize_receipt` ([componentize.md](componentize.md)).
|
|
87
87
|
5. Work the rest through [finish-site.md](finish-site.md) (gap ledger, repair order, proof loop).
|
|
88
|
+
The gate's `sections` and `style` rows are part of it: a setup whose pages cannot take a new
|
|
89
|
+
section, a new order or spacing is not finished.
|
|
88
90
|
|
|
89
91
|
### Phase 4 — Publish and verify
|
|
90
92
|
|
|
@@ -126,13 +126,30 @@ value to its asset. Check it, never redo it: the binding report's `elementCovera
|
|
|
126
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
127
|
| `BG_PSEUDO_ELEMENT` | A background on `::before` / `::after` | Move it onto the element, or render an `<img>` |
|
|
128
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>`
|
|
130
|
-
| `SVG_SPRITE
|
|
129
|
+
| `SVG_TOO_LARGE` | An inline `<svg>` past 64 KB | Ship it as an `.svg` file in an `<img>` |
|
|
130
|
+
| `SVG_SPRITE` | A `<use>` naming a symbol the page does not hold, or a sprite FILE the build does not ship (another host's, a relative path) | Inline the symbol in the page, serve the sprite from the site root, or ship the icon as its own `<svg>` |
|
|
131
|
+
| `IMAGE_NOT_ASSET` | A header or footer picture the Media library holds no asset for | Nothing lost: the rest of the region is the Layout's and this picture stays a field of each page. Setup tries it again on the next build |
|
|
132
|
+
| `SVG_NOT_ICON`, `SVG_DECORATIVE` | An `<svg>` holding a script, an HTML island or a remote image; an `aria-hidden` glyph in the nav or footer, or a nameless one on an older import | Decoration: leave it, or give a logo `role="img"` and an `aria-label` |
|
|
131
133
|
| `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
134
|
|
|
133
|
-
The first four rows count against the image coverage; the
|
|
135
|
+
The first four rows count against the image coverage; the rest do not. A gap you cannot
|
|
134
136
|
close is reported with its code, never described as done.
|
|
135
137
|
|
|
138
|
+
An icon a sprite file draws (`<use href="/sprite.svg#shield">`, a same-site `.svg` the build
|
|
139
|
+
ships) is an image field like any inline icon. An icon a FONT draws (`<i class="ph ph-tag">`) is
|
|
140
|
+
a text field holding the class, written to the `class` attribute alone: change the class to
|
|
141
|
+
change the glyph.
|
|
142
|
+
|
|
143
|
+
`elementGaps` is the same record for everything that is not a picture (`rows` carry a `sample`
|
|
144
|
+
where an image row has a `src`). `EMBED_NOT_VIDEO` (an `<iframe>` that is no video provider's),
|
|
145
|
+
`AUDIO_SOURCE` and `ICON_FONT` (a font icon inside a paragraph that is one rich-text value) are
|
|
146
|
+
shown and editable nowhere, and count against coverage. The rest are declined on purpose and
|
|
147
|
+
never count: `SKIP_LINK`, `FRAMEWORK_UI` (a docs framework's pager, table of contents or
|
|
148
|
+
search), `FRAGMENT_LINK` (an in-page `#` destination under those rules), `ORDINAL_MARKER` (a
|
|
149
|
+
step number the template counts out, on a project imported before step numbers were fields),
|
|
150
|
+
`TEMPLATE_TEXT`, `NOSCRIPT_TEXT` and `HEAD_META_UNMAPPED` (a `<meta>` no native SEO field holds,
|
|
151
|
+
by name).
|
|
152
|
+
|
|
136
153
|
## Media host
|
|
137
154
|
|
|
138
155
|
Use `mediaBaseUrl` from `get_project`: media is served from the CDN origin or the API origin of
|
|
@@ -42,6 +42,8 @@ webhook. Use the project's webhook secret from the dashboard (the package docs n
|
|
|
42
42
|
| `readComponents()` | Component definitions from `bcms-content.json` |
|
|
43
43
|
| `BcmsField` | `path`, `kind` (`richtext`/`image`), `as`; server-safe; no wrapper around a single DOM element ([animation.md](animation.md) §10) |
|
|
44
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
|
+
| `BcmsSections`, `BcmsSection` (from `@bettercms-ai/next/sections`) | The page's own sections, drawn in the CMS page's order (below); a server component |
|
|
46
|
+
| `sectionsFor(slug)`, `sectionsStaticProps(slug, own?)`, `sectionsServerSideProps(slug, own)` | The block list, components and forms `BcmsSections` takes, from the snapshot |
|
|
45
47
|
| `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
48
|
| `bind`, `bindProps` | Binding attributes; instance-aware inside components |
|
|
47
49
|
| `BcmsForm` + `getForm(idOrName)` | Forms from the snapshot ([forms-and-interactions.md](forms-and-interactions.md)) |
|
|
@@ -60,6 +62,91 @@ Rules:
|
|
|
60
62
|
- The package ships no sitemap or robots helper. Write `app/sitemap.ts` and `app/robots.ts`
|
|
61
63
|
yourself from published content, and leave out pages marked noindex.
|
|
62
64
|
|
|
65
|
+
## Sections in the CMS order
|
|
66
|
+
|
|
67
|
+
A page whose sections are written in its own file follows the CMS page's block list through
|
|
68
|
+
`<BcmsSections>`. `bettercms init` writes this wrap (`npx @bettercms-ai/convert --sections`);
|
|
69
|
+
write it by hand for a page the codemod refused ([componentize.md](componentize.md) §5).
|
|
70
|
+
|
|
71
|
+
App Router, in a server component:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { sectionsFor } from "@bettercms-ai/next";
|
|
75
|
+
import { BcmsSections, BcmsSection } from "@bettercms-ai/next/sections";
|
|
76
|
+
|
|
77
|
+
export default function Home() {
|
|
78
|
+
return (
|
|
79
|
+
<main>
|
|
80
|
+
<BcmsSections {...sectionsFor("home")}>
|
|
81
|
+
<BcmsSection bind="hero">{(root) => <Hero rootProps={root} />}</BcmsSection>
|
|
82
|
+
<BcmsSection bind="code:sec1"><LogoCloud /></BcmsSection>
|
|
83
|
+
<BcmsSection bind="features"><section className="features">…</section></BcmsSection>
|
|
84
|
+
</BcmsSections>
|
|
85
|
+
</main>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Pages Router: the page also renders in the browser, so the list is a page prop.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { sectionsStaticProps, type SectionsData } from "@bettercms-ai/next";
|
|
94
|
+
import { BcmsSections, BcmsSection } from "@bettercms-ai/next/sections";
|
|
95
|
+
|
|
96
|
+
export default function Home({ bcmsSections }: { bcmsSections: SectionsData }) {
|
|
97
|
+
return <BcmsSections {...bcmsSections}>…</BcmsSections>;
|
|
98
|
+
}
|
|
99
|
+
export const getStaticProps = sectionsStaticProps("home");
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A page with its own data function passes it in, and the list joins the props it answers:
|
|
103
|
+
`export const getStaticProps = sectionsStaticProps("home", pageStaticProps)`, or
|
|
104
|
+
`sectionsServerSideProps` for `getServerSideProps`. `--sections` writes this for you.
|
|
105
|
+
|
|
106
|
+
A page that only returns a view (`return <HomeView locale="en" />`) has its sections in the
|
|
107
|
+
view's file, and `--sections` wraps them there: it follows the import (relative, a `paths` alias
|
|
108
|
+
of the repository's tsconfig or jsconfig, an index file or a barrel's re-export) up to three
|
|
109
|
+
components down. One view often serves several pages (`/` and `/fr`), so the page reads its own
|
|
110
|
+
list and hands it over, and the view takes it as an optional prop:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
// app/fr/page.tsx
|
|
114
|
+
export default function Page() {
|
|
115
|
+
return <HomeView locale="fr" bcmsSections={sectionsFor("fr")} />;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// views/home-view.tsx: a server or a "use client" component, it only names the type
|
|
119
|
+
export default function HomeView({ bcmsSections, locale }: Props & { bcmsSections?: SectionsData }) {
|
|
120
|
+
return (
|
|
121
|
+
<main>
|
|
122
|
+
<BcmsSections {...(bcmsSections ?? { page: "" })}>
|
|
123
|
+
<BcmsSection bind="hero"><Hero locale={locale} /></BcmsSection>
|
|
124
|
+
…
|
|
125
|
+
</BcmsSections>
|
|
126
|
+
</main>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A render that was handed no list (a route the CMS has no page for) keeps the source order. In the
|
|
132
|
+
Pages Router the page passes on the prop it got: `<HomeView bcmsSections={bcmsSections} />`.
|
|
133
|
+
|
|
134
|
+
- A bind is the family of the section's first bound field (`features-title` → `features`): the
|
|
135
|
+
`bind` on the page's `component` block. A block bound to a section renders it at the block's
|
|
136
|
+
position; a section added in the editor (no bind) renders through `BcmsBlocks`.
|
|
137
|
+
- The stamp goes on the section's own root. One DOM element (`<section>`) gets
|
|
138
|
+
`data-bcms-block`, `data-bcms-style` and the block's style cloned onto it. A COMPONENT child
|
|
139
|
+
cannot be handed unknown props, so write it as a function and spread `root` on the element the
|
|
140
|
+
component renders (`<section {...rootProps}>`). Without the function it is stamped on a box:
|
|
141
|
+
`display:contents` while its block has no style, one `display:flow-root` box carrying the
|
|
142
|
+
style once it has ([animation.md](animation.md) §10).
|
|
143
|
+
- A section no block names: `code:<bind>` always renders where it is written. A bare `<bind>`
|
|
144
|
+
does too until the list names one of the page's sections; from then on it is a removed one.
|
|
145
|
+
Write a bind bare only when the page already places it (`get_page`), otherwise `code:`.
|
|
146
|
+
- `blocks` null (no snapshot) renders every child in source order, untouched.
|
|
147
|
+
- Never inside a `"use client"` file: it reads its children's own elements, which a client
|
|
148
|
+
component cannot do across the server boundary.
|
|
149
|
+
|
|
63
150
|
## Lists, pagination and missing content
|
|
64
151
|
|
|
65
152
|
- `listEntries(model, { page, perPage })`: `page` is 1-based; `perPage` defaults to 20 and the
|
|
@@ -35,7 +35,8 @@ that reads them.
|
|
|
35
35
|
- Publishing copies the stored draft to live, so what you checked in the draft is what ships.
|
|
36
36
|
- **Many at once**: `publish_entries` { action: 'publish' | 'unpublish', entryIds, pageIds } takes
|
|
37
37
|
up to 100 of each in one call (pages publish only). Each item still passes its own gates, and
|
|
38
|
-
nothing stops the run: read `failed` for every refusal and its reason
|
|
38
|
+
nothing stops the run: read `failed` for every refusal and its reason, then `outcome` for what
|
|
39
|
+
the served pages show (the same shape `update_page` returns on a publish, below).
|
|
39
40
|
- **Throw away a draft**: `discard_draft` with `entryId` or `pageId` returns it to exactly what is
|
|
40
41
|
live; a page also drops its unpublished Layout overrides (`no_published_version` if it never
|
|
41
42
|
went live: delete it instead).
|
|
@@ -64,6 +65,19 @@ renders rows no address names, or was built empty). That publish waits for its b
|
|
|
64
65
|
the list shows N rows") and fails with the reason if the build still shows the page's own rows: the
|
|
65
66
|
site's code renders the list itself. `get_next_steps` names what is waiting.
|
|
66
67
|
|
|
68
|
+
A static page whose own code fixes its sections takes all of it in place too, when the project's
|
|
69
|
+
in-place structure switch is on: the publish writes the page's block list into the built HTML.
|
|
70
|
+
The code's bands stand in the published order, a removed one is cut, a section added in
|
|
71
|
+
BetterCMS is drawn between them, and every root takes its block's style. No repository change
|
|
72
|
+
and no build; each rebuild is put right the same way. The gate's `sections` row says which
|
|
73
|
+
routes take it (`accepts: "in-place"`).
|
|
74
|
+
|
|
75
|
+
A collection's item pages take it per TEMPLATE: the collection page's sections are the
|
|
76
|
+
template's, held once, so a section added to that page (`update_page` on the collection page)
|
|
77
|
+
stands on every entry's route, and an entry has no sections of its own. Its publish reads one
|
|
78
|
+
row for all of them (`route` `blog/*`). The entry's body is a section too: add sections around
|
|
79
|
+
it or move it, and space it through its field's style, since its element is the field's.
|
|
80
|
+
|
|
67
81
|
The SEO a page or entry stores is what its `<head>` says, whenever it was set: the title, the
|
|
68
82
|
description, the canonical, the card fields, a `noindex` that is on. A field left empty keeps what
|
|
69
83
|
the build rendered, and a site default only fills a tag the build has none of. Only a real choice
|
|
@@ -79,14 +93,52 @@ Builds run `build:bcms` instead of `build` when `package.json` declares it. Keep
|
|
|
79
93
|
of it and every step the build needs in: `"build": "astro check && astro build"` beside
|
|
80
94
|
`"build:bcms": "astro build"`. The convert codemod adds one to a repo whose build chains a check.
|
|
81
95
|
|
|
96
|
+
### Add a section, or set its spacing
|
|
97
|
+
|
|
98
|
+
One call each, on any page (a page BetterCMS draws, a code-built one, a collection template):
|
|
99
|
+
|
|
100
|
+
- **Add a section here**: `update_page { pageId, blockJson: { insertSection: { componentId, after: "<block id>" } } }`.
|
|
101
|
+
`componentId` is a component from `list_components`; a `builtin:*` id is copied into the project
|
|
102
|
+
once and the copy is placed. Place it with `at` (an index into the page's sections), `before`
|
|
103
|
+
or `after` (a section's block id from `get_page`); none of the three adds it last. `style` and
|
|
104
|
+
`overrides` set its block style and its copy in the same call.
|
|
105
|
+
- **Set spacing on this section**: `update_page { pageId, blockJson: { sectionStyle: { blockId, style } } }`.
|
|
106
|
+
Each key of `style` replaces the stored one and `null` unsets it: `paddingTop`, `paddingBottom`,
|
|
107
|
+
`paddingSides` (px, 0 to 240), `spaceTop`, `spaceBottom`, `spaceSides` (the spacing tokens),
|
|
108
|
+
`bg`, `bgCustom`, `contentWidth`, `contentAlign`, `theme`, `corner`, `shadow`, `border`,
|
|
109
|
+
`opacity`, `hideOn`. A key the block style does not have is refused (`INVALID_STYLE`).
|
|
110
|
+
|
|
111
|
+
The server edits the page as stored, so nothing else on it is resent or overwritten; send
|
|
112
|
+
`ifMatch` to refuse a page that changed since you read it. Both save a DRAFT: add
|
|
113
|
+
`status: "published"` to the same call, or publish after. Then read the answer, top down:
|
|
114
|
+
|
|
115
|
+
1. `held`: the draft is saved and the live page will NOT show it. It names the reason and the one
|
|
116
|
+
action (below). Do the action; do not report the change as made.
|
|
117
|
+
2. `outcome` (on a publish): `status` `live` is every value found on the served page. `partial`
|
|
118
|
+
lists what is not in `notLive`, each with its `reason` and `action` (the table under Reading
|
|
119
|
+
`get_deploy_status`). `next: "get_deploy_status"` means it is not judged whole: nothing has
|
|
120
|
+
looked at it yet, or a section change is still being written (`NOT_CHECKED`). It resolves by
|
|
121
|
+
itself to live or held: poll `publishes` there before you say anything is live.
|
|
122
|
+
3. `section`: the block's `id` and `index`, to place the next one beside it.
|
|
123
|
+
4. `warnings`: `COMPONENT_DRAFT` is a section that renders nothing until `publish_component`.
|
|
124
|
+
|
|
125
|
+
Refusals: `SECTION_NOT_FOUND` (the answer lists the page's section ids), `POSITION_OUT_OF_RANGE`,
|
|
126
|
+
`COMPONENT_NOT_FOUND`, `BUILTIN_NOT_FOUND`. A whole `blockJson` array still replaces the page's
|
|
127
|
+
sections; use it to move or remove one, with `ifMatch`.
|
|
128
|
+
|
|
82
129
|
### A section change that is held at the write
|
|
83
130
|
|
|
84
|
-
`update_page` and `compose_pages` save the draft and answer with `holds` when a
|
|
85
|
-
moves or removes a section the live page cannot show yet. Each hold has a `reason`,
|
|
86
|
-
the `sections` it is about and the one `action`. Never report such a write as done:
|
|
131
|
+
`update_page` and `compose_pages` save the draft and answer `held` first (with `holds`) when a
|
|
132
|
+
write adds, moves or removes a section the live page cannot show yet. Each hold has a `reason`,
|
|
133
|
+
a `message`, the `sections` it is about and the one `action`. Never report such a write as done:
|
|
87
134
|
|
|
88
135
|
- `CODE_FIXED_SECTIONS`: the site's code fixes this page's sections, so no publish ships the
|
|
89
|
-
change.
|
|
136
|
+
change. Read the route's `sections` row in `get_next_steps` (`receipt.gate`): its `fixCode`
|
|
137
|
+
names the lane ([finish-site.md](finish-site.md)). `enable-in-place-structure` is the user's
|
|
138
|
+
switch and needs no code. `run-sections-conversion` is the wrap: `componentize_sections` with
|
|
139
|
+
`codemod: true` and the `digest` of `get_componentize_plan`, or, when that answers `skipped`
|
|
140
|
+
(no repository the platform may commit to), `npx @bettercms-ai/convert@latest --sections --root .`
|
|
141
|
+
in a checkout ([componentize.md](componentize.md) §5). After its build, publish.
|
|
90
142
|
- `SECTION_NEEDS_BUILD`: the build draws the page's sections from BetterCMS. Publish: the build
|
|
91
143
|
starts by itself, and a move or a removal needs none.
|
|
92
144
|
- `STRUCTURE_UNMEASURED`: nothing has measured the page. Publish, then read the publish's outcome
|
|
@@ -117,13 +169,29 @@ the `sections` it is about and the one `action`. Never report such a write as do
|
|
|
117
169
|
| `VALUE_REFUSED` | held | Bind the field to an element that can hold its value |
|
|
118
170
|
| `APP_DOES_NOT_READ` | held | Convert in code: make the page's code read the published field |
|
|
119
171
|
| `ROWS_NEED_BUILD`, `SECTION_NEEDS_BUILD`, `NEEDS_BUILD` | held | Wait for the build the publish started |
|
|
120
|
-
| `STRUCTURE_UNBOUND` | refused |
|
|
172
|
+
| `STRUCTURE_UNBOUND` | refused | The fix the route's `sections` gate row names: the in-place switch, or the sections conversion |
|
|
121
173
|
| `SECTION_MISSING` | held | The same: the served page does not render the section |
|
|
122
|
-
| `
|
|
174
|
+
| `SECTION_NOT_ON_PAGE` | held | In place: a section of the site's own code has no band on this page. The sections conversion, or remove the placement |
|
|
175
|
+
| `SECTION_NOT_RENDERABLE` | held | In place: the added section's component is not published, or it needs a form. `publish_component`, then publish the page again |
|
|
176
|
+
| `STYLE_NOT_PAINTABLE` | held | Rebuild once the section root carries the style marker (`sectionRootAttrs`, or the codemod's `--upgrade-sections`: [components.md](components.md)) |
|
|
123
177
|
| `WINDOW_NOT_OPEN` | held | Nothing: it goes live when its window opens |
|
|
124
178
|
| `ROUTE_NOT_OWNED` | held | Change one of the two page paths that share the URL |
|
|
125
179
|
| `APP_UNREACHABLE`, `NOT_CHECKED`, `EDGE_PENDING`, `IMAGE_UNAVAILABLE`, `STRUCTURE_UNMEASURED` | unverified | Poll again; publish again if it stays |
|
|
126
180
|
|
|
181
|
+
Each row's `action.code` is one of these, and `get_next_steps` lists every held or refused value
|
|
182
|
+
as `publish-held` until it is gone:
|
|
183
|
+
|
|
184
|
+
| `action.code` | What you do |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `rebuild` | No tool rebuilds: change the site's code if the reason names it, then ask the user to Rebuild from Publishing (or `deploy_project` from a checkout) |
|
|
187
|
+
| `wait_for_build` | Poll `get_deploy_status` until the build the publish started is live |
|
|
188
|
+
| `convert_in_code` | Make the page's code read the field ([bindings.md](bindings.md)), then release |
|
|
189
|
+
| `control_section_order` | The route's `sections` gate row names it: the in-place switch (the user's), or `componentize_sections` with `codemod: true` (else `convert --sections` in a checkout), then release |
|
|
190
|
+
| `editor_setup` | `editor_setup` |
|
|
191
|
+
| `rebind` | Bind the field to an element that can hold it, in the site's code |
|
|
192
|
+
| `resolve_route` | Change one of the two page paths |
|
|
193
|
+
| `wait`, `retry` | Poll again; publish again if it stays |
|
|
194
|
+
|
|
127
195
|
### Deploy to Vercel or Netlify
|
|
128
196
|
|
|
129
197
|
To serve a `managed` or `hybrid` site from the owner's own Vercel or Netlify account, call
|
|
@@ -53,7 +53,8 @@ values as they are; rewriting copy is a separate, reported recommendation.
|
|
|
53
53
|
| `<meta name="description">` | `metaDescription` | Copy verbatim. Aim for 120–160 characters; the audit flags only a missing one and one over 160. When the source has none, take a `generate_seo_meta` suggestion and list it for review |
|
|
54
54
|
| `og:title`, `og:description`, `og:image`, `og:type` | `og.title`, `og.description`, `og.image`, `og.type` | Write a key only when it differs from the meta title, the description or the site default. Upload the image first and store its media URL |
|
|
55
55
|
| `twitter:card`, `twitter:title`, `twitter:description`, `twitter:image` | `twitter.card`, `twitter.title`, `twitter.description`, `twitter.image` | Same rule as OG |
|
|
56
|
-
| `twitter:site` | `update_seo` `seoDefaults.twitterHandle` | Once per site |
|
|
56
|
+
| `twitter:site` | `update_seo` `seoDefaults.twitterHandle` | Once per site. A BetterCMS-built import fills it, the card and `og:type` from the home page's head where the project holds none |
|
|
57
|
+
| `keywords`, `author`, `theme-color`, a site verification tag | nothing | No native key stores them. The binding report's `elementGaps` lists each by name (`HEAD_META_UNMAPPED`); report them |
|
|
57
58
|
| A site-wide share image | `update_seo` `seoDefaults.ogImage` | Then pages inherit it and `og.image` stays empty |
|
|
58
59
|
| Canonical pointing at the page itself | nothing | A hosted page gets its own canonical |
|
|
59
60
|
| Canonical pointing at another URL | `canonical` | Keep it: someone consolidated those URLs on purpose |
|
package/package.json
CHANGED