@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.
@@ -59,7 +59,10 @@ These hold on every task. The reference in brackets has the detail and the codes
59
59
  beside a link field. [components.md, content-modeling.md]
60
60
  4. **Nothing is live until it is published, and a page is not its content.** A draft form,
61
61
  component or entry renders nothing live. Read `warnings` on every write: `ENTRY_STILL_DRAFT`
62
- and `ENTRY_SEED_FAILED` mean content did not ship. [publishing.md]
62
+ and `ENTRY_SEED_FAILED` mean content did not ship. An answer that leads with `held`, and a
63
+ publish reading `outcome.status: "partial"`, did not reach the site: do the action it names
64
+ before you report it.
65
+ [publishing.md]
63
66
  5. **Name the folder system every time you say "folder"**: Content sidebar folders never touch a
64
67
  URL, URL page folders are part of the path, media folders organise assets.
65
68
  [content-modeling.md]
@@ -126,7 +129,7 @@ resources: `bettercms://skill/bettercms` for this file, and each reference under
126
129
  | Components, props, variants, component publish codes | [components.md](references/components.md) | creating, swapping, publishing or validating a component | component, prop, variant, sectionType, publish component |
127
130
  | One section, source HTML to bound template: hero, feature grid, pricing table, testimonials, FAQ | [worked-examples.md](references/worked-examples.md) | you want the whole path for one section in production shapes | example, hero, pricing, FAQ, testimonial, source to schema |
128
131
  | The same for an article entry, navigation in the Global Layout, tabs holding columns | [worked-examples-advanced.md](references/worked-examples-advanced.md) | a collection entry with a body, site chrome, or nested blocks | example, blog post, document body, mega menu, tabs, columns |
129
- | Componentize a derived site | [componentize.md](references/componentize.md) | `get_site_composition` shows pages made of fields only | componentize, sections, fields-only, get_componentize_plan |
132
+ | Componentize a derived site | [componentize.md](references/componentize.md) | `get_site_composition` shows pages made of fields only | componentize, sections, fields-only, get_componentize_plan, BcmsSections, --sections |
130
133
  | Click-to-edit attributes, addresses, declared mode | [bindings.md](references/bindings.md) | `grep -rl "data-bcms-" src` finds files; a value the editor cannot select | data-bcms-field, click to edit, binding report, stega, declared |
131
134
  | Animated sites: GSAP, ScrollTrigger, split text, Framer Motion, Lenis, marquees, carousels, Lottie, hydrated islands | [animation.md](references/animation.md) | `package.json` lists `gsap`, `lenis`, `framer-motion`, `swiper` or a text splitter; a publish refused with `SPLIT_PATTERN_UNKNOWN` or `ISLAND_PROPS_UNMATCHED`; a `client-only-island` gate exception | animation, GSAP, ScrollTrigger, SplitText, Lenis, Framer Motion, marquee, Swiper, Lottie, island, client:only, data-bcms-split |
132
135
  | Wiring, env, hosting modes, media host, snapshots | [frameworks.md](references/frameworks.md) | `package.json` lists an adapter; `bcms-content.json` exists | hosting, headless, env, codegen, types, revalidate, snapshot |
@@ -140,7 +143,7 @@ resources: `bettercms://skill/bettercms` for this file, and each reference under
140
143
  | SEO and AEO | [seo.md](references/seo.md) | meta, JSON-LD, sitemap, robots, redirects, AI crawlers, `list_seo_issues` | SEO, meta, JSON-LD, schema.org, sitemap, robots, OG image, redirect, 410, llms.txt |
141
144
  | Accessibility: alt text, form labels, contrast, the audit codes and the manual checks | [accessibility.md](references/accessibility.md) | writing alt text; building a form; before reporting a site done | accessibility, WCAG, alt text, decorative, label, contrast, captions |
142
145
  | Page families from data | [programmatic-seo.md](references/programmatic-seo.md) | a keyword list or CSV should become pages | programmatic SEO, CSV, dataset, landing pages, glossary |
143
- | Publish, deploy, roll back, reconcile a re-import | [publishing.md](references/publishing.md) | a release; a code push that conflicts with CMS edits | publish, deploy, rollback, sync, webhook, schedule, automation |
146
+ | Publish, deploy, roll back, reconcile a re-import; add a section to a page or set its spacing | [publishing.md](references/publishing.md) | a release; a code push that conflicts with CMS edits; "add a section here", "more padding on this section"; a write that answers `held` or a publish that reads `partial` | publish, deploy, rollback, sync, webhook, schedule, automation, add section, insertSection, sectionStyle, spacing, padding, margin, held, partial |
144
147
  | Read-only launch review | [site-review.md](references/site-review.md) | "is this ready to launch?" without fixing | launch review, audit, is it ready, QA |
