@bettercms-ai/mcp 0.63.2 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +1 -0
  2. package/SKILL.md +1 -1
  3. package/dist/index.js +908 -487
  4. package/dist/index.js.map +1 -1
  5. package/dist/skills/bettercms/SKILL.md +175 -0
  6. package/dist/skills/bettercms/references/astro.md +67 -0
  7. package/dist/skills/bettercms/references/bindings.md +201 -0
  8. package/dist/skills/bettercms/references/componentize.md +106 -0
  9. package/dist/skills/bettercms/references/components.md +228 -0
  10. package/dist/skills/bettercms/references/content-experiments.md +62 -0
  11. package/dist/skills/bettercms/references/content-modeling.md +185 -0
  12. package/dist/skills/bettercms/references/editor-chrome.md +88 -0
  13. package/dist/skills/bettercms/references/external-content.md +78 -0
  14. package/dist/skills/bettercms/references/finish-site.md +123 -0
  15. package/dist/skills/bettercms/references/forms-and-interactions.md +203 -0
  16. package/dist/skills/bettercms/references/frameworks.md +149 -0
  17. package/dist/skills/bettercms/references/getting-started.md +106 -0
  18. package/dist/skills/bettercms/references/localization.md +81 -0
  19. package/dist/skills/bettercms/references/media-and-brand.md +142 -0
  20. package/dist/skills/bettercms/references/next.md +93 -0
  21. package/dist/skills/bettercms/references/other-frameworks.md +66 -0
  22. package/dist/skills/bettercms/references/portable-text.md +206 -0
  23. package/dist/skills/bettercms/references/programmatic-seo.md +98 -0
  24. package/dist/skills/bettercms/references/publishing.md +146 -0
  25. package/dist/skills/bettercms/references/seo.md +156 -0
  26. package/dist/skills/bettercms/references/shopify-headless.md +52 -0
  27. package/dist/skills/bettercms/references/site-blueprints.md +250 -0
  28. package/dist/skills/bettercms/references/site-review.md +73 -0
  29. package/dist/skills/bettercms/references/write-limits.md +84 -0
  30. package/package.json +2 -2
