@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.
- package/README.md +86 -86
- package/SKILL.md +160 -160
- package/dist/index.js +38 -9
- package/dist/index.js.map +1 -1
- package/dist/kit-catalog.json +1509 -1509
- package/dist/skills/bettercms/SKILL.md +206 -206
- package/dist/skills/bettercms/references/accessibility.md +171 -171
- package/dist/skills/bettercms/references/animation.md +296 -296
- package/dist/skills/bettercms/references/astro.md +148 -148
- package/dist/skills/bettercms/references/bindings.md +366 -366
- package/dist/skills/bettercms/references/componentize.md +185 -185
- package/dist/skills/bettercms/references/components.md +415 -415
- package/dist/skills/bettercms/references/content-experiments.md +62 -62
- package/dist/skills/bettercms/references/content-modeling.md +382 -382
- package/dist/skills/bettercms/references/detection.md +465 -465
- package/dist/skills/bettercms/references/dock-input.md +121 -121
- package/dist/skills/bettercms/references/editor-chrome.md +92 -92
- package/dist/skills/bettercms/references/external-content.md +78 -78
- package/dist/skills/bettercms/references/finish-site.md +377 -377
- package/dist/skills/bettercms/references/forms-and-interactions.md +245 -245
- package/dist/skills/bettercms/references/frameworks.md +159 -159
- package/dist/skills/bettercms/references/getting-started.md +156 -156
- package/dist/skills/bettercms/references/integrations.md +364 -364
- package/dist/skills/bettercms/references/kit-catalog.md +3259 -3259
- package/dist/skills/bettercms/references/kit-scenarios.md +712 -712
- package/dist/skills/bettercms/references/localization.md +85 -85
- package/dist/skills/bettercms/references/media-and-brand.md +181 -181
- package/dist/skills/bettercms/references/next.md +189 -189
- package/dist/skills/bettercms/references/other-frameworks.md +85 -85
- package/dist/skills/bettercms/references/portable-text.md +213 -213
- package/dist/skills/bettercms/references/programmatic-seo.md +98 -98
- package/dist/skills/bettercms/references/publishing.md +289 -289
- package/dist/skills/bettercms/references/seo.md +357 -357
- package/dist/skills/bettercms/references/shopify-headless.md +52 -52
- package/dist/skills/bettercms/references/site-blueprints.md +895 -895
- package/dist/skills/bettercms/references/site-review.md +74 -74
- package/dist/skills/bettercms/references/worked-examples-advanced.md +509 -509
- package/dist/skills/bettercms/references/worked-examples.md +885 -885
- package/dist/skills/bettercms/references/write-limits.md +217 -217
- 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.
|