145
148
  | A/B tests on section variants | [content-experiments.md](references/content-experiments.md) | an experiment plan or readout | A/B test, experiment, variant test |
146
149
  | Headless Shopify storefront | [shopify-headless.md](references/shopify-headless.md) | Shopify owns commerce, BetterCMS the editorial | Shopify, Hydrogen, storefront, product |
@@ -182,9 +185,9 @@ Stop at the first step that is not clean, fix, then restart. A report that does
182
185
  3. `get_site_composition`: every page you touched is made of sections, with the intended
183
186
  components.
184
187
  4. `list_seo_issues`: no issue on a page you created or edited.
185
- 5. `get_next_steps`: `receipt.complete` is `true`. While it is `false`, take the first row of
186
- `receipt.gate.blocking`, run its `fix`, and read again; nothing else ends an imported site's
187
- setup. The gate, its bars and the exceptions it accepts are in
188
+ 5. `get_next_steps`: `receipt.complete` is `true`. While it is `false`, run
189
+ `receipt.gate.nextAction` and read again after each batch; nothing else ends an imported
190
+ site's setup, and an exception is recorded only for a reason that is true. The gate, its bars and the exceptions it accepts are in
188
191
  [finish-site.md](references/finish-site.md). Suggestions still in `missing` after that, and
189
192
  owner actions, go in your final message by name.
190
193
 
@@ -12,9 +12,10 @@ Read this when `package.json` lists `gsap`, `lenis`, `framer-motion` or `motion`
12
12
 
13
13
  ## 1. What a publish does to animated markup
14
14
 
15
- A text publish on a project in the text-node mode (off by default, for new projects too: the
16
- operator switches it on per project, or for the fleet) writes the change into the element's
17
- text nodes and keeps every element inside it. That is what the recipes below rely on:
15
+ The bound element picks the writer. One that holds text and nothing else is replaced whole, as
16
+ it always was. One that holds markup (inline elements, split spans, a `<br>`, or
17
+ `data-bcms-split`) is written into its text nodes, and every element inside it is kept. That is
18
+ what the recipes below rely on:
18
19
 
19
20
  | The page has | A publish does |
20
21
  |---|---|
@@ -22,6 +23,8 @@ text nodes and keeps every element inside it. That is what the recipes below rel
22
23
  | Text cut into `word` / `char` / `line` spans, same word count | writes each span in place |
23
24
  | The same, word count changed, spans made by a library at runtime | puts the plain text back; the library splits it on its next run |
24
25
  | The same, spans written by your own template | rebuilds them from their own pattern, a new one copied from the last |
