@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,121 +1,121 @@
1
- ---
2
- title: "Custom dock inputs"
3
- description: "When a field earns its own input in the editor dock, how to ask the user first, the input contract and protocol, the writes allowlist, the file layout, and the fallback guarantee."
4
- ---
5
-
6
- # Custom dock inputs
7
-
8
- **Outcome.** A field whose choice the stock dock cannot show (a colour, a preset scale, one look
9
- set by several props) gets a small input the user approved, and every other field keeps the stock
10
- control. Nothing an author or a visitor sees can break because of it.
11
-
12
- ## 1. The restraint ladder
13
-
14
- Industry practice: a custom input is the last resort. Climb in this order and stop at the first
15
- rung that works.
16
-
17
- 1. **The stock control.** The field type already draws one: text, number, select, boolean, image.
18
- 2. **A composition of ui keys.** `ui.control` (`segmented` for a short single choice), `ui.preview`
19
- and `ui.layout` for list items, `ui.group` for props, model `fieldsets` for groups, rows and
20
- tabs. See editor-chrome.md.
21
- 3. **A custom input.** Only when rungs 1 and 2 cannot show the choice, and only with the user's yes.
22
-
23
- A custom input never adds data. It draws fields that already exist in the schema.
24
-
25
- ## 2. Suggest, then the user approves
26
-
27
- `get_next_steps` lists `dock-input-candidate` when a field or prop looks like it needs one. Each
28
- row has the field `key`, a `reason` and, for a look set by several props, the sibling keys it would
29
- `writes`. The rule is a candidate list, never a defect.
30
-
31
- 1. Check rungs 1 and 2 for each row. Drop rows they cover.
32
- 2. Ask the user, in one message per field: name the field, the input you would write and the
33
- reason the stock control cannot do it. Wait for an explicit yes.
34
- 3. On yes: write the input file (section 5), then set `ui.input` with `confirmed: true`. These
35
- merge into the stored `ui`:
36
- `update_content_model { modelId | pageId, fieldUi: { "<path>": { input: { name, writes } } }, ifMatch, confirmed: true }`,
37
- `update_component { componentId, propUi: { "<key>": { input: { name, writes } } }, ifMatch, confirmed: true }`.
38
- `update_field`'s `ui` MERGES per key into the stored `ui`, as `fieldUi` does: send `null` for a key to clear it.
39
- 4. On no: leave it. Nothing is broken.
40
-
41
- A write through the agent tools that sets or changes `ui.input` without `confirmed: true` is
42
- refused 409 `input_needs_confirmation`, naming each path. `confirmed: true` is true only after the
43
- user approved that input; never send it to get past the refusal. Removing `ui.input` (send
44
- `input: null`) needs no approval. The create tools carry no approval: create the field without
45
- `ui.input`, then add it with `update_field` or `update_component`. A person saving in the
46
- dashboard is the approver and is never asked.
47
-
48
- ## 3. The contract
49
-
50
- The input is a React component that receives `DockInputProps` from `@bettercms-ai/dock-input`:
51
-
52
- | Prop | What it is |
53
- |---|---|
54
- | `value` | this field's current value |
55
- | `field` | `{ key, label, type, required, readOnly, writes, config? }`: the stored options and limits |
56
- | `document` | a read-only snapshot of the whole document's values, by field key |
57
- | `readOnly` | true when the author may not edit; draw the value, accept nothing |
58
- | `theme` | `"light"` or `"dark"`, for the kit theme |
59
- | `onChange(value)` | set this field |
60
- | `writeFields(patches)` | set this field and siblings listed in `field.writes`, together |
61
-
62
- `writeFields` takes `[{ field, value }]`, applies all of them or none to the DRAFT, as one undo
63
- step, and resolves `{ ok, reason? }`. Show the `reason` when `ok` is false.
64
-
65
- Build the UI from `@bettercms-ai/kit` so it matches the dock in both themes. The input never sees a
66
- token and makes no network call for content: everything it needs arrives as props.
67
-
68
- ## 4. Writes discipline
69
-
70
- `ui.input.writes` lists EVERY sibling field key the input writes, and nothing else. The dock
71
- refuses a patch to a key outside it, and checks each value against that field's own schema. A
72
- button-look input over `buttonVariant` set on `buttonVariant` declares
73
- `writes: ["buttonSize", "buttonColor"]`. An input that only edits its own field omits `writes`.
74
- At most 16 keys, each an existing sibling.
75
-
76
- ## 5. File layout
77
-
78
- One file per input, in the site's repository:
79
-
80
- ```
81
- bettercms/inputs/<name>.tsx
82
- ```
83
-
84
- - `<name>` is the `ui.input.name`: lowercase letters, digits and dashes, 1 to 48 characters,
85
- starting with a letter (`button-look`, `brand-swatch`).
86
- - The default export is the component, and the file does nothing else. The build wraps it in
87
- `mountDockInput`, which speaks the protocol (section 6), so the component only deals with props.
88
- Never call `mountDockInput` in the file: the input would mount twice.
89
- - The site needs `react`, `react-dom` and `@bettercms-ai/dock-input` in its `devDependencies`
90
- (plus `@bettercms-ai/kit` for kit components). The build bundles them into the input.
91
- - Keep it small and self-contained: kit components and the field's own data, no app imports.
92
-
93
- ## 6. Protocol summary
94
-
95
- `bcms-input/1`, over `window.postMessage` between the dock and a sandboxed frame served from the
96
- project's own inputs origin, never the dashboard's. `mountDockInput` handles all of it.
97
-
98
- - Dock to input: `init` (field, value, document, theme), `value` (after a commit), `result`
99
- (the verdict on a `change`), `readOnly`.
100
- - Input to dock: `ready`, `change` (an id and its patches), `height`, `focus`, `blur`, `error`.
101
- - Every message carries the session nonce from `init`; the dock drops anything else.
102
- - Limits: a patch is at most 64 KB of JSON, at most 10 patches per second, and `ready` must arrive
103
- within 3 seconds. Larger or faster messages are refused, never truncated.
104
-
105
- ## 7. Build, serve and scope
106
-
107
- - v1 is managed sites only (BetterCMS hosting). On a headless or hybrid site, do not suggest one.
108
- - The user's approval (`confirmed: true`) also switches custom inputs on for the project, unless an
109
- admin switched them off in Settings → General → Visual editor; then `get_next_steps` lists
110
- `dock-inputs-off`, and you tell the user instead of working around it.
111
- - Once custom inputs are enabled for the project, the inputs are built with the site and served
112
- on its inputs origin automatically. Push the file, wait for the build, then open the field in the
113
- dock to check it.
114
- - Name only inputs that exist in `bettercms/inputs/`. An unknown name is not an error, it falls back.
115
-
116
- ## 8. The fallback guarantee
117
-
118
- The stock control comes back whenever the input cannot run: an unknown name, inputs disabled for
119
- the project, a build that failed, a frame that throws or misses `ready`. The value is unchanged
120
- and stays editable. So a custom input can only add to the dock, never take a field away from an
121
- author. Removing `ui.input` returns the field to the stock control at once.
1
+ ---
2
+ title: "Custom dock inputs"
3
+ description: "When a field earns its own input in the editor dock, how to ask the user first, the input contract and protocol, the writes allowlist, the file layout, and the fallback guarantee."
4
+ ---
5
+
6
+ # Custom dock inputs
7
+
8
+ **Outcome.** A field whose choice the stock dock cannot show (a colour, a preset scale, one look
9
+ set by several props) gets a small input the user approved, and every other field keeps the stock
10
+ control. Nothing an author or a visitor sees can break because of it.
11
+
12
+ ## 1. The restraint ladder
13
+
14
+ Industry practice: a custom input is the last resort. Climb in this order and stop at the first
15
+ rung that works.
16
+
17
+ 1. **The stock control.** The field type already draws one: text, number, select, boolean, image.
18
+ 2. **A composition of ui keys.** `ui.control` (`segmented` for a short single choice), `ui.preview`
19
+ and `ui.layout` for list items, `ui.group` for props, model `fieldsets` for groups, rows and
20
+ tabs. See editor-chrome.md.
21
+ 3. **A custom input.** Only when rungs 1 and 2 cannot show the choice, and only with the user's yes.
22
+
23
+ A custom input never adds data. It draws fields that already exist in the schema.
24
+
25
+ ## 2. Suggest, then the user approves
26
+
27
+ `get_next_steps` lists `dock-input-candidate` when a field or prop looks like it needs one. Each
28
+ row has the field `key`, a `reason` and, for a look set by several props, the sibling keys it would
29
+ `writes`. The rule is a candidate list, never a defect.
30
+
31
+ 1. Check rungs 1 and 2 for each row. Drop rows they cover.
32
+ 2. Ask the user, in one message per field: name the field, the input you would write and the
33
+ reason the stock control cannot do it. Wait for an explicit yes.
34
+ 3. On yes: write the input file (section 5), then set `ui.input` with `confirmed: true`. These
35
+ merge into the stored `ui`:
36
+ `update_content_model { modelId | pageId, fieldUi: { "<path>": { input: { name, writes } } }, ifMatch, confirmed: true }`,
37
+ `update_component { componentId, propUi: { "<key>": { input: { name, writes } } }, ifMatch, confirmed: true }`.
38
+ `update_field`'s `ui` MERGES per key into the stored `ui`, as `fieldUi` does: send `null` for a key to clear it.
39
+ 4. On no: leave it. Nothing is broken.
40
+
41
+ A write through the agent tools that sets or changes `ui.input` without `confirmed: true` is
42
+ refused 409 `input_needs_confirmation`, naming each path. `confirmed: true` is true only after the
43
+ user approved that input; never send it to get past the refusal. Removing `ui.input` (send
44
+ `input: null`) needs no approval. The create tools carry no approval: create the field without
45
+ `ui.input`, then add it with `update_field` or `update_component`. A person saving in the
46
+ dashboard is the approver and is never asked.
47
+
48
+ ## 3. The contract
49
+
50
+ The input is a React component that receives `DockInputProps` from `@bettercms-ai/dock-input`:
51
+
52
+ | Prop | What it is |
53
+ |---|---|
54
+ | `value` | this field's current value |
55
+ | `field` | `{ key, label, type, required, readOnly, writes, config? }`: the stored options and limits |
56
+ | `document` | a read-only snapshot of the whole document's values, by field key |
57
+ | `readOnly` | true when the author may not edit; draw the value, accept nothing |
58
+ | `theme` | `"light"` or `"dark"`, for the kit theme |
59
+ | `onChange(value)` | set this field |
60
+ | `writeFields(patches)` | set this field and siblings listed in `field.writes`, together |
61
+
62
+ `writeFields` takes `[{ field, value }]`, applies all of them or none to the DRAFT, as one undo
63
+ step, and resolves `{ ok, reason? }`. Show the `reason` when `ok` is false.
64
+
65
+ Build the UI from `@bettercms-ai/kit` so it matches the dock in both themes. The input never sees a
66
+ token and makes no network call for content: everything it needs arrives as props.
67
+
68
+ ## 4. Writes discipline
69
+
70
+ `ui.input.writes` lists EVERY sibling field key the input writes, and nothing else. The dock
71
+ refuses a patch to a key outside it, and checks each value against that field's own schema. A
72
+ button-look input over `buttonVariant` set on `buttonVariant` declares
73
+ `writes: ["buttonSize", "buttonColor"]`. An input that only edits its own field omits `writes`.
74
+ At most 16 keys, each an existing sibling.
75
+
76
+ ## 5. File layout
77
+
78
+ One file per input, in the site's repository:
79
+
80
+ ```
81
+ bettercms/inputs/<name>.tsx
82
+ ```
83
+
84
+ - `<name>` is the `ui.input.name`: lowercase letters, digits and dashes, 1 to 48 characters,
85
+ starting with a letter (`button-look`, `brand-swatch`).
86
+ - The default export is the component, and the file does nothing else. The build wraps it in
87
+ `mountDockInput`, which speaks the protocol (section 6), so the component only deals with props.
88
+ Never call `mountDockInput` in the file: the input would mount twice.
89
+ - The site needs `react`, `react-dom` and `@bettercms-ai/dock-input` in its `devDependencies`
90
+ (plus `@bettercms-ai/kit` for kit components). The build bundles them into the input.
91
+ - Keep it small and self-contained: kit components and the field's own data, no app imports.
92
+
93
+ ## 6. Protocol summary
94
+
95
+ `bcms-input/1`, over `window.postMessage` between the dock and a sandboxed frame served from the
96
+ project's own inputs origin, never the dashboard's. `mountDockInput` handles all of it.
97
+
98
+ - Dock to input: `init` (field, value, document, theme), `value` (after a commit), `result`
99
+ (the verdict on a `change`), `readOnly`.
100
+ - Input to dock: `ready`, `change` (an id and its patches), `height`, `focus`, `blur`, `error`.
101
+ - Every message carries the session nonce from `init`; the dock drops anything else.
102
+ - Limits: a patch is at most 64 KB of JSON, at most 10 patches per second, and `ready` must arrive
103
+ within 3 seconds. Larger or faster messages are refused, never truncated.
104
+
105
+ ## 7. Build, serve and scope
106
+
107
+ - v1 is managed sites only (BetterCMS hosting). On a headless or hybrid site, do not suggest one.
108
+ - The user's approval (`confirmed: true`) also switches custom inputs on for the project, unless an
109
+ admin switched them off in Settings → General → Visual editor; then `get_next_steps` lists
110
+ `dock-inputs-off`, and you tell the user instead of working around it.
111
+ - Once custom inputs are enabled for the project, the inputs are built with the site and served
112
+ on its inputs origin automatically. Push the file, wait for the build, then open the field in the
113
+ dock to check it.
114
+ - Name only inputs that exist in `bettercms/inputs/`. An unknown name is not an error, it falls back.
115
+
116
+ ## 8. The fallback guarantee
117
+
118
+ The stock control comes back whenever the input cannot run: an unknown name, inputs disabled for
119
+ the project, a build that failed, a frame that throws or misses `ready`. The value is unchanged
120
+ and stays editable. So a custom input can only add to the dock, never take a field away from an
121
+ author. Removing `ui.input` returns the field to the stock control at once.
@@ -1,92 +1,92 @@
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 40 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
- A Global field with `overridable: false` and a `lockReason` is not offered on a page: a page write
86
- of it answers 403 **LAYOUT_FIELD_NOT_OVERRIDABLE** with the reason. A page value written earlier is
87
- kept but the Global value renders; `get_layout` scope page lists it under `shadowed`.
88
-
89
- ## Acceptance
90
-
91
- Read back with `get_content_model`, `get_page` or `get_component`. Done means `get_next_steps`
92
- lists none of `dock-budget`, `dock-repeater-unnamed`, `dock-showif-order`.
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 40 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
+ A Global field with `overridable: false` and a `lockReason` is not offered on a page: a page write
86
+ of it answers 403 **LAYOUT_FIELD_NOT_OVERRIDABLE** with the reason. A page value written earlier is
87
+ kept but the Global value renders; `get_layout` scope page lists it under `shadowed`.
88
+
89
+ ## Acceptance
90
+
91
+ Read back with `get_content_model`, `get_page` or `get_component`. Done means `get_next_steps`
92
+ lists none of `dock-budget`, `dock-repeater-unnamed`, `dock-showif-order`.
@@ -1,78 +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.
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.