@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,382 +1,382 @@
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
- ## Contents
18
-
19
- 1. Read the site before modeling
20
- 2. Pages, collections and entries (thresholds, promoting items to a collection)
21
- 3. Four structure layers, four meanings
22
- 4. Field decisions (references, lists, listings, the `document` body, buttons)
23
- 5. Editor labels and order (when to write help text)
24
- 6. Changing a schema safely
25
- 7. Reading a schema written in the V2 language (field type map, glossary)
26
-
27
- ## 1. Read the site before modeling
28
-
29
- Inventory route files and rendered routes, source data schemas, repeated records, page templates,
30
- navigation, taxonomies, authors, tags, media references, SEO, forms, locales and publishing
31
- states. Compare with the project's pages, collections and entries (`list_pages`,
32
- `list_content_models`, `get_content_structure`). Note where a value is shared, where it is an
33
- instance override, and where one entry owns it outright.
34
-
35
- ## 2. Pages, collections and entries
36
-
37
- A page is `singleton` (one page and its own content: home, about, a legal page) or `dynamic`
38
- (one template page whose collection entries each render at the page's path plus the entry slug).
39
- Choose by repetition: a shape that repeats per item is a collection; a one-off is a singleton,
40
- even when it lists items, because the list page reads the collection. A dynamic page is its
41
- collection: the model and its entries both point at the page (`content_models.page_id`,
42
- `content_entries.page_id`), so never create a separate page per entry. A dynamic page made with
43
- `create_page` takes its route pattern and its collection through `update_page { dynamic:
44
- { previewPath, modelId } }`: `/blog/{slug}`, and an existing collection with no page yet.
45
-
46
- A listing elsewhere (latest posts on home, related posts on an article) is a `collection` block
47
- with `source: { modelId }`, never copied entries. It also takes `filters` (max 5, `{ field, op,
48
- value }` with the content API operators), `sort` (`-publishedAt,title`), `pinnedIds` (max 10,
49
- shown first), `excludeCurrent` and `limit` (default 20), beside `order` (`newest` or `oldest`) and
50
- `emptyText`. `cardComponentId` names the component each card is drawn through; without one a card
51
- shows the entry's `titleField` (default `title`) and `excerptField`. On a `dynamic` page the block
52
- lists that page's collection and draws ONE entry on `/<page>/<entrySlug>`, through
53
- `detailComponentId` when set. BetterCMS-rendered pages read it on every request;
54
- a route the repo builds does not draw this block, so list there with the content API.
55
-
56
- | Source pattern | BetterCMS destination |
57
- |---|---|
58
- | Home, about, contact, legal, editorial landing page | Singleton page with its own content |
59
- | Blog / case-study / product list URL | A list page plus a separate collection |
60
- | Repeated records sharing one template | One collection and one dynamic detail page |
61
- | 404, search, gated page | A page that keeps its access and noindex behaviour |
62
- | Author, category, tag, company reused by entries | A shared collection, referenced from the entries |
63
- | Data from an external runtime API | Keep the adapter and its owner; never fabricate entries |
64
- | Sitewide nav, footer, promo bar | Global Layout, never copied page fields |
65
-
66
- **Thresholds.** Three or more pages sharing a URL pattern and a card or detail layout are a
67
- collection. Two are a collection when the set will grow (jobs, case studies). Otherwise they are
68
- singletons.
69
-
70
- **Promote repeater items to a collection** when any one holds; keep them inline when none does
71
- (pricing bullets, a hero proof line, quotes on a one-off campaign page):
72
-
73
- | Signal | Example |
74
- |---|---|
75
- | The item has its own URL | A case-study card linking to its detail page |
76
- | The same item shows on two or more pages | A testimonial on home and on a product page |
77
- | It carries facts that must not drift between copies | Price, stock, rating, job status, a person's photo |
78
- | Another entry points at it | A post's author, an event's speaker |
79
- | Editors will filter, sort or count the items | Jobs by team, events by date |
80
-
81
- ## 3. Four structure layers, four meanings
82
-
83
- Name the layer every time you say "folder". The answer to "where does X live" differs per layer.
84
-
85
- 1. **Content sidebar folders** organise the dashboard only: nothing here reads or writes a URL,
86
- slug, schema, content or publish state. Read with `get_content_structure`, get a proposal from
87
- `suggest_content_structure`, apply it whole with `set_content_structure` once the user agrees,
88
- or change one thing with `create_folder`, `rename_folder`, `move_to_folder` or `delete_folder`
89
- (deleting a folder moves its contents up). `set_content_structure` replaces the whole document:
90
- send the `version` you read; a 412 `VERSION_CONFLICT` means someone changed it, so read and
91
- re-apply. It refuses duplicate folder ids, a parent not in the doc, a cycle, and nesting deeper
92
- than 4. The standard pins the home page and each list page at the top, gives each content
93
- family one folder named for its detail type (the list page and its collection are two items),
94
- keeps other standalone pages in Pages (Pages › <language> on a multilingual site),
95
- taxonomies used by two or more families in
96
- Shared, and Settings last. Components are not sidebar items. Whether folders show depends on a
97
- dashboard feature flag, so an empty sidebar is not proof the structure is missing.
98
- 2. **URL page folders** are part of a page's path unless set not to be included in it. Renaming
99
- or moving one 301s every affected URL and queues a rebuild: a routing change that needs the
100
- user's review. They are set in the dashboard, not through the sidebar tools.
101
- 3. **Component groups** (`component_groups`) organise definitions by family or role
102
- (Navigation, Hero, CTA, Forms), never by the page that uses them ([components.md](components.md) §8).
103
- 4. **Media library folders** organise assets (`folderId` on `create_media_upload`) and never
104
- rewrite a stored content URL.
105
-
106
- What reads published structure: the sitemap (skips noindex pages), robots.txt (points at the
107
- sitemap), RSS (dated by `publishedAt`), `llms.txt` (one link per published item).
108
-
109
- ## 4. Field decisions
110
-
111
- Before designing a section's fields, find the closest component-kit entry: the chooser by
112
- category and edge tag is [kit-scenarios.md](kit-scenarios.md), the full list is the
113
- `bettercms://kit` resource and [kit-catalog.md](kit-catalog.md). The entry's notes are the
114
- field mapping and its pitfalls the review list; the rules below still decide the storage.
115
-
116
- - **Model each visual band as a `group` field (or a repeater), or a component instance.** The
117
- schema's shape decides whether the site can ever be componentized. The componentize lane finds
118
- a section in a `group`, a repeater, or two or more top-level keys sharing a prefix before `-` or
119
- `_` (`"hero_title"` + `"hero_subtitle"` → `hero`). A key with NO shared prefix (`title`,
120
- `intro`) is a lone scalar, refused `NOT_A_SECTION`. Keep ONE separator per band: a `-` family
121
- and a `_` family never share a component. A `group` is the robust choice, and child keys inside
122
- it follow the flat-namespace rule (`"hero_title"`).
123
- - A collection for an entity with its own lifecycle and relationships; a nested group for values
124
- one page or section owns.
125
- - A reference for reusable entities and internal links where the model supports it; an external
126
- URL for links that leave the site.
127
- - **Resolve references before creating anything.** Build the target collection and its entries
128
- first, then the model that points at them. Match each source item to an existing entry by
129
- stable facts (a slug, a product handle, a name plus the start of the quote) and create only what
130
- is missing. Store the entry id, never an href or a title. A write naming a target that does not
131
- exist is refused with 400 (ref_missing). Flag an item with no identity (a quote with no name);
132
- never import it.
133
- - A repeater for small, same-shaped items (cards, FAQ rows, links, social accounts); a nested
134
- component or slot when an item has its own layout or variants.
135
- - **Lists are arrays; tags are taxonomy.** Features, steps, benefits and "what's included" are an
136
- `array` (`itemType: "text"`), or a repeater when each item carries its own state (included or
137
- not, an icon). Never a group holding one field. A label set that filters or classifies entries
138
- is a `tags` field; when each label has its own page, a collection referenced with `multi-reference` (§2).
139
- - **Never freeze a feed.** A list of entries on the source stays a listing: copying the nine
140
- visible cards into a repeater stops it updating. On a page BetterCMS renders, that is the
141
- `collection` block: on the collection's own dynamic page it lists that collection, and with
142
- `source: { modelId }` it lists any collection on any hosted page (§2). It takes `limit` (1 to
143
- 200), `order` (`newest` or `oldest`) or `sort`, `filters`, `pinnedIds`, `excludeCurrent` and
144
- `emptyText`; in an article template a filter value may be `{{field}}` of the entry shown, which
145
- is a "related" list. Pagination and grouping stay a query in the site's code. Always set the
146
- empty state, never write card titles or images into the listing, and put "read next" on the
147
- template once, not on each entry.
148
- - **`document` is the one long body.** A page schema or a collection may hold it, one at
149
- most, and only as a top-level field: a group or a repeater cannot contain it. Label it Body. Short formatted copy stays `richtext`.
150
- - **Model a call to action as a single `button` field.** It stores `{ label, href }` plus the
151
- optional look keys `variant`, `size`, `arrow`, `icon` and `newTab`, and the site draws it with
152
- `BcmsButton` ([astro.md](astro.md), [next.md](next.md)). Never a text
153
- field beside a link field: the pair has nowhere to keep a look, and a new pair in the importer's
154
- spelling (`hero-span-go` + `hero-a-go-href`) is refused with 400 `cta_pair` on `add_field`,
155
- `create_page` and model writes. A page that already holds a pair (`primaryCtaText` +
156
- `primaryCtaHref`) becomes one button with `update_field` `{ pageId, path: "primaryCtaText",
157
- mergeHref: "primaryCtaHref" }`: each entry's DRAFT gets `{ label, href }` and keeps the old values,
158
- so code still reading them keeps rendering; the live copy changes on publish. On a route the
159
- site's app renders at request time the merge is refused (409 `CODE_RENDERED_ROUTE`): change the
160
- code to draw the CTA with `BcmsButton`, and the next build adds the button field. Inside a component
161
- a CTA is a `button` block whose `props` one `button` prop targets ([components.md](components.md) §1).
162
- - A sponsored, untrusted or user-generated link takes `rel` (`nofollow`, `sponsored` or `ugc`) on the
163
- button, or a `link` field value written as `{ href, rel?, newTab? }`; code that opts into the object reads `.href`.
164
- - A `date` field with a time takes `timezone` (`site` or an IANA name such as `Europe/Paris`) in its config;
165
- values must then end in `Z` or an offset, and a bracketed zone suffix is dropped on save.
166
- - Never model SEO columns ([seo.md](seo.md)), Form questions (they live on the Form document) or
167
- Global Layout values as content fields.
168
- - Keep source ids, slugs, locale, status and previous URLs as migration metadata, stable across
169
- re-imports.
170
- - Lock a field whose value another system owns (a store, job board or feed id): `readOnly: "developer"` with a `lockReason`.
171
- Only schema access may then change the value or the lock; other writes are refused (403 `FIELD_LOCKED`) unless they send the stored value back.
172
- - **Prefer a `select` over a `boolean` for a state that may grow** (`draft | review | live`, not
173
- `isLive`). Why: no tool retypes a field, so a third state later costs a new field and a value
174
- migration (§6).
175
- - Choose field types before the first write. A `link` accepts any scheme-less href (`/about`,
176
- `about.html`, `#section`) and `http(s):`, `mailto:`, `tel:`; every other scheme is refused.
177
- Choose the type from what the rendered value is, not habit ([portable-text.md](portable-text.md)).
178
-
179
- ## 5. Editor labels and order
180
-
181
- **Name for meaning, not look.** Before writing a key or label, ask: would the name survive a
182
- redesign? `ctaText`, `authorBio` and `features` pass; `blueButton`, `leftColumn` and `bigHeading`
183
- fail, because the next redesign makes them lie.
184
-
185
- Short labels named for what the editor sees: Heading, Body, Primary button, Label, Link, Image,
186
- Alt text — or the name the site's code already uses (`cents` → Cents), never the copy itself
187
- (`.00`). An import names every field it derives the same way, by its section and its role
188
- there: "Hero headline", "Hero intro", "Hero primary button", "Pricing plans", "Footer link 2
189
- URL", "Header logo"; a list's columns by their role in the row ("Title", "Description", "Link
190
- URL"). A role is numbered only where it repeats in one section, and labels are unique on a page.
191
- Keys never change. Rename a label where the site's code has a better word: a name you give is
192
- kept on every later release, and one the import gave follows the import. Most fields need no help line from you (below). Group by what renders
193
- together, not by the DOM or the source file: core content first, optional appearance second, technical fields hidden. Repeatable items need a meaningful preview label (the FAQ question, the
194
- button label), reorder, and stable item ids. An empty optional field removes its element without
195
- leaving a gap; a long value wraps, and clamps only where the design intends it.
196
-
197
- ### Writing help text
198
-
199
- Most fields get no help. The platform writes a line only where one of the four reasons below
200
- holds and leaves every other element with none, so leaving `helpText` out is the default, not a
201
- gap. It says so: `helpText: { source: "auto" }` in the response, `helpSource: "auto"` on the
202
- stored field. Text you send is stored as yours (`helpSource: "agent"`) and is never rewritten;
203
- `editor_setup` rewrites only its own lines and never a person's.
204
-
205
- Ask one question per field: does the editor need this line to type the right value? Write your
206
- own `helpText` (on a field or a prop) only for one of four reasons, written as one short plain
207
- sentence under 90 characters, with no semicolon, no dash and no figure taken from the page.
208
-
209
- | Reason | Good |
210
- |---|---|
211
- | A format or unit the label cannot show | Price in cents. · Length in minutes. · A .vtt caption file. |
212
- | Where the value shows, when the canvas cannot show it | Shown in search results and link previews. · Read by screen readers, not shown. |
213
- | A real constraint or consequence | Changing this breaks existing links. · Must be later than the start date. |
214
- | A fallback | Leave empty to use the page title. |
215
-
216
- Everything else is left out:
217
-
218
- | Never | Bad | Do instead |
219
- |---|---|---|
220
- | The label or the value again | Heading: "The section's main heading." · `e.g. "Starter Kit"` | Nothing. The API refuses help that echoes the value |
221
- | Coaching the writing | "Keep it short." · "Two to four words works best." · "Start with a verb." | A length is a counter: a number in `config` (`"maxLength": 60`, §6) |
222
- | An example of the value | Badge: "e.g. Most popular" | A `placeholder` where the control takes one (component props do) |
223
- | Strategy | Secondary button: "A lower-commitment option that builds trust." | Nothing. The label says which button it is |
224
- | Internal notes | "Renders as h2." · "Label and link are one unit." · a ticket or decision id | Keep them in the code or the report, never in the form |
225
- | Generic good practice | "Add alt text for screen readers." on every image | Nothing |
226
-
227
- One sentence, plain words, sentence case. If more than about one field in five carries your own
228
- line, you are over-writing. A component, a collection and a Layout Section (`update-section`)
229
- still take a `description` that says what the thing is ("Hero with a heading, supporting text,
230
- two buttons and an image"), never its name again.
231
-
232
- `get_next_steps` lists `help-text-missing` for an element the editor setup has not settled yet, or
233
- one whose line only echoes its label, and `get_binding_report` and `get_site_composition` carry the
234
- coverage as `help`. The platform writes a line only for the four reasons above and leaves most
235
- elements with none; clear the finding by running `editor_setup`, or by writing one of the four
236
- kinds yourself, never with filler.
237
-
238
- **Config keys.** Write only the `config` keys a field type documents (`maxLength`, `min`/`max`,
239
- `options`, `itemType`, `zones`, a rich text's `accept`/`inline`): any other key sends that field to
240
- the old classic editor. The import's own keys (`attr`, `numeric`, `units`, `band`, `seeded`, …) are the
241
- server's: both MCP surfaces drop them from what you write.
242
-
243
- ## 6. Changing a schema safely
244
-
245
- A schema write alone is not a release. Before one, read the model with `get_content_model`, a few
246
- sample entries, the component definitions that touch it and the frontend readers, and write a
247
- before/after field map: key, type, label, required, default, validation, editor group, and what
248
- happens to existing values.
249
-
250
- 1. **Guard concurrent edits.** Pass the model's `optimisticVersion` as `ifMatch` to
251
- `update_content_model`. A 409 means it changed since you read it: re-read and re-plan, never
252
- resend. Omitted, the last write wins, except on the editor-chrome args, which refuse without
253
- it (428).
254
- 2. **Know what the MCP can do.** `add_field` (a model's `modelId` or a page's `pageId`) appends
255
- and never overwrites or retypes. `update_field` changes ONE existing field, addressed by a dot
256
- path through its groups and repeaters (`hero.title`): its label, help text, `required`,
257
- validation `config` (merged by key; null deletes one), `showIf`, `ui` or placement, written at
258
- the version it read. A retype needs `allowRetype: true`, because stored values that do not fit
259
- the new type are lost; a key rename needs the migration in step 3. `update_content_model` edits
260
- the model's name, slug, URL pattern and fieldsets, and removes model fields only with
261
- `removeFields` (step 3). The home page cannot be deleted, and changing a local type file is not
262
- a schema change.
263
- 3. **Retire or rename a field in stages, never in one write.** Renaming a key re-addresses its
264
- content: `update_field` with a new `key` is refused with 409 and the count of entries holding
265
- the old key, unless `allowFieldRemoval: true` discards their values. So a rename is a new field
266
- plus a migration:
267
- 1. Relabel the old field "Old title (Deprecated)" and set help text naming its replacement
268
- (`update_field` `label` and `helpText`).
269
- 2. Move it into a `collapsed: true` fieldset so editors stop reaching for it
270
- ([editor-chrome.md](editor-chrome.md) §2).
271
- 3. Add the replacement (`add_field`, with `modelId` or `pageId`) with the final type and name.
272
- 4. Migrate values in code, draft first, and have the frontend read the new key.
273
- 5. Remove the old one. Page fields: `remove_page_fields` (dry run by default) returns
274
- `keysWithContent` and writes nothing; read it, then run it with `dryRun: false` and
275
- `discardContent: true` only for keys you migrated. Model fields: `update_content_model` with
276
- `removeFields: ["oldKey"]` and `allowFieldRemoval: true`, sent with the `ifMatch` of your
277
- read. It has NO dry run and discards that key's value in every entry, so count the entries
278
- holding it first (`list_content_entries` with `modelId`: `meta.total` is the count, `page`
279
- walks past the 200-row `limit`). Top-level keys only; an unknown key refuses the
280
- whole call; a page's own fields go through `remove_page_fields`. Both discard data: removal
281
- is the last step, with the user's yes.
282
- Why: every stage before 5 is reversible, and a site built against the old key keeps rendering
283
- until its next release reads the new one.
284
- 4. **Put numeric limits in `config` as numbers** (`"maxLength": 60`), with `update_field` on a
285
- field that exists. A numeric string is converted on save; an impossible value is refused with
286
- `FIELD_CONFIG_INVALID`.
287
- 5. **Check existing records before tightening validation.** Backfill or give a real default
288
- before making a field required; keep a source-to-target map and a snapshot for any high-volume
289
- transform.
290
- 6. **Validate on a draft.** Entries written through the MCP pass one write path (request schema,
291
- normalisation, per-field checks, uniqueness), and a write failing any step stores nothing.
292
- Publishing copies the draft to live, so a draft that passes is the value that ships.
293
- 7. **Change a component family together** (same prop keys and types on every variant;
294
- [components.md](components.md) §1 and §3).
295
- 8. **Regenerate types** with `bettercms-codegen` in every consuming site after any model or field
296
- change, commit the output if the repo commits it, and run that repo's build and typecheck with
297
- the script it already has. Templates typed against an old schema compile and read `undefined`.
298
- 9. **Verify** the editor's grouping and controls, one draft edit per changed field class, the
299
- preview binding, the static or headless reading path, and the public output once a release is
300
- authorized.
301
-
302
- Content models, frontend types, component definitions and live content have separate lifecycles;
303
- there is no single "deploy schema" action. Production publication stays behind the user's
304
- authorization and the component readiness and Visual Approval gates.
305
-
306
- **Evidence to return:** model and field ids, the version you wrote against, migrated record
307
- count, codegen and build result, one editor check and one rendered-page check, warnings, and any
308
- change the platform could not make (an open dependency, never a success).
309
-
310
- ## 7. Reading a schema written in the V2 language
311
-
312
- A plan or Kit sample may arrive in the V2 schema language. It is an intermediate model only:
313
- production cannot store it. Translate before the first write.
314
-
315
- - **Keys.** V2 names are camelCase and scoped per object (`hero.heading`). Production keys are
316
- flat, one namespace per model, prefixed by section: `"hero_heading"`.
317
- - **Labels and help.** V2 `title` is `label`; V2 `description` is `helpText` (§5 decides whether
318
- it is written at all).
319
- - **Limits.** V2 `validation` becomes numbers in `config` (§6 step 4).
320
-
321
- | V2 type | Model field (`create_content_model`, `add_field`) | Component prop (`create_component`) |
322
- |---|---|---|
323
- | `string` | `text` with `richText: false` | `text` |
324
- | `text` | `longtext` | `text` |
325
- | `richtext` | `richtext` for section copy; `document` for the one body | `richtext` |
326
- | `number` | `number` | `number` |
327
- | `rating` | `number` with `config.min`, `max`, `step` and `ui.control: "rating"` (stars) | `number` |
328
- | `boolean` | `boolean` | `boolean` |
329
- | `slug` | `slug` (an entry's own slug stays on the entry) | `text` |
330
- | `url`, `link` | `link` | `link`; `url` for a media or embed address |
331
- | `email`, `phone` | `email`; a phone number is `text` with `richText: false` (`phone` opens only in the classic editor) | `text` |
332
- | `date`, `datetime` | `date`, `datetime` | `date` (`YYYY-MM-DD`); a datetime stays `text` |
333
- | `color` | `color` | `color` |
334
- | `select` | `select` | `select` with `config.options` |
335
- | `multiselect` | `multiselect` with `config.options` (a closed `string[]` choice list; `min`/`max` count the choices) | `multiselect` with `config.options` |
336
- | `tags` | `tags` (a `string[]`; `config.options` are suggestions, `allowCustom: false` closes the list); a taxonomy with its own pages is a collection plus `multi-reference` | `tags` |
337
- | `image` | `image` | `image` |
338
- | `file`, `video` | `file`; a hosted video URL is a `link` | `file`; `url` for a hosted video |
339
- | `icon` | `icon` (an icon name or an uploaded image) | `icon` (an icon name or an image) |
340
- | `button` | `button` | `button`: one prop targeting the `props` of a `button` block (§4) |
341
- | `reference` | `reference`; `multi-reference` for many | none: keep it on the model |
342
- | `code` | `longtext` | `text` |
343
- | `json` | `json` | `text` |
344
- | `geopoint` | `location` (`{ lat, lng, label? }`) | `group` of two `number` props |
345
- | `table` | `table` with `config.mode: "data"` (one data table: the editor's Columns and Rows drill-in); a Portable Text table inside `richtext` or `document` when it belongs to running copy | `table` with `config.mode: "data"` |
346
- | `object` | `group` | `group` |
347
- | `array` of objects | `repeater`; item limits in `config.minItems` / `config.maxItems` (the editor shows "max N") | `table` |
348
- | `array` of strings | `array` (`itemType: "text"`) | `table` |
349
- | `array` of several item shapes | one component per shape, placed as sections; on a model, a `repeater` whose rows carry each shape's fields (`modular` opens only in the classic editor) | separate components |
350
- | `slot` | components placed on the page (componentize); never a `sections` or `component-ref` field, which the page form cannot edit | `slot` (one instance; a list with `config.multiple`) |
351
- | a form `reference` | a `form` block inside the section | `form` |
352
-
353
- V2 features with no production shape are dropped and reported, never faked: a per-cell table
354
- type, per-breakpoint values (only `style.hideOn` differs by breakpoint) and per-field
355
- localization. Two have a shape now: typed table columns are a `repeater` with one field per column
356
- (never `ui.layout: "table"`, which the editor cannot draw), and a slot with several children is a `slot` prop with `config.multiple`
357
- ([components.md](components.md) §1).
358
-
359
- | The plan says | Production word |
360
- |---|---|
361
- | Block | Component (the type); section or component instance (the placed band) |
362
- | Site-wide block, global | Global Layout (`update_layout`) |
363
- | CMS item | Entry |
364
- | Collection page | An entry shown through its collection's dynamic page |
365
- | Static page | `singleton` page |
366
- | Collection | Content model plus its `dynamic` page |
367
- | Variant, layout | Components sharing one `sectionType` ([components.md](components.md)) |
368
- | Listing | `collection` block (§4) |
369
- | Picks | `multi-reference` |
370
- | Polymorphic array, modules | one component per shape |
371
-
372
- Tools and reports use the production word. Labels and help an editor reads never say
373
- "document": the `document` field is labelled Body.
374
-
375
- ## Acceptance
376
-
377
- - Every imported route and collection has one clear home; one dynamic template per collection.
378
- - Sidebar moves keep every public route and record id; URL changes carry explicit redirects
379
- (`create_redirect`).
380
- - Repeated data is modelled once, with references or reusable components.
381
- - Editors find and understand the fields without reading source code.
382
- - Model changes compile in each consuming site and keep the published output unchanged.
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
+ ## Contents
18
+
19
+ 1. Read the site before modeling
20
+ 2. Pages, collections and entries (thresholds, promoting items to a collection)
21
+ 3. Four structure layers, four meanings
22
+ 4. Field decisions (references, lists, listings, the `document` body, buttons)
23
+ 5. Editor labels and order (when to write help text)
24
+ 6. Changing a schema safely
25
+ 7. Reading a schema written in the V2 language (field type map, glossary)
26
+
27
+ ## 1. Read the site before modeling
28
+
29
+ Inventory route files and rendered routes, source data schemas, repeated records, page templates,
30
+ navigation, taxonomies, authors, tags, media references, SEO, forms, locales and publishing
31
+ states. Compare with the project's pages, collections and entries (`list_pages`,
32
+ `list_content_models`, `get_content_structure`). Note where a value is shared, where it is an
33
+ instance override, and where one entry owns it outright.
34
+
35
+ ## 2. Pages, collections and entries
36
+
37
+ A page is `singleton` (one page and its own content: home, about, a legal page) or `dynamic`
38
+ (one template page whose collection entries each render at the page's path plus the entry slug).
39
+ Choose by repetition: a shape that repeats per item is a collection; a one-off is a singleton,
40
+ even when it lists items, because the list page reads the collection. A dynamic page is its
41
+ collection: the model and its entries both point at the page (`content_models.page_id`,
42
+ `content_entries.page_id`), so never create a separate page per entry. A dynamic page made with
43
+ `create_page` takes its route pattern and its collection through `update_page { dynamic:
44
+ { previewPath, modelId } }`: `/blog/{slug}`, and an existing collection with no page yet.
45
+
46
+ A listing elsewhere (latest posts on home, related posts on an article) is a `collection` block
47
+ with `source: { modelId }`, never copied entries. It also takes `filters` (max 5, `{ field, op,
48
+ value }` with the content API operators), `sort` (`-publishedAt,title`), `pinnedIds` (max 10,
49
+ shown first), `excludeCurrent` and `limit` (default 20), beside `order` (`newest` or `oldest`) and
50
+ `emptyText`. `cardComponentId` names the component each card is drawn through; without one a card
51
+ shows the entry's `titleField` (default `title`) and `excerptField`. On a `dynamic` page the block
52
+ lists that page's collection and draws ONE entry on `/<page>/<entrySlug>`, through
53
+ `detailComponentId` when set. BetterCMS-rendered pages read it on every request;
54
+ a route the repo builds does not draw this block, so list there with the content API.
55
+
56
+ | Source pattern | BetterCMS destination |
57
+ |---|---|
58
+ | Home, about, contact, legal, editorial landing page | Singleton page with its own content |
59
+ | Blog / case-study / product list URL | A list page plus a separate collection |
60
+ | Repeated records sharing one template | One collection and one dynamic detail page |
61
+ | 404, search, gated page | A page that keeps its access and noindex behaviour |
62
+ | Author, category, tag, company reused by entries | A shared collection, referenced from the entries |
63
+ | Data from an external runtime API | Keep the adapter and its owner; never fabricate entries |
64
+ | Sitewide nav, footer, promo bar | Global Layout, never copied page fields |
65
+
66
+ **Thresholds.** Three or more pages sharing a URL pattern and a card or detail layout are a
67
+ collection. Two are a collection when the set will grow (jobs, case studies). Otherwise they are
68
+ singletons.
69
+
70
+ **Promote repeater items to a collection** when any one holds; keep them inline when none does
71
+ (pricing bullets, a hero proof line, quotes on a one-off campaign page):
72
+
73
+ | Signal | Example |
74
+ |---|---|
75
+ | The item has its own URL | A case-study card linking to its detail page |
76
+ | The same item shows on two or more pages | A testimonial on home and on a product page |
77
+ | It carries facts that must not drift between copies | Price, stock, rating, job status, a person's photo |
78
+ | Another entry points at it | A post's author, an event's speaker |
79
+ | Editors will filter, sort or count the items | Jobs by team, events by date |
80
+
81
+ ## 3. Four structure layers, four meanings
82
+
83
+ Name the layer every time you say "folder". The answer to "where does X live" differs per layer.
84
+
85
+ 1. **Content sidebar folders** organise the dashboard only: nothing here reads or writes a URL,
86
+ slug, schema, content or publish state. Read with `get_content_structure`, get a proposal from
87
+ `suggest_content_structure`, apply it whole with `set_content_structure` once the user agrees,
88
+ or change one thing with `create_folder`, `rename_folder`, `move_to_folder` or `delete_folder`
89
+ (deleting a folder moves its contents up). `set_content_structure` replaces the whole document:
90
+ send the `version` you read; a 412 `VERSION_CONFLICT` means someone changed it, so read and
91
+ re-apply. It refuses duplicate folder ids, a parent not in the doc, a cycle, and nesting deeper
92
+ than 4. The standard pins the home page and each list page at the top, gives each content
93
+ family one folder named for its detail type (the list page and its collection are two items),
94
+ keeps other standalone pages in Pages (Pages › <language> on a multilingual site),
95
+ taxonomies used by two or more families in
96
+ Shared, and Settings last. Components are not sidebar items. Whether folders show depends on a
97
+ dashboard feature flag, so an empty sidebar is not proof the structure is missing.
98
+ 2. **URL page folders** are part of a page's path unless set not to be included in it. Renaming
99
+ or moving one 301s every affected URL and queues a rebuild: a routing change that needs the
100
+ user's review. They are set in the dashboard, not through the sidebar tools.
101
+ 3. **Component groups** (`component_groups`) organise definitions by family or role
102
+ (Navigation, Hero, CTA, Forms), never by the page that uses them ([components.md](components.md) §8).
103
+ 4. **Media library folders** organise assets (`folderId` on `create_media_upload`) and never
104
+ rewrite a stored content URL.
105
+
106
+ What reads published structure: the sitemap (skips noindex pages), robots.txt (points at the
107
+ sitemap), RSS (dated by `publishedAt`), `llms.txt` (one link per published item).
108
+
109
+ ## 4. Field decisions
110
+
111
+ Before designing a section's fields, find the closest component-kit entry: the chooser by
112
+ category and edge tag is [kit-scenarios.md](kit-scenarios.md), the full list is the
113
+ `bettercms://kit` resource and [kit-catalog.md](kit-catalog.md). The entry's notes are the
114
+ field mapping and its pitfalls the review list; the rules below still decide the storage.
115
+
116
+ - **Model each visual band as a `group` field (or a repeater), or a component instance.** The
117
+ schema's shape decides whether the site can ever be componentized. The componentize lane finds
118
+ a section in a `group`, a repeater, or two or more top-level keys sharing a prefix before `-` or
119
+ `_` (`"hero_title"` + `"hero_subtitle"` → `hero`). A key with NO shared prefix (`title`,
120
+ `intro`) is a lone scalar, refused `NOT_A_SECTION`. Keep ONE separator per band: a `-` family
121
+ and a `_` family never share a component. A `group` is the robust choice, and child keys inside
122
+ it follow the flat-namespace rule (`"hero_title"`).
123
+ - A collection for an entity with its own lifecycle and relationships; a nested group for values
124
+ one page or section owns.
125
+ - A reference for reusable entities and internal links where the model supports it; an external
126
+ URL for links that leave the site.
127
+ - **Resolve references before creating anything.** Build the target collection and its entries
128
+ first, then the model that points at them. Match each source item to an existing entry by
129
+ stable facts (a slug, a product handle, a name plus the start of the quote) and create only what
130
+ is missing. Store the entry id, never an href or a title. A write naming a target that does not
131
+ exist is refused with 400 (ref_missing). Flag an item with no identity (a quote with no name);
132
+ never import it.
133
+ - A repeater for small, same-shaped items (cards, FAQ rows, links, social accounts); a nested
134
+ component or slot when an item has its own layout or variants.
135
+ - **Lists are arrays; tags are taxonomy.** Features, steps, benefits and "what's included" are an
136
+ `array` (`itemType: "text"`), or a repeater when each item carries its own state (included or
137
+ not, an icon). Never a group holding one field. A label set that filters or classifies entries
138
+ is a `tags` field; when each label has its own page, a collection referenced with `multi-reference` (§2).
139
+ - **Never freeze a feed.** A list of entries on the source stays a listing: copying the nine
140
+ visible cards into a repeater stops it updating. On a page BetterCMS renders, that is the
141
+ `collection` block: on the collection's own dynamic page it lists that collection, and with
142
+ `source: { modelId }` it lists any collection on any hosted page (§2). It takes `limit` (1 to
143
+ 200), `order` (`newest` or `oldest`) or `sort`, `filters`, `pinnedIds`, `excludeCurrent` and
144
+ `emptyText`; in an article template a filter value may be `{{field}}` of the entry shown, which
145
+ is a "related" list. Pagination and grouping stay a query in the site's code. Always set the
146
+ empty state, never write card titles or images into the listing, and put "read next" on the
147
+ template once, not on each entry.
148
+ - **`document` is the one long body.** A page schema or a collection may hold it, one at
149
+ most, and only as a top-level field: a group or a repeater cannot contain it. Label it Body. Short formatted copy stays `richtext`.
150
+ - **Model a call to action as a single `button` field.** It stores `{ label, href }` plus the
151
+ optional look keys `variant`, `size`, `arrow`, `icon` and `newTab`, and the site draws it with
152
+ `BcmsButton` ([astro.md](astro.md), [next.md](next.md)). Never a text
153
+ field beside a link field: the pair has nowhere to keep a look, and a new pair in the importer's
154
+ spelling (`hero-span-go` + `hero-a-go-href`) is refused with 400 `cta_pair` on `add_field`,
155
+ `create_page` and model writes. A page that already holds a pair (`primaryCtaText` +
156
+ `primaryCtaHref`) becomes one button with `update_field` `{ pageId, path: "primaryCtaText",
157
+ mergeHref: "primaryCtaHref" }`: each entry's DRAFT gets `{ label, href }` and keeps the old values,
158
+ so code still reading them keeps rendering; the live copy changes on publish. On a route the
159
+ site's app renders at request time the merge is refused (409 `CODE_RENDERED_ROUTE`): change the
160
+ code to draw the CTA with `BcmsButton`, and the next build adds the button field. Inside a component
161
+ a CTA is a `button` block whose `props` one `button` prop targets ([components.md](components.md) §1).
162
+ - A sponsored, untrusted or user-generated link takes `rel` (`nofollow`, `sponsored` or `ugc`) on the
163
+ button, or a `link` field value written as `{ href, rel?, newTab? }`; code that opts into the object reads `.href`.
164
+ - A `date` field with a time takes `timezone` (`site` or an IANA name such as `Europe/Paris`) in its config;
165
+ values must then end in `Z` or an offset, and a bracketed zone suffix is dropped on save.
166
+ - Never model SEO columns ([seo.md](seo.md)), Form questions (they live on the Form document) or
167
+ Global Layout values as content fields.
168
+ - Keep source ids, slugs, locale, status and previous URLs as migration metadata, stable across
169
+ re-imports.
170
+ - Lock a field whose value another system owns (a store, job board or feed id): `readOnly: "developer"` with a `lockReason`.
171
+ Only schema access may then change the value or the lock; other writes are refused (403 `FIELD_LOCKED`) unless they send the stored value back.
172
+ - **Prefer a `select` over a `boolean` for a state that may grow** (`draft | review | live`, not
173
+ `isLive`). Why: no tool retypes a field, so a third state later costs a new field and a value
174
+ migration (§6).
175
+ - Choose field types before the first write. A `link` accepts any scheme-less href (`/about`,
176
+ `about.html`, `#section`) and `http(s):`, `mailto:`, `tel:`; every other scheme is refused.
177
+ Choose the type from what the rendered value is, not habit ([portable-text.md](portable-text.md)).
178
+
179
+ ## 5. Editor labels and order
180
+
181
+ **Name for meaning, not look.** Before writing a key or label, ask: would the name survive a
182
+ redesign? `ctaText`, `authorBio` and `features` pass; `blueButton`, `leftColumn` and `bigHeading`
183
+ fail, because the next redesign makes them lie.
184
+
185
+ Short labels named for what the editor sees: Heading, Body, Primary button, Label, Link, Image,
186
+ Alt text — or the name the site's code already uses (`cents` → Cents), never the copy itself
187
+ (`.00`). An import names every field it derives the same way, by its section and its role
188
+ there: "Hero headline", "Hero intro", "Hero primary button", "Pricing plans", "Footer link 2
189
+ URL", "Header logo"; a list's columns by their role in the row ("Title", "Description", "Link
190
+ URL"). A role is numbered only where it repeats in one section, and labels are unique on a page.
191
+ Keys never change. Rename a label where the site's code has a better word: a name you give is
192
+ kept on every later release, and one the import gave follows the import. Most fields need no help line from you (below). Group by what renders
193
+ together, not by the DOM or the source file: core content first, optional appearance second, technical fields hidden. Repeatable items need a meaningful preview label (the FAQ question, the
194
+ button label), reorder, and stable item ids. An empty optional field removes its element without
195
+ leaving a gap; a long value wraps, and clamps only where the design intends it.
196
+
197
+ ### Writing help text
198
+
199
+ Most fields get no help. The platform writes a line only where one of the four reasons below
200
+ holds and leaves every other element with none, so leaving `helpText` out is the default, not a
201
+ gap. It says so: `helpText: { source: "auto" }` in the response, `helpSource: "auto"` on the
202
+ stored field. Text you send is stored as yours (`helpSource: "agent"`) and is never rewritten;
203
+ `editor_setup` rewrites only its own lines and never a person's.
204
+
205
+ Ask one question per field: does the editor need this line to type the right value? Write your
206
+ own `helpText` (on a field or a prop) only for one of four reasons, written as one short plain
207
+ sentence under 90 characters, with no semicolon, no dash and no figure taken from the page.
208
+
209
+ | Reason | Good |
210
+ |---|---|
211
+ | A format or unit the label cannot show | Price in cents. · Length in minutes. · A .vtt caption file. |
212
+ | Where the value shows, when the canvas cannot show it | Shown in search results and link previews. · Read by screen readers, not shown. |
213
+ | A real constraint or consequence | Changing this breaks existing links. · Must be later than the start date. |
214
+ | A fallback | Leave empty to use the page title. |
215
+
216
+ Everything else is left out:
217
+
218
+ | Never | Bad | Do instead |
219
+ |---|---|---|
220
+ | The label or the value again | Heading: "The section's main heading." · `e.g. "Starter Kit"` | Nothing. The API refuses help that echoes the value |
221
+ | Coaching the writing | "Keep it short." · "Two to four words works best." · "Start with a verb." | A length is a counter: a number in `config` (`"maxLength": 60`, §6) |
222
+ | An example of the value | Badge: "e.g. Most popular" | A `placeholder` where the control takes one (component props do) |
223
+ | Strategy | Secondary button: "A lower-commitment option that builds trust." | Nothing. The label says which button it is |
224
+ | Internal notes | "Renders as h2." · "Label and link are one unit." · a ticket or decision id | Keep them in the code or the report, never in the form |
225
+ | Generic good practice | "Add alt text for screen readers." on every image | Nothing |
226
+
227
+ One sentence, plain words, sentence case. If more than about one field in five carries your own
228
+ line, you are over-writing. A component, a collection and a Layout Section (`update-section`)
229
+ still take a `description` that says what the thing is ("Hero with a heading, supporting text,
230
+ two buttons and an image"), never its name again.
231
+
232
+ `get_next_steps` lists `help-text-missing` for an element the editor setup has not settled yet, or
233
+ one whose line only echoes its label, and `get_binding_report` and `get_site_composition` carry the
234
+ coverage as `help`. The platform writes a line only for the four reasons above and leaves most
235
+ elements with none; clear the finding by running `editor_setup`, or by writing one of the four
236
+ kinds yourself, never with filler.
237
+
238
+ **Config keys.** Write only the `config` keys a field type documents (`maxLength`, `min`/`max`,
239
+ `options`, `itemType`, `zones`, a rich text's `accept`/`inline`): any other key sends that field to
240
+ the old classic editor. The import's own keys (`attr`, `numeric`, `units`, `band`, `seeded`, …) are the
241
+ server's: both MCP surfaces drop them from what you write.
242
+
243
+ ## 6. Changing a schema safely
244
+
245
+ A schema write alone is not a release. Before one, read the model with `get_content_model`, a few
246
+ sample entries, the component definitions that touch it and the frontend readers, and write a
247
+ before/after field map: key, type, label, required, default, validation, editor group, and what
248
+ happens to existing values.
249
+
250
+ 1. **Guard concurrent edits.** Pass the model's `optimisticVersion` as `ifMatch` to
251
+ `update_content_model`. A 409 means it changed since you read it: re-read and re-plan, never
252
+ resend. Omitted, the last write wins, except on the editor-chrome args, which refuse without
253
+ it (428).
254
+ 2. **Know what the MCP can do.** `add_field` (a model's `modelId` or a page's `pageId`) appends
255
+ and never overwrites or retypes. `update_field` changes ONE existing field, addressed by a dot
256
+ path through its groups and repeaters (`hero.title`): its label, help text, `required`,
257
+ validation `config` (merged by key; null deletes one), `showIf`, `ui` or placement, written at
258
+ the version it read. A retype needs `allowRetype: true`, because stored values that do not fit
259
+ the new type are lost; a key rename needs the migration in step 3. `update_content_model` edits
260
+ the model's name, slug, URL pattern and fieldsets, and removes model fields only with
261
+ `removeFields` (step 3). The home page cannot be deleted, and changing a local type file is not
262
+ a schema change.
263
+ 3. **Retire or rename a field in stages, never in one write.** Renaming a key re-addresses its
264
+ content: `update_field` with a new `key` is refused with 409 and the count of entries holding
265
+ the old key, unless `allowFieldRemoval: true` discards their values. So a rename is a new field
266
+ plus a migration:
267
+ 1. Relabel the old field "Old title (Deprecated)" and set help text naming its replacement
268
+ (`update_field` `label` and `helpText`).
269
+ 2. Move it into a `collapsed: true` fieldset so editors stop reaching for it
270
+ ([editor-chrome.md](editor-chrome.md) §2).
271
+ 3. Add the replacement (`add_field`, with `modelId` or `pageId`) with the final type and name.
272
+ 4. Migrate values in code, draft first, and have the frontend read the new key.
273
+ 5. Remove the old one. Page fields: `remove_page_fields` (dry run by default) returns
274
+ `keysWithContent` and writes nothing; read it, then run it with `dryRun: false` and
275
+ `discardContent: true` only for keys you migrated. Model fields: `update_content_model` with
276
+ `removeFields: ["oldKey"]` and `allowFieldRemoval: true`, sent with the `ifMatch` of your
277
+ read. It has NO dry run and discards that key's value in every entry, so count the entries
278
+ holding it first (`list_content_entries` with `modelId`: `meta.total` is the count, `page`
279
+ walks past the 200-row `limit`). Top-level keys only; an unknown key refuses the
280
+ whole call; a page's own fields go through `remove_page_fields`. Both discard data: removal
281
+ is the last step, with the user's yes.
282
+ Why: every stage before 5 is reversible, and a site built against the old key keeps rendering
283
+ until its next release reads the new one.
284
+ 4. **Put numeric limits in `config` as numbers** (`"maxLength": 60`), with `update_field` on a
285
+ field that exists. A numeric string is converted on save; an impossible value is refused with
286
+ `FIELD_CONFIG_INVALID`.
287
+ 5. **Check existing records before tightening validation.** Backfill or give a real default
288
+ before making a field required; keep a source-to-target map and a snapshot for any high-volume
289
+ transform.
290
+ 6. **Validate on a draft.** Entries written through the MCP pass one write path (request schema,
291
+ normalisation, per-field checks, uniqueness), and a write failing any step stores nothing.
292
+ Publishing copies the draft to live, so a draft that passes is the value that ships.
293
+ 7. **Change a component family together** (same prop keys and types on every variant;
294
+ [components.md](components.md) §1 and §3).
295
+ 8. **Regenerate types** with `bettercms-codegen` in every consuming site after any model or field
296
+ change, commit the output if the repo commits it, and run that repo's build and typecheck with
297
+ the script it already has. Templates typed against an old schema compile and read `undefined`.
298
+ 9. **Verify** the editor's grouping and controls, one draft edit per changed field class, the
299
+ preview binding, the static or headless reading path, and the public output once a release is
300
+ authorized.
301
+
302
+ Content models, frontend types, component definitions and live content have separate lifecycles;
303
+ there is no single "deploy schema" action. Production publication stays behind the user's
304
+ authorization and the component readiness and Visual Approval gates.
305
+
306
+ **Evidence to return:** model and field ids, the version you wrote against, migrated record
307
+ count, codegen and build result, one editor check and one rendered-page check, warnings, and any
308
+ change the platform could not make (an open dependency, never a success).
309
+
310
+ ## 7. Reading a schema written in the V2 language
311
+
312
+ A plan or Kit sample may arrive in the V2 schema language. It is an intermediate model only:
313
+ production cannot store it. Translate before the first write.
314
+
315
+ - **Keys.** V2 names are camelCase and scoped per object (`hero.heading`). Production keys are
316
+ flat, one namespace per model, prefixed by section: `"hero_heading"`.
317
+ - **Labels and help.** V2 `title` is `label`; V2 `description` is `helpText` (§5 decides whether
318
+ it is written at all).
319
+ - **Limits.** V2 `validation` becomes numbers in `config` (§6 step 4).
320
+
321
+ | V2 type | Model field (`create_content_model`, `add_field`) | Component prop (`create_component`) |
322
+ |---|---|---|
323
+ | `string` | `text` with `richText: false` | `text` |
324
+ | `text` | `longtext` | `text` |
325
+ | `richtext` | `richtext` for section copy; `document` for the one body | `richtext` |
326
+ | `number` | `number` | `number` |
327
+ | `rating` | `number` with `config.min`, `max`, `step` and `ui.control: "rating"` (stars) | `number` |
328
+ | `boolean` | `boolean` | `boolean` |
329
+ | `slug` | `slug` (an entry's own slug stays on the entry) | `text` |
330
+ | `url`, `link` | `link` | `link`; `url` for a media or embed address |
331
+ | `email`, `phone` | `email`; a phone number is `text` with `richText: false` (`phone` opens only in the classic editor) | `text` |
332
+ | `date`, `datetime` | `date`, `datetime` | `date` (`YYYY-MM-DD`); a datetime stays `text` |
333
+ | `color` | `color` | `color` |
334
+ | `select` | `select` | `select` with `config.options` |
335
+ | `multiselect` | `multiselect` with `config.options` (a closed `string[]` choice list; `min`/`max` count the choices) | `multiselect` with `config.options` |
336
+ | `tags` | `tags` (a `string[]`; `config.options` are suggestions, `allowCustom: false` closes the list); a taxonomy with its own pages is a collection plus `multi-reference` | `tags` |
337
+ | `image` | `image` | `image` |
338
+ | `file`, `video` | `file`; a hosted video URL is a `link` | `file`; `url` for a hosted video |
339
+ | `icon` | `icon` (an icon name or an uploaded image) | `icon` (an icon name or an image) |
340
+ | `button` | `button` | `button`: one prop targeting the `props` of a `button` block (§4) |
341
+ | `reference` | `reference`; `multi-reference` for many | none: keep it on the model |
342
+ | `code` | `longtext` | `text` |
343
+ | `json` | `json` | `text` |
344
+ | `geopoint` | `location` (`{ lat, lng, label? }`) | `group` of two `number` props |
345
+ | `table` | `table` with `config.mode: "data"` (one data table: the editor's Columns and Rows drill-in); a Portable Text table inside `richtext` or `document` when it belongs to running copy | `table` with `config.mode: "data"` |
346
+ | `object` | `group` | `group` |
347
+ | `array` of objects | `repeater`; item limits in `config.minItems` / `config.maxItems` (the editor shows "max N") | `table` |
348
+ | `array` of strings | `array` (`itemType: "text"`) | `table` |
349
+ | `array` of several item shapes | one component per shape, placed as sections; on a model, a `repeater` whose rows carry each shape's fields (`modular` opens only in the classic editor) | separate components |
350
+ | `slot` | components placed on the page (componentize); never a `sections` or `component-ref` field, which the page form cannot edit | `slot` (one instance; a list with `config.multiple`) |
351
+ | a form `reference` | a `form` block inside the section | `form` |
352
+
353
+ V2 features with no production shape are dropped and reported, never faked: a per-cell table
354
+ type, per-breakpoint values (only `style.hideOn` differs by breakpoint) and per-field
355
+ localization. Two have a shape now: typed table columns are a `repeater` with one field per column
356
+ (never `ui.layout: "table"`, which the editor cannot draw), and a slot with several children is a `slot` prop with `config.multiple`
357
+ ([components.md](components.md) §1).
358
+
359
+ | The plan says | Production word |
360
+ |---|---|
361
+ | Block | Component (the type); section or component instance (the placed band) |
362
+ | Site-wide block, global | Global Layout (`update_layout`) |
363
+ | CMS item | Entry |
364
+ | Collection page | An entry shown through its collection's dynamic page |
365
+ | Static page | `singleton` page |
366
+ | Collection | Content model plus its `dynamic` page |
367
+ | Variant, layout | Components sharing one `sectionType` ([components.md](components.md)) |
368
+ | Listing | `collection` block (§4) |
369
+ | Picks | `multi-reference` |
370
+ | Polymorphic array, modules | one component per shape |
371
+
372
+ Tools and reports use the production word. Labels and help an editor reads never say
373
+ "document": the `document` field is labelled Body.
374
+
375
+ ## Acceptance
376
+
377
+ - Every imported route and collection has one clear home; one dynamic template per collection.
378
+ - Sidebar moves keep every public route and record id; URL changes carry explicit redirects
379
+ (`create_redirect`).
380
+ - Repeated data is modelled once, with references or reusable components.
381
+ - Editors find and understand the fields without reading source code.
382
+ - Model changes compile in each consuming site and keep the published output unchanged.