26
+ | A split headline under an `aria-label` that says the same words (SplitText's own output) | one field on the headline: writes the label and the words |
27
+ | A counter with its number in `data-target`, `data-count` or a like attribute | writes the attribute; writes the number shown only where it showed the old target, never a script's starting `0` |
25
28
  | A copy of the value under `aria-hidden="true"` or `data-bcms-clone` | writes the copy too (coverage kind `duplicate-track`) |
26
29
  | The value inside `astro-island` props, `__NEXT_DATA__` or the app router's flight scripts | rewrites the serialized string (coverage kind `island-props`) |
27
30
 
@@ -32,8 +35,10 @@ It refuses, and says so on the publish, when the change would have to remove som
32
35
  `ISLAND_PROPS_UNMATCHED` (a hydrated island draws the text from props that do not hold it).
33
36
  Each is fixed in the template with the recipe for its library.
34
37
 
35
- On a project in the older mode, the default, a text publish replaces the element's content with
36
- plain text. There, bind only elements that hold text and nothing else.
38
+ The last two rows follow a value only where the element was written into its text nodes. An
39
+ operator can switch a project, or the fleet, to write every text value that way, so a plain-text
40
+ value's copies and props follow it too; and can switch the text-node writer off, after which a
41
+ publish replaces an element's content with plain text and refuses what that would break.
37
42
 
38
43
  ## 2. Two rules for every library
39
44
 
@@ -218,6 +223,17 @@ the heading. Text, several children, and in Next a component child are still wra
218
223
  `display:contents` span, and `as="div"` still gives a real wrapper. A site built with an older
219
224
  package version keeps its wrapper spans until it is rebuilt.
220
225
 
226
+ `<BcmsSections>` follows the same rule for a whole section: the block id, the style marker and
227
+ the block's style go on the section's own root, and in Astro the list is delimited by two HTML
228
+ comments, never an element. `main > section`, `:first-child` and a ScrollTrigger on `.hero` match
229
+ what they matched before. The one exception is a section that is not one element (several roots,
230
+ an island, in Next a component child with no function form): it gets a box of ours, a node
231
+ between `main` and the section. With no style on the block the box is `display:contents` and
232
+ draws nothing. Once the block has a style the box is `display:flow-root` and carries it: one
233
+ block box, so in a flex or grid parent its children are one item instead of several, and their
234
+ margins stop collapsing with the neighbours'. Give such a section one root element, or the
235
+ function form ([next.md](next.md)), when a selector or the layout depends on it.
236
+
221
237
  ## 11. Lottie, canvas, WebGL, video
222
238
 
223
239
  Pixels drawn by a script are not in the HTML, so nothing in them is a field by itself. What is
@@ -40,6 +40,7 @@ build. Pass `apiUrl` and `workspace` as options, or let the snapshot supply them
40
40
  | Import | Use |
41
41
  |---|---|
42
42
  | `@bettercms-ai/astro/components/BcmsBlocks.astro` | Server render of a block tree. Pass `components` or instances render nothing |
43
+ | `@bettercms-ai/astro/components/BcmsSections.astro` | The page's own sections as named slots, drawn in the CMS page's order (below) |
43
44
  | `@bettercms-ai/astro/components/BcmsLiveBlocks.astro` | Same tree as a hydrated island that follows the editor's draft bridge |
44
45
  | `@bettercms-ai/astro/components/BcmsField.astro` | `path`, `kind` (`richtext`/`image`), `as`; emits `data-bcms-field` + `data-bcms-kind` on its single child element, with no wrapper ([animation.md](animation.md) §10) |
45
46
  | `@bettercms-ai/astro/components/BcmsButton.astro` | A `button` field: `path`, `value`, `class`. One `<a>` with the label bound, the href in `data-bcms-props` and `data-variant` / `data-size`. Style only the looks the site has ([components.md](components.md) §5); renders nothing without a label |
@@ -51,6 +52,75 @@ build. Pass `apiUrl` and `workspace` as options, or let the snapshot supply them
51
52
  - Its CSS class names differ from `BcmsBlocks`. Swapping one for the other unstyles a page unless
52
53
  your stylesheet covers both spellings.
53
54
 
55
+ ## Sections in the CMS order
56
+
57
+ A page whose sections are written in its `.astro` file follows the CMS page's block list through
58
+ `<BcmsSections>`. `bettercms init` writes this wrap (`npx @bettercms-ai/convert --sections`);
59
+ write it by hand for a page the codemod refused ([componentize.md](componentize.md) §5):
60
+
61
+ ```astro
62
+ ---
63
+ import BcmsSections from "@bettercms-ai/astro/components/BcmsSections.astro";
64
+ ---
65
+ <Layout>
66
+ <BcmsSections page="home">
67
+ <Fragment slot="hero"><Hero title={title} /></Fragment>
68
+ <Fragment slot="code:sec1"><LogoStrip /></Fragment>
69
+ <Fragment slot="pricing">{plans.length > 0 && <Pricing plans={plans} />}</Fragment>
70
+ </BcmsSections>
71
+ </Layout>
72
+ ```
73
+
74
+ - `page` is the CMS page's slug. Each section is one `<Fragment slot="…">` around markup you do
75
+ not change. Anything with its own `slot="…"` (it targets the layout) stays outside.
76
+ - A slot is named by its placement's bind: the family of the section's first bound field
77
+ (`pricing-title` → `pricing`), which is the `bind` on the page's `component` block.
78
+ - A block bound to a slot renders it at the block's position. `data-bcms-block`, `data-bcms-style`
79
+ and the block's style go on the section's own root element; no element is added, so the stamp
80
+ never comes between a parent and a child a selector or an animation names
81
+ ([animation.md](animation.md) §10). A slot that renders several roots, or an island, is stamped
82
+ on a box: `display:contents` while its block has no style, one `display:flow-root` box once it
83
+ has. Give it one root element where the layout must not gain a box.
84
+ - A section added in the editor (a block with no bind) is drawn by `BcmsBlocks`, which
85
+ `BcmsSections` imports itself: nothing to pass, and it builds whether or not `bettercms()` is
86
+ registered on that build (a config that adds it only with credentials still draws the section).
87
+ - A slot no block names: `code:<bind>` always renders where it is written. A bare `<bind>` does
88
+ too until the list names one of the page's slots; from then on it is a removed section. Write
89
+ a slot bare only when the page already places that bind (`get_page`), otherwise `code:`.
90
+ - No snapshot (a local build, the API down): every slot renders in source order, untouched.
91
+
92
+ **A page that only draws a view.** Where the page file is a shell (`<HomeView locale="en" />`,
93
+ alone or inside its layout) the sections are written in the view, and `--sections` wraps them
94
+ there: it follows the import (relative, or a `paths` alias the repository's own tsconfig or
95
+ jsconfig declares, index files included) up to three components down, to the first file that
96
+ writes two or more sections or writes its run inside a layout. One view often serves several
97
+ pages (`/` and `/fr`), so the view cannot name the page. Each page hands it down, and the view
98
+ reads it:
99
+
100
+ ```astro
101
+ <!-- src/pages/fr/index.astro -->
102
+ <HomeView locale="fr" bcmsPage="fr" />
103
+
104
+ <!-- src/views/HomeView.astro: `bcmsPage?: string` joins its Props -->
105
+ <MainLayout>
106
+ <BcmsSections page={Astro.props.bcmsPage ?? ""}>
107
+ <Fragment slot="hero"><HeroSection title={copy.hero.title} /></Fragment>
108
+ …
109
+ </BcmsSections>
110
+ </MainLayout>
111
+ ```
112
+
113
+ Each page then follows its own CMS page's list through the one set of slots. A route that hands
114
+ the view no page (the CMS has none for it yet) renders the source order. A slot is bare only
115
+ when every page drawing the view places that bind. A dynamic route's view (`blog/[id].astro` →
116
+ `BlogPostView`) is wrapped the same way with `template`. Write it by hand in this shape when the
117
+ codemod refuses the view ([componentize.md](componentize.md) §5).
118
+ - The list is the published snapshot's, or the draft's when the Visual Editor frames a
119
+ server-rendered page (`prerender = false`). `blocks={…}` gives it yourself.
120
+
121
+ Check: build, then view source. `<!--bcms-sections:start page="home"-->` precedes the sections,
122
+ and each placed section's root carries `data-bcms-block` and `data-bcms-style`.
123
+
54
124
  ## Bindings
