@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.
- package/README.md +1 -0
- package/SKILL.md +1 -1
- package/dist/index.js +908 -487
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +175 -0
- package/dist/skills/bettercms/references/astro.md +67 -0
- package/dist/skills/bettercms/references/bindings.md +201 -0
- package/dist/skills/bettercms/references/componentize.md +106 -0
- package/dist/skills/bettercms/references/components.md +228 -0
- package/dist/skills/bettercms/references/content-experiments.md +62 -0
- package/dist/skills/bettercms/references/content-modeling.md +185 -0
- package/dist/skills/bettercms/references/editor-chrome.md +88 -0
- package/dist/skills/bettercms/references/external-content.md +78 -0
- package/dist/skills/bettercms/references/finish-site.md +123 -0
- package/dist/skills/bettercms/references/forms-and-interactions.md +203 -0
- package/dist/skills/bettercms/references/frameworks.md +149 -0
- package/dist/skills/bettercms/references/getting-started.md +106 -0
- package/dist/skills/bettercms/references/localization.md +81 -0
- package/dist/skills/bettercms/references/media-and-brand.md +142 -0
- package/dist/skills/bettercms/references/next.md +93 -0
- package/dist/skills/bettercms/references/other-frameworks.md +66 -0
- package/dist/skills/bettercms/references/portable-text.md +206 -0
- package/dist/skills/bettercms/references/programmatic-seo.md +98 -0
- package/dist/skills/bettercms/references/publishing.md +146 -0
- package/dist/skills/bettercms/references/seo.md +156 -0
- package/dist/skills/bettercms/references/shopify-headless.md +52 -0
- package/dist/skills/bettercms/references/site-blueprints.md +250 -0
- package/dist/skills/bettercms/references/site-review.md +73 -0
- package/dist/skills/bettercms/references/write-limits.md +84 -0
- 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.
|