@bettercms-ai/mcp 0.83.0 → 0.83.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +86 -86
  2. package/SKILL.md +160 -160
  3. package/dist/index.js +38 -9
  4. package/dist/index.js.map +1 -1
  5. package/dist/kit-catalog.json +1509 -1509
  6. package/dist/skills/bettercms/SKILL.md +206 -206
  7. package/dist/skills/bettercms/references/accessibility.md +171 -171
  8. package/dist/skills/bettercms/references/animation.md +296 -296
  9. package/dist/skills/bettercms/references/astro.md +148 -148
  10. package/dist/skills/bettercms/references/bindings.md +366 -366
  11. package/dist/skills/bettercms/references/componentize.md +185 -185
  12. package/dist/skills/bettercms/references/components.md +415 -415
  13. package/dist/skills/bettercms/references/content-experiments.md +62 -62
  14. package/dist/skills/bettercms/references/content-modeling.md +382 -382
  15. package/dist/skills/bettercms/references/detection.md +465 -465
  16. package/dist/skills/bettercms/references/dock-input.md +121 -121
  17. package/dist/skills/bettercms/references/editor-chrome.md +92 -92
  18. package/dist/skills/bettercms/references/external-content.md +78 -78
  19. package/dist/skills/bettercms/references/finish-site.md +377 -377
  20. package/dist/skills/bettercms/references/forms-and-interactions.md +245 -245
  21. package/dist/skills/bettercms/references/frameworks.md +159 -159
  22. package/dist/skills/bettercms/references/getting-started.md +156 -156
  23. package/dist/skills/bettercms/references/integrations.md +364 -364
  24. package/dist/skills/bettercms/references/kit-catalog.md +3259 -3259
  25. package/dist/skills/bettercms/references/kit-scenarios.md +712 -712
  26. package/dist/skills/bettercms/references/localization.md +85 -85
  27. package/dist/skills/bettercms/references/media-and-brand.md +181 -181
  28. package/dist/skills/bettercms/references/next.md +189 -189
  29. package/dist/skills/bettercms/references/other-frameworks.md +85 -85
  30. package/dist/skills/bettercms/references/portable-text.md +213 -213
  31. package/dist/skills/bettercms/references/programmatic-seo.md +98 -98
  32. package/dist/skills/bettercms/references/publishing.md +289 -289
  33. package/dist/skills/bettercms/references/seo.md +357 -357
  34. package/dist/skills/bettercms/references/shopify-headless.md +52 -52
  35. package/dist/skills/bettercms/references/site-blueprints.md +895 -895
  36. package/dist/skills/bettercms/references/site-review.md +74 -74
  37. package/dist/skills/bettercms/references/worked-examples-advanced.md +509 -509
  38. package/dist/skills/bettercms/references/worked-examples.md +885 -885
  39. package/dist/skills/bettercms/references/write-limits.md +217 -217
  40. package/package.json +1 -1