55
125
 
56
126
  A section root your own code renders spreads `sectionRootAttrs(block)` from `@bettercms-ai/astro`
@@ -72,5 +142,7 @@ helpers. Use them inside component instances so addresses resolve to the instanc
72
142
  | Resized images 404 on a non-production site | `mediaUrl` still the production default | Set `PUBLIC_BCMS_MEDIA_URL` to that rung's API origin |
73
143
  | Resized images still on the CDN after moving media | `mediaUrl` not changed | Set it to `mediaBaseUrl` from `get_project` |
74
144
  | Component instances empty | `components` prop missing | Pass definitions to `BcmsBlocks` |
145
+ | A code section vanished after a build | Its slot is bare and the page's block list does not place its bind | Name it `code:<bind>`, or place the bind on the page |
146
+ | Build fails on `bettercms:config` | A page imports `BcmsSearch` or the virtual module on a build where `bettercms()` is not registered | Register it unconditionally. `BcmsSections`, `BcmsBlocks`, `BcmsImage` and `BcmsForm` need no integration |
75
147
  | Draft link 500 | `BCMS_DRAFT_SECRET` unset | Set it on the server env, redeploy |
76
148
  | `ReferenceError: X is not defined` from a `[slug].astro` route | `getStaticPaths()` reads a `const` declared in the frontmatter; Astro runs it in its own scope, before the page, seeing only imports | Declare the client or reader call inside `getStaticPaths`, or import it from a module |
@@ -68,7 +68,19 @@ value over everything else inside it, so:
68
68
  `button` field rendered with `BcmsButton` ([components.md](components.md)).
69
69
  - **SVG text is never bound; the icon is.** No field is derived from the text inside an `<svg>`,