@@ -0,0 +1,228 @@
1
+ ---
2
+ title: "Components, props and variants"
3
+ description: "The prop contract, instances, sectionType families and variants, publishing codes, rendering components in a site, preview source and component groups."
4
+ ---
5
+
6
+ # Components, props and section variants
7
+
8
+ Doctrine (components vs collections, section anatomy, variants): §3 and §4 of
9
+ `bettercms://playbook/schema`; flows: `new_component` prompt, `componentize_site` prompt. This
10
+ reference is what the code does with what you write. Componentizing a derived site end to end:
11
+ [componentize.md](componentize.md).
12
+
13
+ ## 1. The prop contract
14
+
15
+ A component declares `props`. Each prop is `{ key, label, type, target: { blockId, path },
16
+ defaultValue? }`.
17
+
18
+ - `type` is one of exactly 11: `text`, `richtext`, `image`, `url`, `boolean`, `number`,
19
+ `select`, `group`, `table`, `slot`, `form`.
20
+ - `target.blockId` + `target.path` must point at a real block and property inside the
21
+ component's own `blockJson`. Why: rendering copies each prop's value to that target; a target
22
+ that does not exist renders the default and the edit goes nowhere.
23
+ - `update_component` replaces the whole `props` array. Read with `get_component`, change, write
24
+ all props back.
25
+ - `select` needs `config.options`; `slot` holds one nested component instance; `form` holds one
26
+ Form document.
27
+ - `slot` and `form` are reference-shaped: `target.path` is `props` (the whole envelope) on an
28
+ empty `component` or `form` block, and their values are `{ componentId, overrides? }` and
29
+ `{ formId }`. Neither may appear inside a `group` or `table` row.
30
+ - **A `form` prop stores an id, nothing more.** Modelling the questions again as props gives you
31
+ two answers to "what does this form ask?", and they diverge at the first edit on either side.
32
+ The save schema is `.strict()`, so posting the hydrated form object back is a 400, not a silent
33
+ strip — you find out at the write, not at the render.
34
+ - A draft form is a 403 at every public ingress, so it renders nothing on the live site. Publish
35
+ it before pointing a component at it.
36
+
37
+ ### Where a form prop actually renders
38
+
39
+ The prop binds which form. What the READER does with it depends on how the component was built,
40
+ and only the first row below changes the rendered fields:
41
+
42
+ | Component | Preview / Output | The form comes from | Does the prop change the fields? |
43
+ |---|---|---|---|
44
+ | Built from blocks (a `form` block in `blockJson`) | no Output — Output needs a recorded source file | the renderer's forms map | **yes**, on the live site and the page preview |
45
+ | A component that renders `<BcmsForm>` / `<BcmsBlocks forms>` | yes | a forms catalogue passed beside the blocks | yes, where the catalogue reaches it |
46
+ | A codemod section (`@bettercms-ai/convert`) | yes | static markup; convert stamps `data-bcms-form` onto the existing `<form>` | no — the fields are hand-written HTML |
47
+ | A page route that fetches `/forms/public/:id` itself | inert (page-kind Output never runs scripts) | its own fetch | no |
48
+
49
+ ## 2. Instances
50
+
51
+ A placed component is a block `{ type: "component", props: { componentId, overrides?, bind?,
52
+ source? } }`.
53
+
54
+ - `overrides` is keyed by prop `key`. Rendering walks the definition's props and takes an
55
+ override only for a declared key. An override under any other key is ignored.
56
+ - A richtext override is a Portable Text array ([portable-text.md](portable-text.md)).
57
+ - Inside an instance, edit addresses are `<instanceId>__overrides.<key>`.
58
+
59
+ ## 3. Sections, families and variants
60
+
61
+ - A page is made of sections. A band used on more than one page, or with layout variants, is a
62
+ component with a `sectionType`. A true one-off is a `section` block whose `props.children` hold
63
+ its blocks.
64
+ - Components that share a `sectionType` are one family: its variants.
65
+ - **Give every component a library `category` as well.** Why: `sectionType` groups a family, the
66
+ category is how the component library lists it — an uncategorised component is created fine and
67
+ is then hard to find in the picker, which is where a human looks for it.
68
+ - **Spell `sectionType` exactly the same on every variant**, same case, no spaces. Why: the
69
+ backend (including the key-drift check) groups families case-insensitively but the dashboard
70
+ picker only trims, so "Hero" and "hero" are one family to the backend and two in the picker.
71
+ - **Every variant in a family declares the same prop keys with the same types.** Why: a variant
72
+ swap changes only `componentId`; the overrides stay, and only keys the new variant declares
73
+ are rendered. A key the new variant lacks is dropped from view; a key with a different type
74
+ renders wrong.
75
+ - Before swapping, compare `get_component` props of both variants. If keys differ, tell the user
76
+ which values will not show.
77
+ - **A card's or badge's looks are a `variant` select prop, not a family.** A card that comes as
78
+ Default, Featured and Outline is ONE component with a `select` prop keyed `variant` whose options
79
+ are the looks, and its markup picks classes from that value. Why: an editor switches the look in
80
+ place and the content stays put, and the library holds one card, not three. Families of looks
81
+ (`variantOf`, a style select on every section that places them) are for buttons: a new button
82
+ look is `get_component`, then `create_component` with `variantOf` naming the base.
83
+ - Read `warnings` and act on them; they never block the write:
84
+ - `create_component` / `update_component`: `VARIANT_PROP_KEYS_MISMATCH` (a key no sibling
85
+ declares, or one siblings declare that this variant lacks) and `VARIANT_PROP_TYPE_MISMATCH`
86
+ (same key, different type).
87
+ - `update_page` with `blockJson` that swaps a component block: `COMPONENT_SWAP_DROPS_OVERRIDES`
88
+ (overrides the new component does not render; they stay on the block, so swapping back
89
+ restores them) and `COMPONENT_SWAP_PROP_TYPE_CHANGED`.
90
+
91
+ Pitfalls of a component library:
92
+
93
+ | Pitfall | Signal | Fix |
94
+ |---|---|---|
95
+ | A variant needs keys its siblings lack | `VARIANT_PROP_KEYS_MISMATCH` on every write to it | It is a different band: give it its own `sectionType` family |
96
+ | Near-identical components in separate families ("Hero", "Hero 2", "Home hero") | Several families whose `get_component` props match | One family, variants by layout; every extra family is one more choice in "Add a section" |
97
+ | A component nobody can insert | its id in `get_site_composition` `notInPicker` | Set its `sectionType` |
98
+ | Components nothing places | ids in `get_site_composition` `unused` | Place them or name them to the user; deleting a component is a dashboard action |
99
+ | A required prop left empty | `emptyRequiredProps` on a placement | Fill it; `required` is not enforced on write |
100
+ | Props labelled by their copy or a generic role ("Text") | `get_next_steps` lists `labels-are-copy` | Label and help-text each prop from the site's code (`update_component` `propLabel`, `propHelpText`) |
101
+ | Repeated cards as one prop per card | `plan1Title`, `plan2Title`… | One list prop whose rows are the cards |
102
+
103
+ ## 4. Publishing
104
+
105
+ Publish with `publish_component` or `publish_components` (batch). A refusal is a 409 with a code.
106
+ Handle by code, never by retrying the same call:
107
+
108
+ | Code | Meaning | What to do |
109
+ |---|---|---|
110
+ | `COMPONENT_BLUEPRINT_MATERIALIZATION_REQUIRED` | You tried to publish a platform built-in | Create a project-local component from it, publish that |
111
+ | `COMPONENT_PROJECT_SCOPED_RELEASE_REQUIRED` | The component is workspace-global | Create a project-local variant, publish that |
112
+ | `COMPONENT_IMPLEMENTATION_NOT_READY` | A REPO-managed component lacks exact evidence and human approval, or a variant-group member's family contract is unpublished. Never a `create_component` component outside a variant group | Report `readiness`; the owner resolves it in the dashboard |
113
+ | `COMPONENT_ORCHESTRATION_V2_REQUIRED` | The project runs implementation in Agent Dock | Continue there; stop here |
114
+ | `COMPONENT_PUBLISH_CONFLICT` | The component or its evidence changed mid-publish | Re-read with `get_component`, then publish once more |
115
+ | `COMPONENT_PROJECT_CONTEXT_MISMATCH` | The component's `projectId` differs from the request's project scope | Call with the component's own project |
116
+
117
+ ### Site chrome (navigation and footer)
118
+
119
+ A new project's navigation and footer are draft components placed in a draft Global Layout.
120
+ Nothing of the chrome is live until all three are published, in this order:
121
+
122
+ 1. `publish_component` for navigation-default and footer-default. Each can be published while the
123
+ other is still draft; the publish is refused only for problems it would itself create.
124
+ 2. `publish_layout` with the revision from `get_layout`.
125
+ 3. `get_layout` with copy "published" to confirm.
126
+
127
+ The navigation's logo is optional. When it is empty, the nav shows the brand mark
128
+ (`set_brand_assets`) or the site name. Never generate or add a logo the site does not have. A
129
+ page that renders its own header or footer does not get the Layout's copy on top of it.
130
+
131
+ ## 5. Rendering components in your site
132
+
133
+ - **Next**: `readComponents()` from `@bettercms-ai/next` reads definitions from the
134
+ `bcms-content.json` snapshot. Pass them to `<BcmsBlocks components={…}>`; each `component`
135
+ block is resolved by applying its overrides to the definition.
136
+ - **Astro**: `BcmsBlocks.astro` takes the same `components` prop. Without it every instance
137
+ renders nothing. Get definitions from `readComponents()` of `@bettercms-ai/next` or the
138
+ delivery components endpoint.
139
+ - **SDK only**: `listComponents()` on the read client.
140
+
141
+ Rule: if an instance renders empty, check that definitions were passed before debugging the
142
+ component.
143
+
144
+ ## 6. Preview source
145
+
146
+ - `set_component_source` records which file in the repo implements a component;
147
+ `clear_component_source` removes it. Previews and validation read this mapping.
148
+ - **Call it as soon as you write the file, not at the end of the job.** Why: until the mapping
149
+ exists a preview refuses with `COMPONENT_SOURCE_NOT_RECORDED`, and that refusal arrives later,
150
+ in the dashboard, to whoever opened the component — not to you while you still have the path in
151
+ hand.
152
+ - `@bettercms-ai/preview-runtime` builds component previews for Next and Astro apps and validates
153
+ components in CI. `@bettercms-ai/component-output` verifies preview sessions and runs the
154
+ preview bridge in your app.
155
+
156
+ ### Output validation
157
+
158
+ A component's Output (its live preview) shows only after a validation run renders it. The run
159
+ renders the component from its recorded file, or cuts it out of the deployed page by its
160
+ `data-bcms-block` attribute. A component with neither has nothing to render, so its validation
161
+ can only fail.
162
+
163
+ Do these steps in order, every time you create or componentize a component:
164
+
165
+ 1. Make it renderable. Either call `set_component_source` with the file that renders it, or, after
166
+ `componentize_sections`, put each `blockId` from the receipt's `stamps` on its section root as
167
+ `data-bcms-block` (or run `npx @bettercms-ai/convert --componentize`, then
168
+ `submit_componentize_receipt`).
169
+ 2. Deploy, so the build that validation renders contains that markup.
170
+ 3. `get_component_readiness`. If `validation.gap` is set, do what `validation.fix` says first.
171
+ 4. `declare_component_route` only when the app serves previews somewhere other than the default route.
172
+ 5. `request_component_validation`. It refuses with 422 and the fix when nothing renders the
173
+ component, and opens no request.
174
+ 6. In the app's repository, run
175
+ `npx @bettercms-ai/preview-runtime validate --local --request <requestId>` with `BCMS_API_URL`,
176
+ `BCMS_API_KEY` (a project key with artifact write), `BCMS_PROJECT_ID` and `BCMS_PREVIEW_ORIGIN`
177
+ set in the environment. Never put a key in a tool call.
178
+ 7. `get_component_readiness` again. `outputReady` means the Output renders.
179
+
180
+ Rules:
181
+
182
+ - Your evidence is self-attested. Publishing still needs the owner's Visual Approval in the
183
+ dashboard. No tool gives it: never say you approved a component.
184
+ - CI validation (GitHub Actions) runs in the owner's repository at the owner's cost. The tool
185
+ refuses it with "VALIDATION_PROVIDER_DASHBOARD_ONLY". Ask the owner to start it from the dashboard.
186
+ - A validation failure naming "COMPONENT_SOURCE_NOT_RECORDED" means step 1 or 2 was skipped. It is
187
+ not a bug in the component.
188
+
189
+ ## 7. The values popover (dashboard)
190
+
191
+ Where you edit decides what you change:
192
+
193
+ - On a **placed instance** (a page body or rich-text field): the edit writes that instance's
194
+ override. Only this placement changes.
195
+ - On the **component's own canvas**: the edit writes the default of the variant being edited
196
+ (Base, or the open variant). Every instance without an override changes.
197
+
198
+ Rule: when the user says "change it on this page", edit the instance; "change it everywhere",
199
+ edit the default. Say which one you did.
200
+
201
+ ## 8. Groups
202
+
203
+ Components are organised into `component_groups` (the folders of the Components tab). A component
204
+ `componentize_sections` creates belongs to ONE page, so it is filed by that page, not by family.
205
+
206
+ - `componentize_sections` files every component it creates: under `group` when you pass one
207
+ (found or created by name); otherwise an MCP or release run files it under its page's folder,
208
+ `Sections/<Page title>` (`Sections/<Page title>/<pattern>` for a template), and a dashboard or
209
+ studio run leaves it unfiled. The receipt's `components.groups` lists the Groups it filed into.
210
+ A reused component keeps its Group.
211
+ - Sections a release imports are filed the same way, after the release.
212
+ - `componentize_sections` reuses a component by identity and slug, never by family, so a
213
+ hand-built `Hero / Split` is not picked for a derived hero band. Run `dryRun: true` and read
214
+ `components.wouldDuplicate`: each id is an existing same-family component the run would sit a
215
+ new one beside. Decide with the user before applying.
216
+ - Components componentized from the dashboard are not filed automatically.
217
+ - File one yourself with `create_component` or `update_component` `group` (by name).
218
+
219
+ Which pages use a component is computed on request by searching page and entry JSON; it is not a
220
+ stored list.
221
+
222
+ ## 9. Verify
223
+
224
+ 1. `get_component` on each component you changed: props complete, `sectionType` spelled like its
225
+ siblings.
226
+ 2. `get_site_composition`: every page is sections; every component you meant to publish is
227
+ published.
228
+ 3. `get_binding_report` on a page that uses the component: instance values bind.
@@ -0,0 +1,62 @@
1
+ ---
2
+ title: "Content experiments"
3
+ description: "A/B tests on section variants: hypothesis, design, readout and pitfalls."
4
+ ---
5
+
6
+ # Content experiments on section variants
7
+
8
+ **Outcome.** Editors manage approved variant content, the runtime serves a reproducible
9
+ experiment, and measurement answers the question that was asked. A component variant is a
10
+ presentation option. It is not an A/B test until something assigns, exposes and measures it.
11
+
12
+ ## Decide whether an experiment is viable
13
+
14
+ Read the connected project (`get_project`) and the site's analytics and consent setup. Write
15
+ down one hypothesis, the primary metric, guardrail metrics, the audience, the unit of assignment
16
+ (user, session or account), the baseline, the planned duration and the stop rules.
17
+
18
+ Check the MCP tool list for experiment assignment, exposure events or reporting. When none is
19
+ there, assignment and measurement live in a dedicated code or analytics service; BetterCMS owns
20
+ the variant definitions and the editorial values only. Say that to the user rather than implying
21
+ the dashboard runs the test.
22
+
23
+ Do not start a test when traffic is too low for a useful result, the metric cannot be observed,
24
+ or a legal or consent dependency is unresolved. A plain launch or a qualitative study is the
25
+ better answer there.
26
+
27
+ ## Model and serve variants
28
+
29
+ - **Treatments are variants of one family.** Components that share a `sectionType` are one
30
+ family, and every variant declares the same prop keys with the same types. Why: a swap changes
31
+ only `componentId`, so a key the other variant lacks is silently not rendered. Read both with
32
+ `get_component` before the test starts; detail in [components.md](components.md) §3.
33
+ - **Keep a stable control.** Freeze the control's copy and tokens for the run, so an unrelated
34
+ edit does not change the test mid-flight.
35
+ - **Instance override versus default.** An edit on a placed instance writes that placement's
36
+ override; an edit on the component canvas writes the variant's default and reaches every
37
+ instance without an override. Decide which one the treatment is before anyone edits.
38
+ - **A swap warns, it does not block.** `update_page` answers a swap that would hide overrides
39
+ with `COMPONENT_SWAP_DROPS_OVERRIDES` or `COMPONENT_SWAP_PROP_TYPE_CHANGED`. Read the warnings
40
+ before calling a treatment ready.
41
+ - **Publish both.** A draft component renders nothing on the live site, and publishing copies
42
+ the draft to live; an unpublished treatment is an empty band, not a variant.
43
+
44
+ Preview every variant at desktop and mobile widths in its real route. Assignment must be
45
+ sticky for the chosen unit; avoid sending both variants to one person through cache keys,
46
+ server/client hydration or locale routing. Emit an exposure only when the variant was shown.
47
+ Follow the site's consent settings for analytics and personalization. Keep accessible names,
48
+ forms, pricing facts and critical navigation equivalent unless the experiment tests them on
49
+ purpose and the risk is reviewed. Crawlers should see a stable canonical route, never cloaked
50
+ content.
51
+
52
+ ## Measure and finish
53
+
54
+ Fix the analysis window and the decision rule before reading results. Check sample imbalance,
55
+ missing exposure events, bot and internal traffic, cross-device leakage and guardrail
56
+ regressions before interpreting any uplift. Report the observed estimate with its uncertainty,
57
+ never a guaranteed improvement.
58
+
59
+ A winner is promoted by an explicit change: make it the default, or set the instances, then
60
+ publish and verify through the normal release path ([publishing.md](publishing.md)). Archive the
61
+ hypothesis, the variants, the measurement definition, the result and the rollback point. Keep
62
+ the losing variant while it is needed for audit or rollback.
@@ -0,0 +1,185 @@
1
+ ---
2
+ title: "Structure and content modeling"
3
+ description: "Pages vs collections, the four folder layers, field decisions and naming, and changing a schema safely, including retiring a field in stages."
4
+ ---
5
+
6
+ # Structure and content modeling
7
+
8
+ **Outcome.** A content tree an editor understands on day one, durable relationships between
9
+ records, no accidental URL change, and model changes that every consuming site builds against.
10
+
11
+ Doctrine: §1–§5 and §14 of `bettercms://playbook/schema` (schema shape and sections) and
12
+ `bettercms://playbook/structure` (sidebar); flow: `propose_schema` prompt; rule ids
13
+ `CM-10`…`CM-14`, `CM-40`…`CM-44`. What the server does and does not check on a write:
14
+ [write-limits.md](write-limits.md). Planning a whole site by business type:
15
+ [site-blueprints.md](site-blueprints.md).
16
+
17
+ ## 1. Read the site before modeling
18
+
19
+ Inventory route files and rendered routes, source data schemas, repeated records, page templates,
20
+ navigation, taxonomies, authors, tags, media references, SEO, forms, locales and publishing
21
+ states. Compare with the project's pages, collections and entries (`list_pages`,
22
+ `list_content_models`, `get_content_structure`). Note where a value is shared, where it is an
23
+ instance override, and where one entry owns it outright.
24
+
25
+ ## 2. Pages, collections and entries
26
+
27
+ A page is `singleton` (one page and its own content: home, about, a legal page) or `dynamic`
28
+ (one template page whose collection entries each render at the page's path plus the entry slug).
29
+ Choose by repetition: a shape that repeats per item is a collection; a one-off is a singleton,
30
+ even when it lists items, because the list page reads the collection. A dynamic page is its
31
+ collection: the model and its entries both point at the page (`content_models.page_id`,
32
+ `content_entries.page_id`), so never create a separate page per entry.
33
+
34
+ | Source pattern | BetterCMS destination |
35
+ |---|---|
36
+ | Home, about, contact, legal, editorial landing page | Singleton page with its own content |
37
+ | Blog / case-study / product list URL | A list page plus a separate collection |
38
+ | Repeated records sharing one template | One collection and one dynamic detail page |
39
+ | 404, search, gated page | A page that keeps its access and noindex behaviour |
40
+ | Author, category, tag, company reused by entries | A shared collection, referenced from the entries |
41
+ | Data from an external runtime API | Keep the adapter and its owner; never fabricate entries |
42
+ | Sitewide nav, footer, promo bar | Global Layout, never copied page fields |
43
+
44
+ ## 3. Four structure layers, four meanings
45
+
46
+ Name the layer every time you say "folder". The answer to "where does X live" differs per layer.
47
+
48
+ 1. **Content sidebar folders** organise the dashboard only: nothing here reads or writes a URL,
49
+ slug, schema, content or publish state. Read with `get_content_structure`, get a proposal from
50
+ `suggest_content_structure`, apply it whole with `set_content_structure` once the user agrees,
51
+ or change one thing with `create_folder`, `rename_folder`, `move_to_folder` or `delete_folder`
52
+ (deleting a folder moves its contents up). `set_content_structure` replaces the whole document:
53
+ send the `version` you read; a 412 `VERSION_CONFLICT` means someone changed it, so read and
54
+ re-apply. It refuses duplicate folder ids, a parent not in the doc, a cycle, and nesting deeper
55
+ than 4. The standard pins the home page and each list page at the top, gives each content
56
+ family one folder named for its detail type (the list page and its collection are two items),
57
+ keeps other standalone pages in Other pages, taxonomies used by two or more families in
58
+ Shared, and Settings last. Components are not sidebar items. Whether folders show depends on a
59
+ dashboard feature flag, so an empty sidebar is not proof the structure is missing.
60
+ 2. **URL page folders** are part of a page's path unless set not to be included in it. Renaming
61
+ or moving one 301s every affected URL and queues a rebuild: a routing change that needs the
62
+ user's review. They are set in the dashboard, not through the sidebar tools.
63
+ 3. **Component groups** (`component_groups`) organise definitions by family or role
64
+ (Navigation, Hero, CTA, Forms), never by the page that uses them ([components.md](components.md) §8).
65
+ 4. **Media library folders** organise assets (`folderId` on `create_media_upload`) and never
66
+ rewrite a stored content URL.
67
+
68
+ What reads published structure: the sitemap (skips noindex pages), robots.txt (points at the
69
+ sitemap), RSS (dated by `publishedAt`), `llms.txt` (one link per published item).
70
+
71
+ ## 4. Field decisions
72
+
73
+ - **Model each visual band as a `group` field (or a repeater), or a component instance.** The
74
+ schema's shape decides whether the site can ever be componentized. The componentize lane finds
75
+ a section in a `group`, a repeater, or two or more top-level keys sharing a prefix before `-` or
76
+ `_` (`"hero_title"` + `"hero_subtitle"` → `hero`). A key with NO shared prefix (`title`,
77
+ `intro`) is a lone scalar, refused `NOT_A_SECTION`. Keep ONE separator per band: a `-` family
78
+ and a `_` family never share a component. A `group` is the robust choice, and child keys inside
79
+ it follow the flat-namespace rule (`"hero_title"`).
80
+ - A collection for an entity with its own lifecycle and relationships; a nested group for values
81
+ one page or section owns.
82
+ - A reference for reusable entities and internal links where the model supports it; an external
83
+ URL for links that leave the site.
84
+ - A repeater for small, same-shaped items (cards, FAQ rows, links, social accounts); a nested
85
+ component or slot when an item has its own layout or variants.
86
+ - Never model SEO columns ([seo.md](seo.md)), Form questions (they live on the Form document) or
87
+ Global Layout values as content fields.
88
+ - Keep source ids, slugs, locale, status and previous URLs as migration metadata, stable across
89
+ re-imports.
90
+ - **Prefer a `select` over a `boolean` for a state that may grow** (`draft | review | live`, not
91
+ `isLive`). Why: no tool retypes a field, so a third state later costs a new field and a value
92
+ migration (§6).
93
+ - Choose field types before the first write. A `link` accepts any scheme-less href (`/about`,
94
+ `about.html`, `#section`) and `http(s):`, `mailto:`, `tel:`; every other scheme is refused.
95
+ Choose the type from what the rendered value is, not habit ([portable-text.md](portable-text.md)).
96
+
97
+ ## 5. Editor labels and order
98
+
99
+ **Name for meaning, not look.** Before writing a key or label, ask: would the name survive a
100
+ redesign? `ctaText`, `authorBio` and `features` pass; `blueButton`, `leftColumn` and `bigHeading`
101
+ fail, because the next redesign makes them lie.
102
+
103
+ Short labels named for what the editor sees: Heading, Body, Primary button, Label, Link, Image,
104
+ Alt text — or the name the site's code already uses (`cents` → Cents), never the copy itself
105
+ (`.00`). Every field gets help text showing its shape (`e.g. "49"`). Group by what renders
106
+ together, not by the DOM or the source file: core content first, optional appearance second, technical fields hidden. Help text states an effect ("Shown below the
107
+ heading on this page"). Repeatable items need a meaningful preview label (the FAQ question, the
108
+ button label), reorder, and stable item ids. An empty optional field removes its element without
109
+ leaving a gap; a long value wraps, and clamps only where the design intends it.
110
+
111
+ ## 6. Changing a schema safely
112
+
113
+ A schema write alone is not a release. Before one, read the model with `get_content_model`, a few
114
+ sample entries, the component definitions that touch it and the frontend readers, and write a
115
+ before/after field map: key, type, label, required, default, validation, editor group, and what
116
+ happens to existing values.
117
+
118
+ 1. **Guard concurrent edits.** Pass the model's `optimisticVersion` as `ifMatch` to
119
+ `update_content_model`. A 409 means it changed since you read it: re-read and re-plan, never
120
+ resend. Omitted, the last write wins, except on the editor-chrome args, which refuse without
121
+ it (428).
122
+ 2. **Know what the MCP can do.** `add_field` (a model's `modelId` or a page's `pageId`) appends
123
+ and never overwrites or retypes. `update_field` changes ONE existing field, addressed by a dot
124
+ path through its groups and repeaters (`hero.title`): its label, help text, `required`,
125
+ validation `config` (merged by key; null deletes one), `showIf`, `ui` or placement, written at
126
+ the version it read. A retype needs `allowRetype: true`, because stored values that do not fit
127
+ the new type are lost; a key rename needs the migration in step 3. `update_content_model` edits
128
+ the model's name, slug, URL pattern and fieldsets, and removes model fields only with
129
+ `removeFields` (step 3). The home page cannot be deleted, and changing a local type file is not
130
+ a schema change.
131
+ 3. **Retire or rename a field in stages, never in one write.** Renaming a key re-addresses its
132
+ content: `update_field` with a new `key` is refused with 409 and the count of entries holding
133
+ the old key, unless `allowFieldRemoval: true` discards their values. So a rename is a new field
134
+ plus a migration:
135
+ 1. Relabel the old field "Old title (Deprecated)" and set help text naming its replacement
136
+ (`update_field` `label` and `helpText`).
137
+ 2. Move it into a `collapsed: true` fieldset so editors stop reaching for it
138
+ ([editor-chrome.md](editor-chrome.md) §2).
139
+ 3. Add the replacement (`add_field`, with `modelId` or `pageId`) with the final type and name.
140
+ 4. Migrate values in code, draft first, and have the frontend read the new key.
141
+ 5. Remove the old one. Page fields: `remove_page_fields` (dry run by default) returns
142
+ `keysWithContent` and writes nothing; read it, then run it with `dryRun: false` and
143
+ `discardContent: true` only for keys you migrated. Model fields: `update_content_model` with
144
+ `removeFields: ["oldKey"]` and `allowFieldRemoval: true`, sent with the `ifMatch` of your
145
+ read. It has NO dry run and discards that key's value in every entry, so count the entries
146
+ holding it first (`list_content_entries` with `modelId`: `meta.total` is the count, `page`
147
+ walks past the 200-row `limit`). Top-level keys only; an unknown key refuses the
148
+ whole call; a page's own fields go through `remove_page_fields`. Both discard data: removal
149
+ is the last step, with the user's yes.
150
+ Why: every stage before 5 is reversible, and a site built against the old key keeps rendering
151
+ until its next release reads the new one.
152
+ 4. **Put numeric limits in `config` as numbers** (`"maxLength": 60`), with `update_field` on a
153
+ field that exists. A numeric string is converted on save; an impossible value is refused with
154
+ `FIELD_CONFIG_INVALID`.
155
+ 5. **Check existing records before tightening validation.** Backfill or give a real default
156
+ before making a field required; keep a source-to-target map and a snapshot for any high-volume
157
+ transform.
158
+ 6. **Validate on a draft.** Entries written through the MCP pass one write path (request schema,
159
+ normalisation, per-field checks, uniqueness), and a write failing any step stores nothing.
160
+ Publishing copies the draft to live, so a draft that passes is the value that ships.
161
+ 7. **Change a component family together** (same prop keys and types on every variant;
162
+ [components.md](components.md) §1 and §3).
163
+ 8. **Regenerate types** with `bettercms-codegen` in every consuming site after any model or field
164
+ change, commit the output if the repo commits it, and run that repo's build and typecheck with
165
+ the script it already has. Templates typed against an old schema compile and read `undefined`.
166
+ 9. **Verify** the editor's grouping and controls, one draft edit per changed field class, the
167
+ preview binding, the static or headless reading path, and the public output once a release is
168
+ authorized.
169
+
170
+ Content models, frontend types, component definitions and live content have separate lifecycles;
171
+ there is no single "deploy schema" action. Production publication stays behind the user's
172
+ authorization and the component readiness and Visual Approval gates.
173
+
174
+ **Evidence to return:** model and field ids, the version you wrote against, migrated record
175
+ count, codegen and build result, one editor check and one rendered-page check, warnings, and any
176
+ change the platform could not make (an open dependency, never a success).
177
+
178
+ ## Acceptance
179
+
180
+ - Every imported route and collection has one clear home; one dynamic template per collection.
181
+ - Sidebar moves keep every public route and record id; URL changes carry explicit redirects
182
+ (`create_redirect`).
183
+ - Repeated data is modelled once, with references or reusable components.
184
+ - Editors find and understand the fields without reading source code.
185
+ - Model changes compile in each consuming site and keep the published output unchanged.
@@ -0,0 +1,88 @@
1
+ ---
2
+ title: "Editor chrome (dock layout)"
3
+ description: "Group, row, tab, icon, segmented control, list item names and showIf order for the dashboard dock; the ifMatch rule and the two chrome refusals."
4
+ ---
5
+
6
+ # Editor chrome
7
+
8
+ **Outcome.** Every dock screen an editor opens fits the click budget, every list names its items,
9
+ and a condition is answered before the field it reveals. The delivered content is byte-identical.
10
+
11
+ Doctrine: §14 of `bettercms://playbook/schema`; flow: `field-ui` prompt. Editor chrome (field
12
+ `ui`, model `fieldsets`, prop `ui`) changes how the dashboard dock draws a field. It never moves a
13
+ delivered value. Each recipe is one write, then a read-back.
14
+
15
+ ## 1. Read, then write with `ifMatch`
16
+
17
+ Take `optimisticVersion` from `get_content_model` or `get_component`. For a page's own fields call
18
+ `get_page`, send `pageId`, and use `backingModel.optimisticVersion`. The path args (`fieldUi`,
19
+ `fieldFieldset`, `fieldOrder`, `propUi`, `propGroup`) and the layout keys (`kind`, `tab`, `icon`,
20
+ `itemLabel`, `ui.group`) get 428 `precondition-required` without it. A 409 carrying
21
+ `currentVersion` means someone wrote since your read: read again and re-apply the intent, never
22
+ the old body. Why: path args are merged onto the stored row, and the version is what stops them landing on a
23
+ schema you never saw.
24
+
25
+ ## 2. Group, row, tab
26
+
27
+ Give a fieldset a `kind`:
28
+
29
+ | `kind` | Draws | Limits |
30
+ |---|---|---|
31
+ | `group` | Fields folded under one header (`collapsed: true` starts it shut) | 12 rows inside one group |
32
+ | `row` | Short fields on one line | at most 3 fields |
33
+ | `tab` | A tab strip | at most 3 tabs, names of 20 characters or fewer, top-level fields only, tabs never nest |
34
+
35
+ A group joins a tab with `tab: "<tab id>"`. A fieldset with no `kind` stays flat. Props have no
36
+ fieldsets: the first prop of a group sends `ui.group: { id, label, kind }` and each later one
37
+ `{ id }`; `kind` is `group` or `row`.
38
+
39
+ Stay inside the budget `get_next_steps` measures per screen: 8 ungrouped rows, 12 rows in a group,
40
+ 3 levels, any value within 2 activations. Why: past it the rule `dock-budget` keeps listing the
41
+ screen. A closed group in a second tab already costs 2 activations, so leave groups in later tabs
42
+ open.
43
+
44
+ ## 3. Icons
45
+
46
+ `ui.icon` belongs on nesting fields, fieldsets and a prop's `ui.group`, never on a leaf. Take names
47
+ from the dock icon list the MCP serves as a resource (`resources/list`); any other string is a
48
+ 400. Set a sidebar item's icon with `set_icon`.
49
+
50
+ ## 4. Segmented, dropdown or slider
51
+
52
+ - `ui.control: "segmented"` on a single-choice select with 2–4 short options: one click replaces
53
+ two. A multi-select or 5+ options is refused; keep the default dropdown there.
54
+ - `"slider"` needs a `number` with numeric `config.min` below `config.max` and at most 1000 steps.
55
+
56
+ ## 5. Name list items
57
+
58
+ On each repeater or repeatable zone set `ui.itemLabel` (1–24 characters: "Slide", "Person") and
59
+ `ui.preview.title` naming a child key. Without either, every row reads "Item N" and
60
+ `get_next_steps` keeps listing `dock-repeater-unnamed`.
61
+
62
+ ## 6. Conditional fields
63
+
64
+ A field's `showIf` reads another field's value. Place the field it reads ABOVE it, so the author
65
+ answers the condition first; a `showIf` that reads a later field is listed as `dock-showif-order`.
66
+ Reorder with `fieldOrder` on `update_content_model`.
67
+
68
+ ## 7. Refusals
69
+
70
+ | Code | Meaning | Do |
71
+ |---|---|---|
72
+ | 428 `precondition-required` | Layout keys without `ifMatch` | Read, send the version |
73
+ | 409 **ui_human_owned** | A person arranged those paths (the body names them) | Leave them out; `override: true` only when the user asked you to replace their layout |
74
+ | 409 **ui_studio_blind** | The Studio agent does not change editor chrome | Echo the chrome exactly as read, or omit it (the stored chrome is kept) |
75
+ | 400 with a code | A vocabulary rule (**tab_max**, **row_member_count**, **control_segmented**…) | Fix what the message names; §14 lists every code |
76
+
77
+ To undo an agent change, write back the values from your read before the change, with `ifMatch`.
78
+
79
+ ## 8. Chrome once, in the Layout
80
+
81
+ Navigation, footer and dialogs live in the Global Layout (`get_layout`, `update_layout`). When
82
+ `get_next_steps` reports `chrome-duplicated`, declare the element in the Layout and push; the page
83
+ copies are stripped for you. Never delete them by hand, and never give them dock chrome.
84
+
85
+ ## Acceptance
86
+
87
+ Read back with `get_content_model`, `get_page` or `get_component`. Done means `get_next_steps`
88
+ lists none of `dock-budget`, `dock-repeater-unnamed`, `dock-showif-order`.
@@ -0,0 +1,78 @@
1
+ ---
2
+ title: "Content in another service"
3
+ description: "Move content a site loads from another CMS or API (Contentful, Sanity, Strapi, Cosmic…) into BetterCMS by rewriting the client module, with the env names the platform supplies."
4
+ ---
5
+
6
+ # Content that lives in another service
7
+
8
+ Read when: the site loads its content from another CMS or content API. Signals:
9
+
10
+ - A dependency in `package.json`: `cosmicjs` / `@cosmicjs/sdk`, `contentful`, `@sanity/client` /
11
+ `next-sanity`, `@strapi/*`, `@prismicio/*`, `storyblok-js-client` / `@storyblok/*`,
12
+ `@datocms/*`, `@wordpress/*`, `graphql-request` or `@apollo/client` pointed at a CMS endpoint.
13
+ - `.env.example` names that service's keys (`*_BUCKET_SLUG`, `*_READ_KEY`, `*_SPACE_ID`,
14
+ `*_ACCESS_TOKEN`, `*_PROJECT_ID` for another CMS). BetterCMS reports the missing ones on import
15
+ as `envWarning`.
16
+ - Pages fetch in `getServerSideProps`, `getStaticProps`, server components, `load()` or
17
+ `useFetch()` from a module that wraps that client.
18
+
19
+ Two ways out, in this order, and the user types no key in either. When the values already exist
20
+ on this machine, the platform takes them from the user's own files (the first section). When they
21
+ do not, the rule Sanity follows on a migration applies: **the content moves into BetterCMS and the
22
+ site reads it from BetterCMS** (the steps). Never ask the user for a key, and never leave the site
23
+ depending on one it does not have: on BetterCMS it would build and serve, then render empty or
24
+ crash in the browser.
25
+
26
+ ## First: are the values on this machine?
27
+
28
+ `list_env_vars` names what the repository's `.env.example` declares and the project does not set
29
+ (`missing`). When the user already runs this site, the values are usually in the repository's own
30
+ `.env.local` or `.env`. Then run, in the repository:
31
+
32
+ npx @bettercms-ai/cli@latest env push
33
+
34
+ It uploads the declared variables from those files straight to BetterCMS and prints their names
35
+ only. Never open those files, print a value or ask the user to paste one: a secret in this
36
+ conversation is a secret in a log. Rebuild, and the site runs as it did before.
37
+
38
+ When `env push` reports the variables as not found, nobody here has the old service's keys. That
39
+ is the case the steps below handle: the content moves into BetterCMS.
40
+
41
+ ## The steps
42
+
43
+ Check: with every one of the other service's variables unset, `npm run build` succeeds and every
44
+ page renders its content.
45
+
46
+ 1. **Find every read.** List the client module and each function the pages call, with the shape
47
+ each returns (the fields the components use, not the whole API response).
48
+ 2. **Model it** ([content-modeling.md](content-modeling.md)). One model per content type the pages
49
+ render: a list is a collection, a one-off (navigation, footer, landing hero) is a singleton.
50
+ Field names follow the code's own names, so the components don't change.
51
+ 3. **Seed the content.** If the user can export from the old service themselves, import that.
52
+ Otherwise create the entries from what the site shows: the repo's fixtures, sample data and
53
+ README, and the rendered pages in `get_site_composition`. Real-looking copy and working image
54
+ URLs, never lorem ipsum. The user edits it in BetterCMS afterwards.
55
+ 4. **Rewrite the client module, not the pages.** Keep every function's name and return shape, and
56
+ replace its body with a read from BetterCMS's Delivery API ([frameworks.md](frameworks.md)
57
+ shows the client per framework). Reading published content needs no key. The platform already
58
+ sets these on every build and every running server app, under the prefix the framework
59
+ exposes:
60
+
61
+ | Framework | Variables |
62
+ |---|---|
63
+ | Next.js | `NEXT_PUBLIC_BCMS_API_URL`, `NEXT_PUBLIC_BCMS_WORKSPACE`, `NEXT_PUBLIC_BCMS_PROJECT_ID` |
64
+ | Vite (React, Vue, Svelte) | `VITE_BCMS_*` |
65
+ | Nuxt | `NUXT_PUBLIC_BCMS_*` |
66
+ | Astro, SvelteKit | `PUBLIC_BCMS_*` |
67
+ | Create React App | `REACT_APP_BCMS_*` |
68
+ | Gatsby | `GATSBY_BCMS_*` |
69
+ | Server code, any framework | `BCMS_API_URL`, `BCMS_WORKSPACE`, `BCMS_PROJECT_ID` |
70
+
71
+ Do not add them to `.env` files. They are supplied, like Sanity's project id.
72
+ 5. **Keep actions that need the other service's secret working without it.** Payments, uploads and
73
+ writes to the old service stay in the code, guarded: when the key is unset, the button explains
74
+ that the feature is not configured. The page never throws.
75
+ 6. **Remove what is no longer read.** Drop the old SDK from `package.json` and its keys from
76
+ `.env.example` once nothing imports them.
77
+ 7. **Prove it.** Build with the old variables unset and open every page. Then continue with the
78
+ `convert_site` prompt: its bindings now point at BetterCMS content, and the Visual Editor edits it.