@@ -1,377 +1,377 @@
1
- ---
2
- title: "Finish an imported site"
3
- description: "Take an imported or converted site to fully editable: the gap ledger in .bettercms/ledger.json, the repair order, and the proof loop per route."
4
- ---
5
-
6
- # Finish an imported or converted site
7
-
8
- **Outcome.** Every intended value editable, sections reusable, design faithful, and launch
9
- verified with evidence. "Fully editable" is a measured result, never an inference from imported
10
- schemas. New to the project, or no repo on disk yet: [getting-started.md](getting-started.md).
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
-
19
- ## Inputs and baseline
20
-
21
- Require the project, source repository or import snapshot, site URL, import manifest, hosting
22
- mode and the user's goal. Confirm the bound project with `get_project` (hosting mode,
23
- `authoringPreference`), read `bettercms://playbook/schema`, and inspect the repository and the
24
- previous import report. If any are unavailable, continue with a read-only gap analysis and name
25
- the missing evidence.
26
-
27
- Capture a baseline before editing: routes, screenshots at representative widths, content model
28
- and entry counts, component and global-layout inventory (`get_site_composition`, `get_layout`),
29
- media and form inventory, `list_seo_issues`, `get_deploy_status`, `get_binding_report`, and the
30
- source commit. Keep existing URLs and behaviour unless the user asked for a change.
31
-
32
- ## The finish line for a convert or build job
33
-
34
- A conversion is done only when the `convert_site` prompt's loop is: `submit_conversion_receipt`
35
- answers `complete: true` (no path left in `paths.pending`), and after the deploy
36
- `get_binding_report` shows `unmatched` at 0, `coverage.pending` empty and, once
37
- `set_binding_mode` has flipped the site to declared and a second release has run, mode
38
- `declared`. A build job works from `bettercms://playbook/schema` (the `build_site` prompt carries
39
- the same flow on a local MCP) and is done when the site is deployed, `get_component_readiness`
40
- is clean for every component it placed, and `get_next_steps` lists nothing left to do from code.
41
- Both end in the verify loop of SKILL.md, and an imported site ends only on the setup gate below.
42
- One converted page, or a receipt with paths still pending, is not a finished site.
43
-
44
- Setup is not a conversion. An imported site whose release serves its pages is editable through the
45
- fields the release binds on the built page, and the setup gate measures exactly that. Run the
46
- convert codemod only for what the gate asks it for (`run-sections-conversion`, a form to wire) or
47
- when the owner asks for the source to declare its bindings. "The codemod bound few paths" is no
48
- gate failure and nothing to report to the owner.
49
-
50
- ## The setup gate: what "finished" is measured by
51
-
52
- `get_next_steps` returns `receipt.complete` and `receipt.gate`. The platform computes the gate
53
- from the last release, one verdict per route of the live build. The CLI that launched you reads
54
- it again when you exit and prints it to the user, so the gate is the report, not your summary.
55
- While the gate is incomplete that command exits 3 (0 once it is complete; 2 is a wrong command
56
- line), which is what a script waiting on the setup reads.
57
-
58
- | Check | Passes when | Where it shows |
59
- |---|---|---|
60
- | `page` | a CMS page, or a collection entry, serves the route | `routes.rows[].page` |
61
- | `published` | that page, and its entry, is published | `routes.rows[].page` |
62
- | `twin` | no second page claims the same route | `blocking.rows` |
63
- | `text` | 2% or less of the visible characters sit outside a field | `routes.rows[].text` |
64
- | `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` |
65
- | `link` | 95% of the links have a field for the label or the destination | `routes.rows[].link` |
66
- | `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) |
67
- | `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` |
68
- | `svg` | every inline SVG a visitor sees (a logo, an icon, an illustration) belongs to a field; a hidden symbol sheet is not counted, nor the glyphs of a rating drawn as one image (`role="img"` round them) | `routes.rows[].svg` |
69
- | `number` | every element whose whole text is a number (a stat, a price, a rating) belongs to a field | `routes.rows[].number` |
70
- | `attribute` | every `alt`, `aria-label`, `title` and `placeholder` with words in it belongs to a field; copy inside a field's rich text is that field's, and a picture's or icon's name is its field's | `routes.rows[].attribute` |
71
- | `names` | never a row: a name holds nothing open. A field, a section, a component or a prop whose name a person cannot read (a slug, a hash, a structural word such as `Section 2` or `wrap`, a number beside the same name such as `Icon 2`) is listed as advice with the call that renames it. The import already names a section by its own title, and a section's list by that title, where the markup gave it nothing better | `advice` (check `names`), and `naming` |
72
- | `seo` | the page has a title and a description, and a canonical where the build ships one for this site's origin. A title over 70 characters or a description over 200 is the site's own copy as built, and a site with no default share image has none to give: both are advice (`receipt.gate.advice`), never rows | `blocking.rows` |
73
- | `structuredData` | JSON-LD the build ships on the route is on the page or the entry | `blocking.rows` |
74
- | `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` |
75
- | `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` |
76
- | `components` | the authoring question is answered, and no section is left that a componentize run would place. A heading or paragraph that stands outside any section is a field of its page, not a section: advice (`receipt.gate.advice`) | `blocking.rows`, `route: null` |
77
- | `forms` | every form of the customer's reaches somebody: its own address, else the project's default, else the workspace's owners, the order a submission is sent in; a form setup ignored (sign-in, add to basket) is not counted | `blocking.rows`, `route: null` |
78
- | `dock` | no component is left with props the server's own ui derivation would still dress: one it has not reached has no prop carrying V2 `ui`, or a list prop (`table`, `array`, `multi-reference`) with no row title (`ui.preview.title` or `ui.itemLabel`). The import derives each prop's ui when it makes a component, from its closest `bettercms://kit` entry or the generic rules, so a fresh import has no such row; a component whose props it leaves to the default controls (two short texts) is done. Button looks are not counted. Which kit entry a component IS is not part of this check: it is advice, `receipt.gate.kit` (below) | `blocking.rows`, `route: null`, the component and its props in `detail` |
79
-
80
- `pages` counts routes built, created and published. `coverage` sums `editable` of `total` across
81
- the site, `svg`, `number` and `attribute` included, and `open` counts the rows still open per
82
- check. `routes.rows` is ordered worst first, with `failing` per route and the reasons the
83
- deriver `skipped` copy there. `blocking.rows` is the work list: each row carries a `detail` with
84
- its number, a `fixCode` and a `fix`, the call that closes it.
85
-
86
- `nextAction` is the one thing to do now: the fix a tool can make that closes the most rows
87
- (`fixCode`, `count`, the `fix` of its first row, the first `routes`), two with the same count in a
88
- fixed order. Run it on every row that carries that `fixCode`, call `get_next_steps`, and repeat
89
- until `complete`: following it reaches `complete` with no step that changes nothing. A fix no
90
- tool can make is named only when nothing else is left. A `fix` of `null` means no tool closes that row by itself: its
91
- `detail` names what to change in the site's code, so change it, commit, push and `rebuild_project`.
92
- Only a collection's listing route, a URL that cannot be placed and two pages nobody but the owner
93
- can choose between are left; record each as an exception with a true reason, never stop to ask. A
94
- row its fix did not move twice is recorded the same way.
95
-
96
- A `sections` or `style` row is a page nobody can add a section to, reorder or space, however
97
- complete its copy is. Its `fixCode` names the one fix, and `fix` the call when a tool can make it:
98
-
99
- | `fixCode` | The fix |
100
- |---|---|
101
- | `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 |
102
- | `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. Push what changed, then `rebuild_project` and `get_deploy_status { waitSeconds: 50 }` until it is live |
103
- | `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, `update_project { structureInPlace: true }`. No repository change |
104
- | `run-setup` | Nothing measured what the row reads, or a page is still to register: `editor_setup { force: true }`, then read again |
105
-
106
- The other rows name their fix the same way:
107
-
108
- | `fixCode` | The fix |
109
- |---|---|
110
- | `bind-elements` | What no field owns on the route is `pages[].skipped` of `get_conversion_brief { complete: true }` (each node with why the import left it) and the `unaddressable` buckets of `get_binding_report`. Declare each in the site's template (`data-bcms-field`; an SVG as an image or icon field, a number as its own field, attribute copy with `data-bcms-props`) and push. **Do not run `@bettercms-ai/convert` for this row.** The brief's `paths` are fields the CMS already has, and on a built site the release has already bound them on the page: the codemod creates no field, and run over them it refuses copy that lives in Markdown, pictures from a content collection and values a helper formats, by the thousand |
111
- | `rename-fields` | `get_page`, read the section's source, then name every listed field of the page in ONE `update_content_model` call (`pageId`, `fieldLabel` and `fieldHelpText` keyed by field path, `ifMatch` from `get_page`'s `backingModel`), or `update_field` for a single one; `update_component` for a component (`name`, and `propLabel` / `propHelpText` for its props in one call). See "Names" below |
112
- | `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` |
113
- | `import-structured-data` | `editor_setup { force: true }` imports the build's JSON-LD; `update_seo` when it has to be written by hand |
114
- | `model-collection` | not a row any more. Setup derives a collection wherever a family's pages share one template, with its fields, its entries and their bodies. A family it left as pages is listed as advice in `receipt.gate.families`: every route in it is an editable page. **Never `create_content_model` for it on an imported site.** One hand-written model reads the whole project as hand-authored, and the import then registers no further page, derives no collection and adds no field, which is where "the model holds only title and description" comes from. Mention the family to the owner as something a collection could hold later, and go on |
115
- | `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)) |
116
- | `componentize` | `set_authoring_preference` when nobody answered, then `get_componentize_plan` and `componentize_sections` ([componentize.md](componentize.md)) |
117
- | `set-form-recipient` | `update_form` with `notifyEmails` set to the address the owner gave when setup started (it is in your first prompt); `update_form { ignored: true }` when it is not one of their forms. Only a form the built site posts to BetterCMS is listed, and only when a submission would reach nobody at all (no address on the form, none on the project, no owner on the workspace); the release publishes it once it is wired. Never ask for an address at the end of a run |
118
- | `set-dock-ui` | the last step of the repair order below: `get_component`, then `update_component` with `propUi` and `propTab` for the props the row names ([editor-chrome.md](editor-chrome.md)) |
119
-
120
- A row nothing measured reads `unmeasured: true` with `run-setup`, and `gate.unmeasured` lists the
121
- checks that have one: a release older than a count, or a project set up before setup recorded
122
- what it found on the build (the style marker of each section root, the looks it measured, the
123
- canonicals the build ships). It is not a failure of the site and not a pass either: run
124
- `editor_setup { force: true }` once and read again.
125
-
126
- Setup closes what is deterministic by itself, on every run, so your first receipt is already past
127
- it and an open row of these means run setup again: `run-setup`, `derive-buttons` (the family and
128
- the map), `import-structured-data`, `fill-seo` from the built head and the page's own copy,
129
- `rename-fields` for a field still carrying a derived name, `componentize` once the authoring
130
- answer is `components`, and `enable-in-place-structure` on a project it creates. What is left
131
- takes judgement and is yours: `bind-elements`, `set-form-recipient` (the
132
- address given at the start), `run-sections-conversion`, a name a person gave, a title over its limit, a look named by a number, and
133
- the authoring answer itself (the user's). `rebuild` is the user's.
134
-
135
- Beside the gate, `data` lists what else is unfinished, each item by its `ruleId`: content
136
- (`mcp-pages-draft`, `mcp-pages-missing-meta`, `mcp-model-empty`, `mcp-form-no-recipient`,
137
- `mcp-pending-review`), bindings and structure (`conversion-pending`, `bindings-not-declared`,
138
- `unaddressable-copy`, `upgrade-section-roots`, `upgrade-element-styles`, `componentize-pages`,
139
- `components-unpublished`), the authoring answer (`authoring-undecided`), the Layout lane
140
- (`layout-write-blocked`, `layout-field-unread`, `layout-locator-stale`, `layout-rebuild-required`,
141
- `layout-unpublished`, `chrome-duplicated`), publishes (`publish-held`) and the Content sidebar
142
- (`organise-content`). `setup-gate` is first while the gate is open and `publish-held` next.
143
-
144
- Stamps are not enough to pass either check: the lane that serves a route must have PROVED it.
145
- Each release adds a probe section to the middle of one built page of each kind (a page, a
146
- listing, an item template) and a probe padding to one of its sections, in memory, finds both in
147
- the result and takes them out again; the sections conversion proves each page it wraps the same
148
- way. `routes.rows[].proof` is that record (`lane`, `route`, `insert`, `style`: `ok` or the
149
- reason). A row whose proof was never run asks for it where it runs: `editor_setup { force: true }`
150
- for the release, the conversion again for a wrapped page. `SLOTS_NOT_PLACED` on a wrapped page
151
- means every slot is still `code:`: place the page's sections, then run the conversion again.
152
-
153
- The gate picks the fix per route, in that order of evidence: a wrapped page waits for its build; a
154
- page a script draws needs the conversion; a static page needs only the switch. Run the fix the
155
- row names and no other: the conversion does nothing for a row that says `rebuild`.
156
-
157
- Two rows have no `fixCode`. A page that places a section twice: remove the second placement with
158
- `update_page`. A route neither lane serves: the release cannot write it (`refusal`) and the
159
- conversion refused it (`wrap`); its `detail` names both. Put it to the user: write the wrap by
160
- hand ([astro.md](astro.md), [next.md](next.md)) or change the page. No exception covers it
161
- (`document-body` excuses only a route that places no section). Check a route's row again after
162
- each release; never mark one done from the codemod's own output.
163
-
164
- A `page` row on a route setup left without a page carries the pages lane's reason in its
165
- `detail` (`invalid-path`, `html-too-large`, `page-deleted`, `slug-taken`, `folder-conflict`,
166
- `entry-not-registered`, `authored-content` and the rest), with what closes it: a change in the
167
- site's code, another page to move, or the user's decision. Only `unreadable`, `write-failed`
168
- and `not-registered` carry a `fix`. An `image` row with a `null` fix names a picture by its code
169
- ([media-and-brand.md](media-and-brand.md) Where images hide): the change is in the site's code.
170
- `imageGaps` also lists the decoration the import declined, which blocks nothing. On a
171
- collection's listing route the rows that link to its entries are counted as the collection's,
172
- so the listing page is judged on its own heading, intro and calls to action, and on the words
173
- of a row the release bound to its entry (a field's there too). The release binds
174
- each such row to its entry: the row carries `data-bcms-entry`, a part showing a field as stored
175
- carries `data-bcms-entry-field` (`entry:<id>:<path>`, written when that entry publishes), and a
176
- part the site's code made from a field (a cut excerpt, a reformatted date or price, a thumbnail,
177
- the item link) carries `data-bcms-entry-from` with the transform: edit the entry. When it publishes,
178
- a picture (`|rendition`) and a number (`|format`) are written in place; any other such part that no
179
- longer shows its value is reported `entry-derived` with why (`truncate`, `route`, `format`,
180
- `art-direction`, `repo-path`, `format-unreadable`).
181
- Never add a field for a row to the listing page. A taxonomy's term routes (`/tags/<term>`, each
182
- listing another collection's entries) are a small collection of terms, and their rows are bound
183
- the same way. A term has a `body` only where its template has an article. Chrome the Layout
184
- could not take (an icon with no Media asset) is template copy on item routes: one shared field
185
- of the collection, as it is a page's own field on a page.
186
-
187
- ### Names
188
-
189
- The import names a field by its section and its role there (`Hero headline`, `FAQ questions`).
190
- Where the markup says no more it numbers them (`Pricing text 4`, `Hero image 2`), and a section
191
- whose classes name nothing is called by its heading's words. No build can say that a picture is
192
- the dashboard screenshot; the site's code can. So these never hold the gate open: they are
193
- `receipt.gate.naming`, and `get_next_steps` lists the step `names-from-code` until each is
194
- renamed. Do it in the same job, after the gate's own rows and before the final report: an
195
- editor reads these names all day, and a setup that leaves them is not finished.
196
-
197
- Name from the site's code, the way a developer writes a schema. Open the section's source file
198
- and use what it calls things (`plan.cents`, `billingFrequency`, `StatsBand.astro`):
199
-
200
- | What | The name | Like | Never |
201
- |---|---|---|---|
202
- | A section's KIND (a component's `sectionType`) | what it IS, one to three words, sentence case. The picker lists kinds and a page's outline shows it under each section | `Announcement banner`, `Hero`, `Logo cloud`, `Feature tabs`, `Testimonial`, `Pricing`, `FAQ`, `Call to action` | its heading's copy or first word (`Simple`, `Trusted`), `Section 4`, a file path |
203
- | A layout of that kind (a component's `name`) | how THIS layout differs from the kind's others; the kind alone when it has one layout | `Centered`, `Split with media`, `Pricing` | its heading's copy, the page it came from with a number (`Hero 2`) |
204
- | One placed section's title | nothing you write. The outline shows the section's own heading, as written and read live, with its kind underneath | `Equip Your Projects with ScrewFast` over `Hero` | a `label` you invent, a shortened or reworded heading, the kind said again |
205
- | A field or prop | what it HOLDS there, one to three words | `Dismiss key`, `Starter plan price`, `Dashboard screenshot`, `Billing frequency` | its own copy, a role with a number (`Text 4`), the section's name alone |
206
- | Help text | most fields get none. One plain sentence under 90 characters, only for a format or unit, a place the canvas cannot show, a real constraint or a fallback. No semicolons, no dashes, no values from the page | `Leave empty to use the page title.` | the label or the value said again, writing advice, a figure copied from another page |
207
-
208
- **Advice is not work left**, except the names. `gate.naming` is part of the job: rename every
209
- row before you report, as above. `receipt.gate.advice`, `gate.families` and `gate.kit`
210
- are things a person may want to change about the site's own content or build: a title longer than
211
- a search result shows, no default share image, an element outside any section, a route family that
212
- could be a collection, a kit entry to confirm, a name to sharpen. The site is fully editable with
213
- every one of them standing. When `gate.complete` is true and `gate.naming` is absent or empty the setup is
214
- DONE: say so, list the advice in a few lines as optional improvements, and never call any of it pending, blocked,
215
- remaining or left over, and never end on a question about it.
216
-
217
- A section says two things: what it IS (its kind) and what it SAYS (its title). Only the kind is
218
- yours to judge. The import sets it from the band's shape or the site's own word for it; where it
219
- could read neither, the kind is the key's word, often the heading's first, and you correct it with
220
- `update_component { sectionType }`, spelled exactly like the kind's other layouts. The title is
221
- never named by anyone: the outline prints the section's heading as the page holds it, so two
222
- heroes are `Hero` twice in the picker and two different titles on the page. Do not write a block
223
- `label` to title a section, and do not shorten, re-case or reword a heading into one. A `label`
224
- is only ever a person's own rename.
225
-
226
- A numbered run that is one thing repeated (plans, features, logos) is a list, not twelve names:
227
- fold it into one list field or one `table` prop whose rows are the cards
228
- ([componentize.md](componentize.md)), and name the list (`Plans`). A name you write is yours: a
229
- later setup never renames it.
230
-
231
- ### Exceptions go on the record
232
-
233
- A check you are leaving on purpose is recorded, one row per route and check, with
234
- `editor_setup` and `exempt: [{ route, check, reason, note }]`. `note` states what is being left
235
- in a sentence a person can verify. `reason: null` withdraws one.
236
-
237
- | `reason` | May excuse |
238
- |---|---|
239
- | `form-validation-copy` | `text` |
240
- | `legal-boilerplate` | `text` |
241
- | `third-party-widget` | `text`, `image`, `link`, `svg`, `number`, `attribute` |
242
- | `decorative` | `text`, `image`, `svg` |
243
- | `duplicate-track` | `text`, `image`, `link`, `svg`, `number`, `attribute` (the cloned track of a marquee or slider) |
244
- | `client-only-island` | nothing: it lists an island the build cannot show, and the route still meets the bar outside it |
245
- | `user-declined` | `page`, `published`, `text`, `image`, `link`, `svg`, `number`, `attribute`, `seo`, `structuredData`, `collections`, `dock`, and only with the user's own words quoted in `note` (`User: "leave pricing in code"`) |
246
- | `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) |
247
- | `default-controls` | `dock`, on `route: "component/<slug>"`: a component whose props read right with the dock's default controls (one short heading, a toggle); `note` says why |
248
-
249
- Nothing else excuses `sections`, and nothing excuses `style`, `twin`, `names`, `buttons`,
250
- `components` or `forms`: not the user's decision either. A name is fixed by renaming, and a form
251
- that is not the customer's is marked ignored.
252
-
253
- A row excuses what its check read when it was recorded. When the route reads differently
254
- later (more copy nobody owns, another picture), the row stops counting and the check blocks
255
- again: record it again if it still stands. The CLI prints every `user-declined` row marked for
256
- the user to confirm.
257
-
258
- Refused whatever the note says: `out-of-time`, `out-of-context`, `too-many-routes`,
259
- `tool-failed`, `not-asked`, `good-enough`. A sentence to the user about a gap is not an
260
- exception either: the gate stays incomplete, and the CLI exits non-zero with that gap listed.
261
-
262
- ### What setup derives on a collection's item pages
263
-
264
- Setup reads a few entry pages of each collection and makes one field of everything they render
265
- at the same place: text, each image (an `<img>` with only a `srcset`, a `<picture>`, a
266
- background its stylesheet paints), a call to action (one `button`, or a label and its `-href`
267
- where the project derives pairs), a plain list, and a gallery or spec table as a repeater whose
268
- rows each entry fills from its own page. The article's container becomes the place the entry's
269
- `body` is read from and bound, so `body` holds the article alone. An element the site's code
270
- already declares keeps its declaration and gets no second field. A collection from before this
271
- gains the fields add-only; its `body` is bound only while no entry of it was edited. Check the
272
- result per entry route in the gate, never by adding the same fields by hand.
273
-
274
- Copy the template shows unchanged on every entry (a "Related posts" heading, an "Add to cart"
275
- button) is one field marked `config.shared`, and its value lives on that field, once: no entry
276
- holds it. Edit it as a field of any one entry and publish that entry: the value moves onto the
277
- field and every item page is repainted. Until then it is that entry's draft, shown on no page.
278
- A translated entry keeps its own words for it. Never write the same value into each entry.
279
-
280
- Git is answered before you start, never asked at the end. `bettercms init` asks the owner once
281
- whether BetterCMS may push, and the first prompt carries the answer:
282
-
283
- - **Yes:** after every change to the repository, commit it, push, wait until `get_deploy_status`
284
- shows the build of your commit live, run `editor_setup`, then read `get_next_steps`. The gate
285
- measures the build of their repository, so a change you did not push can never pass.
286
- - **No, or the prompt says nothing about it:** do not push. Verification releases go out from the
287
- working tree, so the job never needs a commit to prove itself; say once, in the final report,
288
- which routes wait on their push and that an unpushed conversion is lost on the next rebuild
289
- from their repository.
290
-
291
- The same goes for the authoring answer and where forms send submissions: when the first prompt
292
- states one, it is settled. Do not ask again, and do not end on a question the prompt answered.
293
-
294
- ## The gap ledger: `.bettercms/ledger.json`
295
-
296
- Keep the ledger as a file in the repo, not in chat: it survives a context reset, a new session
297
- and a second agent, and "Continue BetterCMS setup" re-reads it. It holds no secrets. One row per
298
- actionable gap:
299
-
300
- | Field | Meaning |
301
- |---|---|
302
- | `id`, `category`, `severity` | Stable issue identity and priority |
303
- | `route`, `sourcePath`, `cmsId` | Where the issue lives |
304
- | `expected`, `observed` | Evidence, not a vague opinion |
305
- | `repair`, `owner`, `status` | Planned action and accountable party |
306
- | `verification` | Link, receipt or screenshot proving the fix |
307
-
308
- ```json
309
- { "projectId": "…", "updatedAt": "…", "gaps": [
310
- { "id": "pricing-cta-href", "category": "bindings", "severity": "high", "route": "/pricing",
311
- "sourcePath": "src/components/Cta.astro", "cmsId": null,
312
- "expected": "CTA href editable", "observed": "hard-coded href", "repair": "bind via data-bcms-props",
313
- "owner": "agent", "status": "open", "verification": null } ] }
314
- ```
315
-
316
- Statuses: `open`, `in-progress`, `verified`, `requires-decision`, `blocked-by-platform`,
317
- `excluded` (with a reason). Never erase a gap after a failed repair: keep the prior observation
318
- and add the new result. Update the file as each row moves, so a reset loses at most one row.
319
-
320
- ## Repair order, and the reference for each step
321
-
322
- | # | Step | Reference |
323
- |---|---|---|
324
- | 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) |
325
- | 1 | Routing and data shape: page / collection / entry classification and folders; keep public URLs | [content-modeling.md](content-modeling.md) |
326
- | 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) |
327
- | 3 | Editor contract: field types, labels, help text settled on every element — a line only where the editor needs one, most get none (`get_next_steps` shows no `help-text-missing`), validation, Portable Text accept lists, nested groups | [portable-text.md](portable-text.md) |
328
- | 4 | Design: media, logo roles, fonts and brand tokens, without changing visual intent | [media-and-brand.md](media-and-brand.md) |
329
- | 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) |
330
- | 6 | Behaviour: forms, links, menus, accordions, modals, consent, custom code | [forms-and-interactions.md](forms-and-interactions.md) |
331
- | 7 | Discoverability: native SEO, structured data, robots and sitemap, redirects, internal links | [seo.md](seo.md) |
332
- | 8 | Editor chrome, last and never a condition of being done. The import has already given each prop its V2 `ui`, from its closest `bettercms://kit` entry or the generic rules. `receipt.gate.kit` lists the components that name no kit entry, each with the import's closest match as `hint`: where you have the time, read the entry, check its htmlSignals and pitfalls against the section, and name it (`update_component { kitEntry }`), which rebuilds the ui from it. Do it after the gate is complete, one component at a time. Never report it as pending, blocked or left over | [editor-chrome.md](editor-chrome.md) |
333
-
334
- Load the step's reference when the step starts, and let it go when the step's rows are verified.
335
-
336
- ## Proof loop
337
-
338
- For each route class and component family, test one safe draft edit for every field class:
339
- plain text, rich text, image, alt text, link, repeatable group, component slot, and form
340
- reference where present. Confirm the correct element changes in preview and other instances do
341
- not change by accident. Restore test values. Verify a default change reaches only instances
342
- without an override.
343
-
344
- Build with the repository's own build script. Fetch live or staging URLs and check HTTP status
345
- and the rendered copy, not dashboard status. Check responsive screenshots, keyboard use, form
346
- status, assets, SEO output and broken links. Record the freshness of `get_site_composition`,
347
- `get_binding_report` and `list_seo_issues`: a binding report whose `refreshReason` is
348
- never-generated or outdated is not proof, and on a headless site no BetterCMS release ever
349
- writes one. Per-route checks fan out well to subagents; have each return its verdict and the
350
- failing evidence only.
351
-
352
- **Coverage:** `verified intended fields / discovered intended fields`, plus route, section, asset
353
- and form counts. A gap is excluded only with a reason, and 100% is never reported while
354
- exclusions hide core editorial content.
355
-
356
- Cases that must be tested:
357
-
358
- - A hero repeated on two pages: one instance override changes only one page; a definition
359
- default changes the other.
360
- - A collection detail template with at least two entries and an entry-specific image and SEO
361
- value.
362
- - Long and empty copy at desktop and mobile widths.
363
- - A rich-text custom block, a missing asset, a broken internal link, and a form with an
364
- unverified destination.
365
- - A component whose CMS field saves while hard-coded source still renders.
366
-
367
- ## Launch decision
368
-
369
- 1. **Completed:** the routes and components repaired, with evidence.
370
- 2. **Remaining:** gaps that need a product change, an integration credential, a licence
371
- decision, an editor decision or a manual approval.
372
- 3. **Recommendation:** ready, ready with named non-critical exceptions, or not ready, with
373
- reasons and the rollback point.
374
-
375
- A clean draft audit is not the user's publish authorization. On headless, verify the external
376
- host's deployed code and content; on managed and hybrid, verify the release with
377
- `get_deploy_status` and a cache-busted public response ([publishing.md](publishing.md)).
1
+ ---
2
+ title: "Finish an imported site"
3
+ description: "Take an imported or converted site to fully editable: the gap ledger in .bettercms/ledger.json, the repair order, and the proof loop per route."
4
+ ---
5
+
6
+ # Finish an imported or converted site
7
+
8
+ **Outcome.** Every intended value editable, sections reusable, design faithful, and launch
9
+ verified with evidence. "Fully editable" is a measured result, never an inference from imported
10
+ schemas. New to the project, or no repo on disk yet: [getting-started.md](getting-started.md).
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
+
19
+ ## Inputs and baseline
20
+
21
+ Require the project, source repository or import snapshot, site URL, import manifest, hosting
22
+ mode and the user's goal. Confirm the bound project with `get_project` (hosting mode,
23
+ `authoringPreference`), read `bettercms://playbook/schema`, and inspect the repository and the
24
+ previous import report. If any are unavailable, continue with a read-only gap analysis and name
25
+ the missing evidence.
26
+
27
+ Capture a baseline before editing: routes, screenshots at representative widths, content model
28
+ and entry counts, component and global-layout inventory (`get_site_composition`, `get_layout`),
29
+ media and form inventory, `list_seo_issues`, `get_deploy_status`, `get_binding_report`, and the
30
+ source commit. Keep existing URLs and behaviour unless the user asked for a change.
31
+
32
+ ## The finish line for a convert or build job
33
+
34
+ A conversion is done only when the `convert_site` prompt's loop is: `submit_conversion_receipt`
35
+ answers `complete: true` (no path left in `paths.pending`), and after the deploy
36
+ `get_binding_report` shows `unmatched` at 0, `coverage.pending` empty and, once
37
+ `set_binding_mode` has flipped the site to declared and a second release has run, mode
38
+ `declared`. A build job works from `bettercms://playbook/schema` (the `build_site` prompt carries
39
+ the same flow on a local MCP) and is done when the site is deployed, `get_component_readiness`
40
+ is clean for every component it placed, and `get_next_steps` lists nothing left to do from code.
41
+ Both end in the verify loop of SKILL.md, and an imported site ends only on the setup gate below.
42
+ One converted page, or a receipt with paths still pending, is not a finished site.
43
+
44
+ Setup is not a conversion. An imported site whose release serves its pages is editable through the
45
+ fields the release binds on the built page, and the setup gate measures exactly that. Run the
46
+ convert codemod only for what the gate asks it for (`run-sections-conversion`, a form to wire) or
47
+ when the owner asks for the source to declare its bindings. "The codemod bound few paths" is no
48
+ gate failure and nothing to report to the owner.
49
+
50
+ ## The setup gate: what "finished" is measured by
51
+
52
+ `get_next_steps` returns `receipt.complete` and `receipt.gate`. The platform computes the gate
53
+ from the last release, one verdict per route of the live build. The CLI that launched you reads
54
+ it again when you exit and prints it to the user, so the gate is the report, not your summary.
55
+ While the gate is incomplete that command exits 3 (0 once it is complete; 2 is a wrong command
56
+ line), which is what a script waiting on the setup reads.
57
+
58
+ | Check | Passes when | Where it shows |
59
+ |---|---|---|
60
+ | `page` | a CMS page, or a collection entry, serves the route | `routes.rows[].page` |
61
+ | `published` | that page, and its entry, is published | `routes.rows[].page` |
62
+ | `twin` | no second page claims the same route | `blocking.rows` |
63
+ | `text` | 2% or less of the visible characters sit outside a field | `routes.rows[].text` |
64
+ | `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` |
65
+ | `link` | 95% of the links have a field for the label or the destination | `routes.rows[].link` |
66
+ | `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) |
67
+ | `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` |
68
+ | `svg` | every inline SVG a visitor sees (a logo, an icon, an illustration) belongs to a field; a hidden symbol sheet is not counted, nor the glyphs of a rating drawn as one image (`role="img"` round them) | `routes.rows[].svg` |
69
+ | `number` | every element whose whole text is a number (a stat, a price, a rating) belongs to a field | `routes.rows[].number` |
70
+ | `attribute` | every `alt`, `aria-label`, `title` and `placeholder` with words in it belongs to a field; copy inside a field's rich text is that field's, and a picture's or icon's name is its field's | `routes.rows[].attribute` |
71
+ | `names` | never a row: a name holds nothing open. A field, a section, a component or a prop whose name a person cannot read (a slug, a hash, a structural word such as `Section 2` or `wrap`, a number beside the same name such as `Icon 2`) is listed as advice with the call that renames it. The import already names a section by its own title, and a section's list by that title, where the markup gave it nothing better | `advice` (check `names`), and `naming` |
72
+ | `seo` | the page has a title and a description, and a canonical where the build ships one for this site's origin. A title over 70 characters or a description over 200 is the site's own copy as built, and a site with no default share image has none to give: both are advice (`receipt.gate.advice`), never rows | `blocking.rows` |
73
+ | `structuredData` | JSON-LD the build ships on the route is on the page or the entry | `blocking.rows` |
74
+ | `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` |
75
+ | `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` |
76
+ | `components` | the authoring question is answered, and no section is left that a componentize run would place. A heading or paragraph that stands outside any section is a field of its page, not a section: advice (`receipt.gate.advice`) | `blocking.rows`, `route: null` |
77
+ | `forms` | every form of the customer's reaches somebody: its own address, else the project's default, else the workspace's owners, the order a submission is sent in; a form setup ignored (sign-in, add to basket) is not counted | `blocking.rows`, `route: null` |
78
+ | `dock` | no component is left with props the server's own ui derivation would still dress: one it has not reached has no prop carrying V2 `ui`, or a list prop (`table`, `array`, `multi-reference`) with no row title (`ui.preview.title` or `ui.itemLabel`). The import derives each prop's ui when it makes a component, from its closest `bettercms://kit` entry or the generic rules, so a fresh import has no such row; a component whose props it leaves to the default controls (two short texts) is done. Button looks are not counted. Which kit entry a component IS is not part of this check: it is advice, `receipt.gate.kit` (below) | `blocking.rows`, `route: null`, the component and its props in `detail` |
79
+
80
+ `pages` counts routes built, created and published. `coverage` sums `editable` of `total` across
81
+ the site, `svg`, `number` and `attribute` included, and `open` counts the rows still open per
82
+ check. `routes.rows` is ordered worst first, with `failing` per route and the reasons the
83
+ deriver `skipped` copy there. `blocking.rows` is the work list: each row carries a `detail` with
84
+ its number, a `fixCode` and a `fix`, the call that closes it.
85
+
86
+ `nextAction` is the one thing to do now: the fix a tool can make that closes the most rows
87
+ (`fixCode`, `count`, the `fix` of its first row, the first `routes`), two with the same count in a
88
+ fixed order. Run it on every row that carries that `fixCode`, call `get_next_steps`, and repeat
89
+ until `complete`: following it reaches `complete` with no step that changes nothing. A fix no
90
+ tool can make is named only when nothing else is left. A `fix` of `null` means no tool closes that row by itself: its
91
+ `detail` names what to change in the site's code, so change it, commit, push and `rebuild_project`.
92
+ Only a collection's listing route, a URL that cannot be placed and two pages nobody but the owner
93
+ can choose between are left; record each as an exception with a true reason, never stop to ask. A
94
+ row its fix did not move twice is recorded the same way.
95
+
96
+ A `sections` or `style` row is a page nobody can add a section to, reorder or space, however
97
+ complete its copy is. Its `fixCode` names the one fix, and `fix` the call when a tool can make it:
98
+
99
+ | `fixCode` | The fix |
100
+ |---|---|
101
+ | `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 |
102
+ | `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. Push what changed, then `rebuild_project` and `get_deploy_status { waitSeconds: 50 }` until it is live |
103
+ | `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, `update_project { structureInPlace: true }`. No repository change |
104
+ | `run-setup` | Nothing measured what the row reads, or a page is still to register: `editor_setup { force: true }`, then read again |
105
+
106
+ The other rows name their fix the same way:
107
+
108
+ | `fixCode` | The fix |
109
+ |---|---|
110
+ | `bind-elements` | What no field owns on the route is `pages[].skipped` of `get_conversion_brief { complete: true }` (each node with why the import left it) and the `unaddressable` buckets of `get_binding_report`. Declare each in the site's template (`data-bcms-field`; an SVG as an image or icon field, a number as its own field, attribute copy with `data-bcms-props`) and push. **Do not run `@bettercms-ai/convert` for this row.** The brief's `paths` are fields the CMS already has, and on a built site the release has already bound them on the page: the codemod creates no field, and run over them it refuses copy that lives in Markdown, pictures from a content collection and values a helper formats, by the thousand |
111
+ | `rename-fields` | `get_page`, read the section's source, then name every listed field of the page in ONE `update_content_model` call (`pageId`, `fieldLabel` and `fieldHelpText` keyed by field path, `ifMatch` from `get_page`'s `backingModel`), or `update_field` for a single one; `update_component` for a component (`name`, and `propLabel` / `propHelpText` for its props in one call). See "Names" below |
112
+ | `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` |
113
+ | `import-structured-data` | `editor_setup { force: true }` imports the build's JSON-LD; `update_seo` when it has to be written by hand |
114
+ | `model-collection` | not a row any more. Setup derives a collection wherever a family's pages share one template, with its fields, its entries and their bodies. A family it left as pages is listed as advice in `receipt.gate.families`: every route in it is an editable page. **Never `create_content_model` for it on an imported site.** One hand-written model reads the whole project as hand-authored, and the import then registers no further page, derives no collection and adds no field, which is where "the model holds only title and description" comes from. Mention the family to the owner as something a collection could hold later, and go on |
115
+ | `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)) |
116
+ | `componentize` | `set_authoring_preference` when nobody answered, then `get_componentize_plan` and `componentize_sections` ([componentize.md](componentize.md)) |
117
+ | `set-form-recipient` | `update_form` with `notifyEmails` set to the address the owner gave when setup started (it is in your first prompt); `update_form { ignored: true }` when it is not one of their forms. Only a form the built site posts to BetterCMS is listed, and only when a submission would reach nobody at all (no address on the form, none on the project, no owner on the workspace); the release publishes it once it is wired. Never ask for an address at the end of a run |
118
+ | `set-dock-ui` | the last step of the repair order below: `get_component`, then `update_component` with `propUi` and `propTab` for the props the row names ([editor-chrome.md](editor-chrome.md)) |
119
+
120
+ A row nothing measured reads `unmeasured: true` with `run-setup`, and `gate.unmeasured` lists the
121
+ checks that have one: a release older than a count, or a project set up before setup recorded
122
+ what it found on the build (the style marker of each section root, the looks it measured, the
123
+ canonicals the build ships). It is not a failure of the site and not a pass either: run
124
+ `editor_setup { force: true }` once and read again.
125
+
126
+ Setup closes what is deterministic by itself, on every run, so your first receipt is already past
127
+ it and an open row of these means run setup again: `run-setup`, `derive-buttons` (the family and
128
+ the map), `import-structured-data`, `fill-seo` from the built head and the page's own copy,
129
+ `rename-fields` for a field still carrying a derived name, `componentize` once the authoring
130
+ answer is `components`, and `enable-in-place-structure` on a project it creates. What is left
131
+ takes judgement and is yours: `bind-elements`, `set-form-recipient` (the
132
+ address given at the start), `run-sections-conversion`, a name a person gave, a title over its limit, a look named by a number, and
133
+ the authoring answer itself (the user's). `rebuild` is the user's.
134
+
135
+ Beside the gate, `data` lists what else is unfinished, each item by its `ruleId`: content
136
+ (`mcp-pages-draft`, `mcp-pages-missing-meta`, `mcp-model-empty`, `mcp-form-no-recipient`,
137
+ `mcp-pending-review`), bindings and structure (`conversion-pending`, `bindings-not-declared`,
138
+ `unaddressable-copy`, `upgrade-section-roots`, `upgrade-element-styles`, `componentize-pages`,
139
+ `components-unpublished`), the authoring answer (`authoring-undecided`), the Layout lane
140
+ (`layout-write-blocked`, `layout-field-unread`, `layout-locator-stale`, `layout-rebuild-required`,
141
+ `layout-unpublished`, `chrome-duplicated`), publishes (`publish-held`) and the Content sidebar
142
+ (`organise-content`). `setup-gate` is first while the gate is open and `publish-held` next.
143
+
144
+ Stamps are not enough to pass either check: the lane that serves a route must have PROVED it.
145
+ Each release adds a probe section to the middle of one built page of each kind (a page, a
146
+ listing, an item template) and a probe padding to one of its sections, in memory, finds both in
147
+ the result and takes them out again; the sections conversion proves each page it wraps the same
148
+ way. `routes.rows[].proof` is that record (`lane`, `route`, `insert`, `style`: `ok` or the
149
+ reason). A row whose proof was never run asks for it where it runs: `editor_setup { force: true }`
150
+ for the release, the conversion again for a wrapped page. `SLOTS_NOT_PLACED` on a wrapped page
151
+ means every slot is still `code:`: place the page's sections, then run the conversion again.
152
+
153
+ The gate picks the fix per route, in that order of evidence: a wrapped page waits for its build; a
154
+ page a script draws needs the conversion; a static page needs only the switch. Run the fix the
155
+ row names and no other: the conversion does nothing for a row that says `rebuild`.
156
+
157
+ Two rows have no `fixCode`. A page that places a section twice: remove the second placement with
158
+ `update_page`. A route neither lane serves: the release cannot write it (`refusal`) and the
159
+ conversion refused it (`wrap`); its `detail` names both. Put it to the user: write the wrap by
160
+ hand ([astro.md](astro.md), [next.md](next.md)) or change the page. No exception covers it
161
+ (`document-body` excuses only a route that places no section). Check a route's row again after
162
+ each release; never mark one done from the codemod's own output.
163
+
164
+ A `page` row on a route setup left without a page carries the pages lane's reason in its
165
+ `detail` (`invalid-path`, `html-too-large`, `page-deleted`, `slug-taken`, `folder-conflict`,
166
+ `entry-not-registered`, `authored-content` and the rest), with what closes it: a change in the
167
+ site's code, another page to move, or the user's decision. Only `unreadable`, `write-failed`
168
+ and `not-registered` carry a `fix`. An `image` row with a `null` fix names a picture by its code
169
+ ([media-and-brand.md](media-and-brand.md) Where images hide): the change is in the site's code.
170
+ `imageGaps` also lists the decoration the import declined, which blocks nothing. On a
171
+ collection's listing route the rows that link to its entries are counted as the collection's,
172
+ so the listing page is judged on its own heading, intro and calls to action, and on the words
173
+ of a row the release bound to its entry (a field's there too). The release binds
174
+ each such row to its entry: the row carries `data-bcms-entry`, a part showing a field as stored
175
+ carries `data-bcms-entry-field` (`entry:<id>:<path>`, written when that entry publishes), and a
176
+ part the site's code made from a field (a cut excerpt, a reformatted date or price, a thumbnail,
177
+ the item link) carries `data-bcms-entry-from` with the transform: edit the entry. When it publishes,
178
+ a picture (`|rendition`) and a number (`|format`) are written in place; any other such part that no
179
+ longer shows its value is reported `entry-derived` with why (`truncate`, `route`, `format`,
180
+ `art-direction`, `repo-path`, `format-unreadable`).
181
+ Never add a field for a row to the listing page. A taxonomy's term routes (`/tags/<term>`, each
182
+ listing another collection's entries) are a small collection of terms, and their rows are bound
183
+ the same way. A term has a `body` only where its template has an article. Chrome the Layout
184
+ could not take (an icon with no Media asset) is template copy on item routes: one shared field
185
+ of the collection, as it is a page's own field on a page.
186
+
187
+ ### Names
188
+
189
+ The import names a field by its section and its role there (`Hero headline`, `FAQ questions`).
190
+ Where the markup says no more it numbers them (`Pricing text 4`, `Hero image 2`), and a section
191
+ whose classes name nothing is called by its heading's words. No build can say that a picture is
192
+ the dashboard screenshot; the site's code can. So these never hold the gate open: they are
193
+ `receipt.gate.naming`, and `get_next_steps` lists the step `names-from-code` until each is
194
+ renamed. Do it in the same job, after the gate's own rows and before the final report: an
195
+ editor reads these names all day, and a setup that leaves them is not finished.
196
+
197
+ Name from the site's code, the way a developer writes a schema. Open the section's source file
198
+ and use what it calls things (`plan.cents`, `billingFrequency`, `StatsBand.astro`):
199
+
200
+ | What | The name | Like | Never |
201
+ |---|---|---|---|
202
+ | A section's KIND (a component's `sectionType`) | what it IS, one to three words, sentence case. The picker lists kinds and a page's outline shows it under each section | `Announcement banner`, `Hero`, `Logo cloud`, `Feature tabs`, `Testimonial`, `Pricing`, `FAQ`, `Call to action` | its heading's copy or first word (`Simple`, `Trusted`), `Section 4`, a file path |
203
+ | A layout of that kind (a component's `name`) | how THIS layout differs from the kind's others; the kind alone when it has one layout | `Centered`, `Split with media`, `Pricing` | its heading's copy, the page it came from with a number (`Hero 2`) |
204
+ | One placed section's title | nothing you write. The outline shows the section's own heading, as written and read live, with its kind underneath | `Equip Your Projects with ScrewFast` over `Hero` | a `label` you invent, a shortened or reworded heading, the kind said again |
205
+ | A field or prop | what it HOLDS there, one to three words | `Dismiss key`, `Starter plan price`, `Dashboard screenshot`, `Billing frequency` | its own copy, a role with a number (`Text 4`), the section's name alone |
206
+ | Help text | most fields get none. One plain sentence under 90 characters, only for a format or unit, a place the canvas cannot show, a real constraint or a fallback. No semicolons, no dashes, no values from the page | `Leave empty to use the page title.` | the label or the value said again, writing advice, a figure copied from another page |
207
+
208
+ **Advice is not work left**, except the names. `gate.naming` is part of the job: rename every
209
+ row before you report, as above. `receipt.gate.advice`, `gate.families` and `gate.kit`
210
+ are things a person may want to change about the site's own content or build: a title longer than
211
+ a search result shows, no default share image, an element outside any section, a route family that
212
+ could be a collection, a kit entry to confirm, a name to sharpen. The site is fully editable with
213
+ every one of them standing. When `gate.complete` is true and `gate.naming` is absent or empty the setup is
214
+ DONE: say so, list the advice in a few lines as optional improvements, and never call any of it pending, blocked,
215
+ remaining or left over, and never end on a question about it.
216
+
217
+ A section says two things: what it IS (its kind) and what it SAYS (its title). Only the kind is
218
+ yours to judge. The import sets it from the band's shape or the site's own word for it; where it
219
+ could read neither, the kind is the key's word, often the heading's first, and you correct it with
220
+ `update_component { sectionType }`, spelled exactly like the kind's other layouts. The title is
221
+ never named by anyone: the outline prints the section's heading as the page holds it, so two
222
+ heroes are `Hero` twice in the picker and two different titles on the page. Do not write a block
223
+ `label` to title a section, and do not shorten, re-case or reword a heading into one. A `label`
224
+ is only ever a person's own rename.
225
+
226
+ A numbered run that is one thing repeated (plans, features, logos) is a list, not twelve names:
227
+ fold it into one list field or one `table` prop whose rows are the cards
228
+ ([componentize.md](componentize.md)), and name the list (`Plans`). A name you write is yours: a
229
+ later setup never renames it.
230
+
231
+ ### Exceptions go on the record
232
+
233
+ A check you are leaving on purpose is recorded, one row per route and check, with
234
+ `editor_setup` and `exempt: [{ route, check, reason, note }]`. `note` states what is being left
235
+ in a sentence a person can verify. `reason: null` withdraws one.
236
+
237
+ | `reason` | May excuse |
238
+ |---|---|
239
+ | `form-validation-copy` | `text` |
240
+ | `legal-boilerplate` | `text` |
241
+ | `third-party-widget` | `text`, `image`, `link`, `svg`, `number`, `attribute` |
242
+ | `decorative` | `text`, `image`, `svg` |
243
+ | `duplicate-track` | `text`, `image`, `link`, `svg`, `number`, `attribute` (the cloned track of a marquee or slider) |
244
+ | `client-only-island` | nothing: it lists an island the build cannot show, and the route still meets the bar outside it |
245
+ | `user-declined` | `page`, `published`, `text`, `image`, `link`, `svg`, `number`, `attribute`, `seo`, `structuredData`, `collections`, `dock`, and only with the user's own words quoted in `note` (`User: "leave pricing in code"`) |
246
+ | `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) |
247
+ | `default-controls` | `dock`, on `route: "component/<slug>"`: a component whose props read right with the dock's default controls (one short heading, a toggle); `note` says why |
248
+
249
+ Nothing else excuses `sections`, and nothing excuses `style`, `twin`, `names`, `buttons`,
250
+ `components` or `forms`: not the user's decision either. A name is fixed by renaming, and a form
251
+ that is not the customer's is marked ignored.
252
+
253
+ A row excuses what its check read when it was recorded. When the route reads differently
254
+ later (more copy nobody owns, another picture), the row stops counting and the check blocks
255
+ again: record it again if it still stands. The CLI prints every `user-declined` row marked for
256
+ the user to confirm.
257
+
258
+ Refused whatever the note says: `out-of-time`, `out-of-context`, `too-many-routes`,
259
+ `tool-failed`, `not-asked`, `good-enough`. A sentence to the user about a gap is not an
260
+ exception either: the gate stays incomplete, and the CLI exits non-zero with that gap listed.
261
+
262
+ ### What setup derives on a collection's item pages
263
+
264
+ Setup reads a few entry pages of each collection and makes one field of everything they render
265
+ at the same place: text, each image (an `<img>` with only a `srcset`, a `<picture>`, a
266
+ background its stylesheet paints), a call to action (one `button`, or a label and its `-href`
267
+ where the project derives pairs), a plain list, and a gallery or spec table as a repeater whose
268
+ rows each entry fills from its own page. The article's container becomes the place the entry's
269
+ `body` is read from and bound, so `body` holds the article alone. An element the site's code
270
+ already declares keeps its declaration and gets no second field. A collection from before this
271
+ gains the fields add-only; its `body` is bound only while no entry of it was edited. Check the
272
+ result per entry route in the gate, never by adding the same fields by hand.
273
+
274
+ Copy the template shows unchanged on every entry (a "Related posts" heading, an "Add to cart"
275
+ button) is one field marked `config.shared`, and its value lives on that field, once: no entry
276
+ holds it. Edit it as a field of any one entry and publish that entry: the value moves onto the
277
+ field and every item page is repainted. Until then it is that entry's draft, shown on no page.
278
+ A translated entry keeps its own words for it. Never write the same value into each entry.
279
+
280
+ Git is answered before you start, never asked at the end. `bettercms init` asks the owner once
281
+ whether BetterCMS may push, and the first prompt carries the answer:
282
+
283
+ - **Yes:** after every change to the repository, commit it, push, wait until `get_deploy_status`
284
+ shows the build of your commit live, run `editor_setup`, then read `get_next_steps`. The gate
285
+ measures the build of their repository, so a change you did not push can never pass.
286
+ - **No, or the prompt says nothing about it:** do not push. Verification releases go out from the
287
+ working tree, so the job never needs a commit to prove itself; say once, in the final report,
288
+ which routes wait on their push and that an unpushed conversion is lost on the next rebuild
289
+ from their repository.
290
+
291
+ The same goes for the authoring answer and where forms send submissions: when the first prompt
292
+ states one, it is settled. Do not ask again, and do not end on a question the prompt answered.
293
+
294
+ ## The gap ledger: `.bettercms/ledger.json`
295
+
296
+ Keep the ledger as a file in the repo, not in chat: it survives a context reset, a new session
297
+ and a second agent, and "Continue BetterCMS setup" re-reads it. It holds no secrets. One row per
298
+ actionable gap:
299
+
300
+ | Field | Meaning |
301
+ |---|---|
302
+ | `id`, `category`, `severity` | Stable issue identity and priority |
303
+ | `route`, `sourcePath`, `cmsId` | Where the issue lives |
304
+ | `expected`, `observed` | Evidence, not a vague opinion |
305
+ | `repair`, `owner`, `status` | Planned action and accountable party |
306
+ | `verification` | Link, receipt or screenshot proving the fix |
307
+
308
+ ```json
309
+ { "projectId": "…", "updatedAt": "…", "gaps": [
310
+ { "id": "pricing-cta-href", "category": "bindings", "severity": "high", "route": "/pricing",
311
+ "sourcePath": "src/components/Cta.astro", "cmsId": null,
312
+ "expected": "CTA href editable", "observed": "hard-coded href", "repair": "bind via data-bcms-props",
313
+ "owner": "agent", "status": "open", "verification": null } ] }
314
+ ```
315
+
316
+ Statuses: `open`, `in-progress`, `verified`, `requires-decision`, `blocked-by-platform`,
317
+ `excluded` (with a reason). Never erase a gap after a failed repair: keep the prior observation
318
+ and add the new result. Update the file as each row moves, so a reset loses at most one row.
319
+
320
+ ## Repair order, and the reference for each step
321
+
322
+ | # | Step | Reference |
323
+ |---|---|---|
324
+ | 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) |
325
+ | 1 | Routing and data shape: page / collection / entry classification and folders; keep public URLs | [content-modeling.md](content-modeling.md) |
326
+ | 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) |
327
+ | 3 | Editor contract: field types, labels, help text settled on every element — a line only where the editor needs one, most get none (`get_next_steps` shows no `help-text-missing`), validation, Portable Text accept lists, nested groups | [portable-text.md](portable-text.md) |
328
+ | 4 | Design: media, logo roles, fonts and brand tokens, without changing visual intent | [media-and-brand.md](media-and-brand.md) |
329
+ | 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) |
330
+ | 6 | Behaviour: forms, links, menus, accordions, modals, consent, custom code | [forms-and-interactions.md](forms-and-interactions.md) |
331
+ | 7 | Discoverability: native SEO, structured data, robots and sitemap, redirects, internal links | [seo.md](seo.md) |
332
+ | 8 | Editor chrome, last and never a condition of being done. The import has already given each prop its V2 `ui`, from its closest `bettercms://kit` entry or the generic rules. `receipt.gate.kit` lists the components that name no kit entry, each with the import's closest match as `hint`: where you have the time, read the entry, check its htmlSignals and pitfalls against the section, and name it (`update_component { kitEntry }`), which rebuilds the ui from it. Do it after the gate is complete, one component at a time. Never report it as pending, blocked or left over | [editor-chrome.md](editor-chrome.md) |
333
+
334
+ Load the step's reference when the step starts, and let it go when the step's rows are verified.
335
+
336
+ ## Proof loop
337
+
338
+ For each route class and component family, test one safe draft edit for every field class:
339
+ plain text, rich text, image, alt text, link, repeatable group, component slot, and form
340
+ reference where present. Confirm the correct element changes in preview and other instances do
341
+ not change by accident. Restore test values. Verify a default change reaches only instances
342
+ without an override.
343
+
344
+ Build with the repository's own build script. Fetch live or staging URLs and check HTTP status
345
+ and the rendered copy, not dashboard status. Check responsive screenshots, keyboard use, form
346
+ status, assets, SEO output and broken links. Record the freshness of `get_site_composition`,
347
+ `get_binding_report` and `list_seo_issues`: a binding report whose `refreshReason` is
348
+ never-generated or outdated is not proof, and on a headless site no BetterCMS release ever
349
+ writes one. Per-route checks fan out well to subagents; have each return its verdict and the
350
+ failing evidence only.
351
+
352
+ **Coverage:** `verified intended fields / discovered intended fields`, plus route, section, asset
353
+ and form counts. A gap is excluded only with a reason, and 100% is never reported while
354
+ exclusions hide core editorial content.
355
+
356
+ Cases that must be tested:
357
+
358
+ - A hero repeated on two pages: one instance override changes only one page; a definition
359
+ default changes the other.
360
+ - A collection detail template with at least two entries and an entry-specific image and SEO
361
+ value.
362
+ - Long and empty copy at desktop and mobile widths.
363
+ - A rich-text custom block, a missing asset, a broken internal link, and a form with an
364
+ unverified destination.
365
+ - A component whose CMS field saves while hard-coded source still renders.
366
+
367
+ ## Launch decision
368
+
369
+ 1. **Completed:** the routes and components repaired, with evidence.
370
+ 2. **Remaining:** gaps that need a product change, an integration credential, a licence
371
+ decision, an editor decision or a manual approval.
372
+ 3. **Recommendation:** ready, ready with named non-critical exceptions, or not ready, with
373
+ reasons and the rollback point.
374
+
375
+ A clean draft audit is not the user's publish authorization. On headless, verify the external
376
+ host's deployed code and content; on managed and hybrid, verify the release with
377
+ `get_deploy_status` and a cache-busted public response ([publishing.md](publishing.md)).