70
70
  and no binding there is measured. Each inline icon IS an editable `image` field (`config.media:
71
- "icon"`, an svg value): an editor swaps it. Never remove one to tidy a form; group it.
71
+ "icon"`, an svg value): an editor swaps it. Never remove one to tidy a form; group it. So is a
72
+ sprite icon (`<svg><use href="#id">`) whose `<symbol>` is in the same page, a wordmark set in
73
+ `<text>`, and an illustration up to 64 KB. A swap keeps the element's classes and size and
74
+ stays inline, so `currentColor` still applies; scripts and remote references are stripped.
75
+ - **Copy in an attribute is a field of its own.** Every release derives a text field for an
76
+ `aria-label`, a `title`, a `placeholder` outside a form, and the number a count-up script
77
+ counts to (`data-target`, `data-count`, `data-value` and their kin). Its `config.attr` names the
78
+ attribute; it is bound on `data-bcms-props` as `<path>|text|<attribute>` and published into that
79
+ attribute alone, never matched against the page's words. A label that only repeats the words
80
+ on its own element gets none. To declare one in code, write the same `data-bcms-props` entry.
81
+ - **Text beside an icon in a plain element** (`<span>Pages<svg/></span>`) is a field when it is
82
+ the element's one text node: the save writes that node and leaves the icon. Split over several
83
+ text nodes it stays `BARE_TEXT`, so wrap the label in its own `<span>` where you can.
72
84
  - **Icon-only links in the nav and footer** take their field name from what a screen reader
73
85
  reads: the anchor's `aria-label`, its `sr-only` text, a `title`, the SVG `<title>` or an `alt`
74
86
  (never `<style>` or `<script>` text). Give each one an `aria-label` or `sr-only` label, or every
@@ -107,7 +119,8 @@ picture a page shows: an `<img>` (its `src`, or where that is a placeholder a la
107
119
  `data-src`, then the largest `srcset` candidate), a whole `<picture>` as ONE field, a
108
120
  `<video poster>`, an inline or STYLESHEET `background-image` (Tailwind `bg-[url(...)]` included)
109
121
  and an inline `<svg>`. What it declines is listed with a reason in the binding report's
110
- `imageGaps` ([media-and-brand.md](media-and-brand.md) Where images hide).
122
+ `imageGaps`, and everything else it declines in `elementGaps`
123
+ ([media-and-brand.md](media-and-brand.md) Where images hide).
111
124
 
112
125
  Publishing an image keeps the page responsive. A library image re-points every `srcset`
113
126
  candidate and each `<source>` at the new picture at that candidate's own width and format, and
@@ -46,7 +46,10 @@ Never let a component contain itself, directly or through a child.
46
46
  once the field is an image). A nav, footer or
47
47
  modal group is `NOT_A_SECTION` with `detail: LAYOUT_CHROME`: chrome is the Layout on every
48
48
  site, never a page band. Author the real sections among those with `create_component` and
49
- `set_page_content` instead.
49
+ `set_page_content` instead. The other codes need no decision: `ALREADY_COMPONENTIZED` (a
50
+ placement exists), `EMPTY_GROUP` (no field holds a value), `NESTED_REPEATER` (a repeater three
51
+ deep; two levels become a nested `table` sub-field) and `PAGE_NOT_EMPTY` (the page holds
52
+ blocks this lane does not own, and it will not overwrite them).
50
53
  2. Show the user the sections, how many components it creates and which pages it rewrites.
51
54
  Confirm before writing. A group the user says is not a section: `componentize_sections
52
55
  { digest, sections: [groupKey], pageIds, decline: true }` (`decline: false` undoes it). The
@@ -56,9 +59,11 @@ Never let a component contain itself, directly or through a child.
56
59
  again and confirm again. Then run it for real.
57
60
  4. What it writes: each component as a DRAFT, and each page's draft blocks as an ordered list of
58
61
  instances. By default each instance carries `props.bind` and the words stay in the page's
59
- field group, so bindings and the coverage meter do not move. `copy: "instance"` moves the
60
- words onto each placement and marks the old fields `origin: "componentized"`; nothing is
61
- deleted. Running it twice is safe: an existing placement comes back as
62
+ field group, so bindings and the coverage meter do not move. `copy: "instance"` is the
63
+ page-builder model: each placement takes its group's CURRENT draft values into its own
64
+ `props.overrides` (a repeater becomes one `table` prop holding every row), records the page
65
+ path each came from in `props.source`, and drops `props.bind`. The old fields are kept and
66
+ marked `origin: "componentized"`; nothing is deleted. Running it twice is safe: an existing placement comes back as
62
67
  `ALREADY_COMPONENTIZED`.
63
68
  5. `publish_component` for each, then publish the pages. An unpublished component renders
64
69
  nothing on the live site.
@@ -67,7 +72,8 @@ Never let a component contain itself, directly or through a child.
67
72
  commit to). In a checkout, run the codemod with `--componentize` and a `--receipt` file, commit
68
73
  and push the files it wrote, and hand that receipt verbatim to `submit_componentize_receipt`. It records
69
74
  each extracted section as the source of the component with the same slug, so no
70
- `set_component_source` per component is needed. Deploy.
75
+ `set_component_source` per component is needed. Deploy. A page whose bands could not be
76
+ lifted is wrapped where it stands instead (§5).
71
77
  7. Output validation, from step 3 of §6.
72
78
 
73
79
  ## 3. Names and groups
@@ -75,7 +81,10 @@ Never let a component contain itself, directly or through a child.
75
81
  Name a family by what it is (`Hero`, `CTA`, `Testimonials`) and each variant by how it looks
76
82
  (`Hero / Split`, `CTA / Centered`). Never put a page name in a reusable definition; which pages
77
83
  use it is a usage question (§8). The componentize lane files a section under its page's slug
78
- (`home-hero`) but keeps the name the section's own (`Hero`). Keep related fields together in the
84
+ (`home-hero`) but keeps the name the section's own (`Hero`). Its components go into the Component
85
+ Group you pass as `group` (a folder of the dashboard's Components tab), else into one named for
86
+ the section family: nav, header, footer or menu into Layout, an unnamed section into Sections,
87
+ any other into the family's own name (Hero, FAQ). Keep related fields together in the
79
88
  order an editor reads them:
80
89
 
81
90
  ```text
@@ -111,3 +120,61 @@ and after publish:
111
120
  - one chrome edit reaches every route.
112
121
 
113
122
  Then `get_binding_report` and `get_site_composition` (§9).
123
+
124
+ ## 5. The page wrap (`--sections`): sections that stay in the site's code
125
+
126
+ Lifting a band into a component (§2) needs an inline run and a plan. A page built from imported
127
+ section components with props has neither, and it does not need them: `--sections` wraps the
128
+ page's top-level sections in `<BcmsSections>`, one named slot each, so the site's own build draws
129
+ them in the CMS page's order, with each block's spacing and style, and draws a section added in
130
+ the editor between them. It changes no section's markup, props, classes or styles.
131
+
132
+ `bettercms init` runs it before the agent starts and commits it locally
133
+ (`chore(bettercms): render sections from BetterCMS`); the dispatch of step 6 runs it after
134
+ `--componentize`. Do not redo or revert that commit. Pushing it is the user's decision. To run
135
+ it yourself, in a clean checkout:
136
+
137
+ ```sh
138
+ npx @bettercms-ai/convert --sections --root . --dry-run # the plan, per page
139
+ npx @bettercms-ai/convert --sections --root .
140
+ npx @bettercms-ai/convert --verify-styles --root . # must report no style edits
141
+ ```
142
+
143
+ A page file that only draws a view component (`<HomeView locale="en" />`) is wrapped in the
144
+ view's file, where its sections are written; the dry run names that file per page, and the page
145
+ itself only gains the prop that tells the view which page it is ([astro.md](astro.md),
146
+ [next.md](next.md)). A lone component that is itself one section is wrapped in the page.
147
+
148
+ A page it leaves alone keeps rendering as before, and is listed with one of these:
149
+
150
+ | Reason | The page | What you do |
151
+ |---|---|---|
152
+ | `SECTION_RUN_NOT_FOUND` | Returns markup from several places, or has loose text between sections | Give it one root, wrap loose text in an element, run again; or write the wrap by hand |
153
+ | `SECTION_RUN_NOT_CONTIGUOUS` | A child with `slot="…"`, a `<script>` or a `<style>` sits between two sections | Move it above or below the sections, run again |
154
+ | `SECTIONS_IN_LAYOUT` | Gives its layout no children: the layout draws the sections | Move the sections into the page, or wrap them by hand in the layout for that one page |
155
+ | `SECTION_RUN_DYNAMIC` | A `[slug]` route no collection page has for its item template (a parameter above the last segment, or no collection at that path) | Nothing: its sections stay in code. A collection's own `[slug]` file is wrapped once, for the collection page, and every entry draws that list |
156
+ | `CLIENT_COMPONENT` | A `"use client"` page that exports more than the page | Move those exports to a server file and run it again. Any other client page is wrapped: it moves into `page.client.tsx` and its page file becomes the server parent that reads the order |
157
+ | `PAGE_PROPS_IN_USE` | Pages Router page on `getInitialProps`, a data function it only re-exports, or a rest parameter | Add `bcmsSections` to its props by hand ([next.md](next.md)) |
158
+ | `PAGE_UNKNOWN` | No CMS page for the route | Create and publish the page, run again |
159
+ | `DIALECT_UNSUPPORTED` | Markdown, MDX or HTML | Nothing: the body is one `document` field |
160
+ | `NAME_TAKEN` | Already uses the name `BcmsSections` for something else | Rename that, run again |
161
+ | `SDK_REQUIRED` | The repo's SDK predates the component | Add `--add-sdk`, install, run again |
162
+ | `VIEW_IMPORT_UNRESOLVED` | Draws one component, and its import names no file: an alias no tsconfig or jsconfig of the repository declares (one inherited through `extends` is not read) | Declare the alias in the config's own `compilerOptions.paths`, or import the view by a relative path; run again |
163
+ | `VIEW_DEPTH_EXCEEDED` | Its sections are more than three components below the page | Wrap by hand in the file that writes them, and pass the page down ([astro.md](astro.md), [next.md](next.md)) |
164
+ | `VIEW_SHARED_INCOMPATIBLE` | Draws a view another page draws too, and the CMS names the view's sections differently on the two (or one is a page and the other a collection's item template, in Astro) | Give each page a view of its own, or make the two CMS pages place the same binds, then run again |
165
+
166
+ The by-hand markup is in [astro.md](astro.md) and [next.md](next.md), and it follows the rules of
167
+ [animation.md](animation.md) §10: slot tags around sections you do not edit, no new element. On
168
+ a wrapped page two notes ask for a hand edit. `ROOT_IS_COMPONENT` (Next): the section moves and
169
+ takes its spacing on a box around it, which is one block box once styled; write its child as a
170
+ function and spread the root props on the component's root element to style the root itself.
171
+ `EXPRESSION`: a `.map()` or a conditional is ONE section; to move its rows separately, give each
172
+ row its own slot.
173
+
174
+ Slots are written `code:<bind>` unless the page already places that bind; a `code:` slot is
175
+ never removed by the list. After the page's placements exist, run `--sections` again (or
176
+ `bettercms init`): it writes those slots bare, and from then on removing the block removes the
177
+ section. Any other framework gets `FRAMEWORK_UNSUPPORTED` and no write.
178
+
179
+ Check: after the build, the route's row in `get_binding_report` reads `stamped` equal to `placed`
180
+ and `ordered: true`.
@@ -32,6 +32,13 @@ defaultValue? }`.
32
32
  - **A `button` prop is one unit.** Its value is `{ label, href, variant?, size?, arrow?, icon?,
33
33
  newTab? }` and its `target.path` is `props` on a `button` block. For a new component never
34
34
  declare a label prop plus a url prop: the two drift apart on the first edit.
35
+ - **A button the site's own script handles has no href.** Its value is `{ label, action: "code",
36
+ variant?, size?, arrow?, icon? }`: a `<button type="button">` that opens a dialog, accepts
37
+ cookies or starts a tour. The label and the look are edited; the click stays the site's.
38
+ `href`, `newTab` and `rel` beside `action: "code"` are refused, and a value with neither an
39
+ href nor the action is refused as before. Setup derives these as button fields on a new
40
+ import. A control of the page itself is never one: a menu or theme toggle, a carousel arrow, a
41
+ dialog's close, an accordion or tab trigger, a copy button, a pagination step.
35
42
  - `target.blockId` + `target.path` must point at a real block and property inside the
36
43
  component's own `blockJson`. Why: rendering copies each prop's value to that target; a target
37
44
  that does not exist renders the default and the edit goes nowhere.
@@ -192,16 +199,24 @@ component.
192
199
 
193
200
  ### When the site's own code renders the sections
194
201
 
195
- A code-built site ships only what its templates draw, whatever the page's `blockJson` says.
202
+ A code-built site ships only what its templates draw, whatever the page's `blockJson` says. The
203
+ setup gate judges it per route (`sections` and `style`, [finish-site.md](finish-site.md)), and
204
+ adding or spacing one section is the recipe in [publishing.md](publishing.md).
196
205
 
197
206
  - **Render the page's block list, never hard-coded bands.** A section added or moved in the
198
207
  editor reaches the live site only when the template loops over the page's blocks in order and
199
- each section root carries its block id as `data-bcms-block`. The codemod's `--componentize` pass
200
- generates `src/lib/bcms-sections.ts` for the loop and adds those attributes. Until it ships, the
208
+ each section root carries its block id as `data-bcms-block`. The codemod writes both: `--sections`
209
+ wraps a page as it is (below), and `--componentize` lifts its bands into components and
210
+ generates `src/lib/bcms-sections.ts` for the loop. Until one ships, the
201
211
  editor still adds, moves, swaps and removes sections (saved, and drawn on the canvas at once),
202
212
  tells the author that publishing leaves the live order to the site's code, and its "Let BetterCMS
203
213
  control section order" button commits that same codemod to the connected repository. `submit_conversion_receipt` keeps
204
214
  `complete: false` with one `SECTIONS_UNSTAMPED` row per route.
215
+ - **A page of imported section components is wrapped, not lifted.** `<BcmsSections>` takes each
216
+ top-level section as a named slot and draws the block list in order, stamping each section's
217
+ own root; a section added in the editor renders between them. `bettercms init` writes and
218
+ commits the wrap; what it refused, and the markup to write by hand, is in
219
+ [componentize.md](componentize.md) §5.
205
220
  - **The order is measured, not assumed.** A release reads the stamped section roots in document order:
206
221
  a build that draws them in the CMS order (`ordered: true` on the binding report's structure row)
207
222
  owns nothing, whatever sections it has no code for; one that draws them in its own order
@@ -219,15 +234,43 @@ A code-built site ships only what its templates draw, whatever the page's `block
219
234
  `BLOCK_ID_ONLY`, …) its block style by hand, confirm with `--verify-styles`, push and release.
220
235
  - **A `button` field renders with `BcmsButton`** (`path`, `value`): one `<a data-bcms-button>`
221
236
  carrying `data-variant` and `data-size`, so every look control (style, size, arrow, icon, new
222
- tab) applies on the canvas and on publish.
237
+ tab) applies on the canvas and on publish. A value with `action: "code"` renders
238
+ `<button type="button" data-bcms-button>` with the same children and no href: attach your
239
+ handler to it as before. A publish writes its label and look and never an href, a target or a
240
+ rel, and never turns it into an anchor.
223
241
  - **A button's looks are the ones the site already has.** Each release measures the built pages
224
- and keeps the project's Button family equal to the looks found there, named as the site names
225
- them (`btn-outline` is Outline). Style offers those looks and no others, and a publish paints a
226
- pick with the site's own classes. Never write CSS for a look so that a control "works": there
227
- is no fixed list of variants to implement. A `button` value's `variant` is one of those looks;
228
- a refused value comes back with the looks the project has. A look the site drops is flagged,
229
- never deleted. A new look is the owner's design decision: add its class to the site, then
230
- Rebuild.
242
+ and keeps the project's Button family equal to the looks found there. Style offers those looks
243
+ and no others, and a publish paints a pick with the site's own classes. Never write CSS for a
244
+ look so that a control "works": there is no fixed list of variants to implement. A `button`
245
+ value's `variant` is one of those looks; a refused value comes back with the looks the project
246
+ has. A look the site drops is flagged, never deleted. A new look is the owner's design
247
+ decision: add its class to the site, then Rebuild.
248
+ - **Each look is named for the role it plays on that site, and a site has only the roles it
249
+ uses.** One site's family is Primary, Secondary, Tertiary; another's is Primary, Secondary,
250
+ Ghost, Outline, Text Link. Never create, rename toward, or ask for a role the site does not
251
+ use, and never number one ("Outline 2"): the release gives the names, in this order.
252
+
253
+ | Role | The look that gets it |
254
+ |---|---|
255
+ | The site's own name | A look the site names keeps that name: `btn-primary` is Primary, `btn-ghost` is Ghost, `data-variant="link"` is Text Link. Its stored `variant` value stays the site's own. |
256
+ | Primary | Exactly one: the filled look used most (placements, then pages, then hero placements). On a site that fills nothing, its most used outline or ghost. |
257
+ | Brand | The most used look filled with a colour, when Primary is a neutral dark or light fill, or when the site names a less used look Primary. |
258
+ | Secondary | The look most often beside Primary in a button group; else the next most used look that is not a text link. |
259
+ | Tertiary | A third filled look. Further fills are Dark, Light, or the colour's name. |
260
+ | Outline | Bordered and not filled. |
261
+ | Ghost | No fill and no border, padded like a button or filled on hover. |
262
+ | Text Link | A plain link the site styles as a call to action. Menu and footer links and links in copy are not looks. |
263
+
264
+ What counts as a button is one rule for the family and for the fields: what the site names one
265
+ (`btn`, `button`, `cta`, `role="button"`), or what its stylesheet paints as one (a fill or a
266
+ border of its own). Two looks paint nothing at rest and still count: a ghost, by its own padded
267
+ box, and a text link drawn as a row that underlines. In a menu those two count only beside a
268
+ painted button.
269
+
270
+ A light and a dark rendition, a hover or disabled state, a size, and one paint spelled in two
271
+ sets of classes are all ONE look. Two looks left with one role are told apart in words from
272
+ their paint ("Outline on dark", "Pill Outline"). A role once given stays: a look added later
273
+ takes a role that is free and renames nothing, and a name a person gave is never changed back.
231
274
  Without the SDK, write that same anchor: `data-bcms-button="<path>"`, the label in its own
232
275
  `<span data-bcms-field="<path>.label">`, the href through `data-bcms-props`. The arrow is the
233
276
  `svg.bcms-btn-arrow` it draws; never leave a hand-written arrow `<svg>` beside it, or turning
@@ -179,7 +179,12 @@ fail, because the next redesign makes them lie.
179
179
 
180
180
  Short labels named for what the editor sees: Heading, Body, Primary button, Label, Link, Image,
181
181
  Alt text — or the name the site's code already uses (`cents` → Cents), never the copy itself
182
- (`.00`). Most fields need no help line from you (below). Group by what renders
182
+ (`.00`). An import names every field it derives the same way, by its section and its role
183
+ there: "Hero headline", "Hero intro", "Hero primary button", "Pricing plans", "Footer link 2
184
+ URL", "Header logo"; a list's columns by their role in the row ("Title", "Description", "Link
185
+ URL"). A role is numbered only where it repeats in one section, and labels are unique on a page.
186
+ Keys never change. Rename a label where the site's code has a better word: a name you give is
187
+ kept on every later release, and one the import gave follows the import. Most fields need no help line from you (below). Group by what renders
183
188
  together, not by the DOM or the source file: core content first, optional appearance second, technical fields hidden. Repeatable items need a meaningful preview label (the FAQ question, the
184
189
  button label), reorder, and stable item ids. An empty optional field removes its element without
185
190
  leaving a gap; a long value wraps, and clamps only where the design intends it.