@bettercms-ai/mcp 0.68.2 → 0.69.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.
@@ -117,12 +117,15 @@ resources: `bettercms://skill/bettercms` for this file, and each reference under
117
117
  | Set up or resume a project | [getting-started.md](references/getting-started.md) | first contact, or "Continue BetterCMS setup" | setup, resume, onboarding, connect, import my site |
118
118
  | Finish an imported or converted site | [finish-site.md](references/finish-site.md) | a convert or build job; "make it fully editable"; the gap ledger | make it editable, finish, gap ledger, convert |
119
119
  | Move content out of another CMS (Cosmic, Contentful, Sanity, Strapi, Prismic, WordPress…) | [external-content.md](references/external-content.md) | a CMS SDK in package.json; `envWarning` on import; pages that render empty without a third-party key | Contentful, Sanity, Strapi, envWarning, missing key, empty pages |
120
- | Pages vs collections, folders, fields, schema changes | [content-modeling.md](references/content-modeling.md) | modelling a route family; a model write returns 409 | schema, model, collection, field, folder, rename, remove field |
120
+ | Read a source site before modelling it | [detection.md](references/detection.md) | converting from a URL or built HTML; deciding what a band, a repeat or a value is | detect, classify, section, repeat, global, carousel clone, hidden state |
121
+ | Pages vs collections, folders, fields, schema changes | [content-modeling.md](references/content-modeling.md) | modelling a route family; a model write returns 409 | schema, model, collection, field, folder, rename, remove field, listing, filter, locked field |
121
122
  | Dock layout: groups, rows, tabs, icons, item names, showIf order | [editor-chrome.md](references/editor-chrome.md) | `get_next_steps` lists a `dock-*` rule; the `field-ui` prompt | dock, fieldset, tab, group, itemLabel, showIf, field-ui |
122
123
  | A custom input for one field in the dock: when, the user's approval, the contract | [dock-input.md](references/dock-input.md) | `get_next_steps` lists `dock-input-candidate`; a 409 `input_needs_confirmation` | custom input, ui.input, dock input, confirmed, swatch |
123
- | What the server does and does not check on a write | [write-limits.md](references/write-limits.md) | before relying on a limit; reading a refused write | validation, maxLength, unique, 400, refused write |
124
- | Plan a site by business type | [site-blueprints.md](references/site-blueprints.md) | portfolio, agency, corporate, SaaS/AI or ecommerce IA | new site, IA, sitemap plan, portfolio, SaaS, ecommerce |
124
+ | What the server does and does not check on a write | [write-limits.md](references/write-limits.md) | before relying on a limit; reading a refused write; a bulk load or re-run | validation, maxLength, unique, 400, refused write, dry run, externalKey, upsert, re-run |
125
+ | Plan a site by business type | [site-blueprints.md](references/site-blueprints.md) | the IA of a known site type, with its collections, forms and structured data | new site, IA, portfolio, agency, corporate, SaaS, ecommerce, directory, blog, docs, restaurant, events, course, jobs, real estate, nonprofit, multi-brand |
125
126
  | Components, props, variants, component publish codes | [components.md](references/components.md) | creating, swapping, publishing or validating a component | component, prop, variant, sectionType, publish component |
127
+ | One section, source HTML to bound template: hero, feature grid, pricing table, testimonials, FAQ | [worked-examples.md](references/worked-examples.md) | you want the whole path for one section in production shapes | example, hero, pricing, FAQ, testimonial, source to schema |
128
+ | The same for an article entry, navigation in the Global Layout, tabs holding columns | [worked-examples-advanced.md](references/worked-examples-advanced.md) | a collection entry with a body, site chrome, or nested blocks | example, blog post, document body, mega menu, tabs, columns |
126
129
  | Componentize a derived site | [componentize.md](references/componentize.md) | `get_site_composition` shows pages made of fields only | componentize, sections, fields-only, get_componentize_plan |
127
130
  | Click-to-edit attributes, addresses, declared mode | [bindings.md](references/bindings.md) | `grep -rl "data-bcms-" src` finds files; a value the editor cannot select | data-bcms-field, click to edit, binding report, stega, declared |
128
131
  | Wiring, env, hosting modes, media host, snapshots | [frameworks.md](references/frameworks.md) | `package.json` lists an adapter; `bcms-content.json` exists | hosting, headless, env, codegen, types, revalidate, snapshot |
@@ -133,7 +136,8 @@ resources: `bettercms://skill/bettercms` for this file, and each reference under
133
136
  | Brand kit, tokens, fonts, logo, images, icons | [media-and-brand.md](references/media-and-brand.md) | design fidelity, alt text, a logo or favicon change | brand, tokens, fonts, logo, image, alt text, favicon |
134
137
  | Forms, menus, accordions, modals, consent | [forms-and-interactions.md](references/forms-and-interactions.md) | attaching or verifying a form; an interactive section | form, submission, Turnstile, modal, accordion, menu |
135
138
  | Languages, locale prefixes, translation, hreflang | [localization.md](references/localization.md) | the build serves `/fr`-style prefixes; `locale-unvarianted` in a binding report | language, locale, /fr, translate, hreflang, i18n |
136
- | SEO and AEO | [seo.md](references/seo.md) | meta, JSON-LD, sitemap, robots, redirects, `list_seo_issues` | SEO, meta, JSON-LD, sitemap, robots, OG image, redirect |
139
+ | SEO and AEO | [seo.md](references/seo.md) | meta, JSON-LD, sitemap, robots, redirects, AI crawlers, `list_seo_issues` | SEO, meta, JSON-LD, schema.org, sitemap, robots, OG image, redirect, 410, llms.txt |
140
+ | Accessibility: alt text, form labels, contrast, the audit codes and the manual checks | [accessibility.md](references/accessibility.md) | writing alt text; building a form; before reporting a site done | accessibility, WCAG, alt text, decorative, label, contrast, captions |
137
141
  | Page families from data | [programmatic-seo.md](references/programmatic-seo.md) | a keyword list or CSV should become pages | programmatic SEO, CSV, dataset, landing pages, glossary |
138
142
  | Publish, deploy, roll back, reconcile a re-import | [publishing.md](references/publishing.md) | a release; a code push that conflicts with CMS edits | publish, deploy, rollback, sync, webhook, schedule, automation |
139
143
  | Read-only launch review | [site-review.md](references/site-review.md) | "is this ready to launch?" without fixing | launch review, audit, is it ready, QA |
@@ -0,0 +1,171 @@
1
+ ---
2
+ title: "Accessibility"
3
+ description: "WCAG 2.2 AA rules for converted and built sites: alt text, media, forms, contrast, links, motion, the audit codes, and the manual check list to run before reporting done."
4
+ ---
5
+
6
+ # Accessibility
7
+
8
+ **Outcome.** A converted or built site meets WCAG 2.2 AA where BetterCMS lets an agent control
9
+ it, and the report says plainly what was checked and what was not.
10
+
11
+ `list_seo_issues` reports the checkable items as `a11y.` codes, from the stored blocks of
12
+ published pages: image alt missing, h1 missing, several h1, an h1 inside body text, a skipped
13
+ heading level, generic link or button text, a video with no transcript. Each names the page and
14
+ its block ids. It cannot see inside a `component` or `collection` block, an entry's fields, or a
15
+ route the repo's code renders, and a clean result says nothing about contrast, focus order,
16
+ keyboard use or form behaviour. Those stay manual: read the rendered page, then report what you
17
+ saw. Never write "WCAG compliant"; write "checked against this list, with these exceptions".
18
+
19
+ Related: [media-and-brand.md](media-and-brand.md) (assets, SVG, video, focal point),
20
+ [forms-and-interactions.md](forms-and-interactions.md) (form fields, interactive patterns),
21
+ [portable-text.md](portable-text.md) (heading levels, lists, tables), [seo.md](seo.md).
22
+
23
+ ## Contents
24
+
25
+ - [Where each fact is stored](#where-each-fact-is-stored) · [Alt text](#alt-text)
26
+ - [Video and audio](#video-and-audio) · [Forms](#forms) · [Structure and links](#structure-and-links)
27
+ - [Colour, contrast and motion](#colour-contrast-and-motion) · [Check list before done](#check-list-before-done)
28
+ - [What to report](#what-to-report)
29
+
30
+ ## Where each fact is stored
31
+
32
+ | Fact | Lives in | Write with |
33
+ |---|---|---|
34
+ | Alt text of an image | `altText` on the image value, and on the media asset | the value in the entry or page write; `update_media` `altText` for the asset |
35
+ | Caption | `caption` on the asset | `update_media` `caption` |
36
+ | Transcript (video or audio) | the asset, published wherever the file is used | `update_media` `transcript` (at most 100,000 characters) |
37
+ | Decorative image | `decorative` on the asset; an empty `altText` on one value | `update_media` `decorative: true`; the value in the entry or page write |
38
+ | Captions track (`.vtt`) | not stored by any tool | report as missing; the owner adds it |
39
+ | Form label, required mark, help text, consent wording | the Form's fields | `create_form` / `update_form` |
40
+ | Icon-only link or button label | the element's `aria-label` or visually hidden text in the code, or the component prop that feeds it | code, or the component prop |
41
+ | Page language | the page or entry `locale` | see [localization.md](localization.md) |
42
+ | Page title | `metaTitle` | see [seo.md](seo.md) |
43
+
44
+ An image value is an object, never a bare string. Alt on the asset is the default; alt on the
45
+ value is for this placement only. Set both when the same file reads differently in two places.
46
+
47
+ ## Alt text
48
+
49
+ An asset that is decoration wherever it is used takes `update_media` `decorative: true`: the
50
+ audit stops asking for its alt text. An image VALUE has no such flag; an empty alt marks that one
51
+ placement as decoration. A missing alt and a deliberately empty alt look the same to the next
52
+ reader, so list every empty alt you left on purpose.
53
+
54
+ | Image | Alt |
55
+ |---|---|
56
+ | Informative | What it shows that matters in this place. Never the file name, "image of", "photo" or "logo" |
57
+ | Logo | The company name. A linked logo says where it goes ("Caldera home") |
58
+ | Person | "Portrait of {name}" |
59
+ | Product | The product name and what the photo shows |
60
+ | Chart or diagram | One sentence with the trend or the point; put the numbers in real text nearby |
61
+ | Image of text (size guide, menu) | Rebuild it as a table or list; keep the image only as a diagram |
62
+ | Background image that carries meaning | Make it an image with alt, or mark it decorative |
63
+ | Decorative (rule, flourish, duplicate of nearby text) | Empty alt; `decorative: true` on the asset when it is never informative |
64
+
65
+ - Copy the source's real alt when it is good. Rewrite "image of ..." and file-name alt to
66
+ describe the image.
67
+ - Missing or junk alt on a meaningful image: write it from the visible context and flag
68
+ `alt-written` for review. With no context to draw on, leave it empty and flag `alt-missing`.
69
+ - An empty alt is right for decoration and wrong on a portfolio, product or team photo. Flag
70
+ every empty alt on a content image.
71
+ - Generated images (`generate_image`, `transform_image`) always get `altText`.
72
+
73
+ ## Video and audio
74
+
75
+ - Never invent captions or a transcript. If the source has none, leave the field empty, and
76
+ report it.
77
+ - A transcript you produced from your own speech-to-text goes on the asset with `update_media`
78
+ `transcript`. It then publishes on the page, the Markdown twin, llms-full.txt and the delivery
79
+ API. BetterCMS does not generate transcripts.
80
+ - Captions are `.vtt` files and no BetterCMS field stores them. State "captions missing" in the
81
+ report for any video with speech.
82
+ - A video needs a poster image and autoplays only muted.
83
+ - An embed needs a `title` naming the provider and what it shows. Write one when missing.
84
+ - An animation, Lottie or 3D scene needs a poster or fallback image and a text label, and must
85
+ stop for reduced motion.
86
+
87
+ ## Forms
88
+
89
+ Forms are Form records, so the rules apply to the form's fields ([forms-and-interactions.md](forms-and-interactions.md)).
90
+
91
+ - Every field has a `label`, which the renderer shows as a visible label. A placeholder is not a
92
+ label. If the source used placeholders only, write a label from each, and flag it.
93
+ - Mark required fields with `required: true`. The hosted renderer adds an asterisk; say in the
94
+ field's `helpText` or the form intro that starred fields are required, so colour or a symbol is
95
+ never the only signal.
96
+ - `helpText` carries the format or constraint ("Use the number on your invoice"). A refused
97
+ submission returns `issues[].field` and `issues[].rule`; show the field name and the fix in the
98
+ form's error copy.
99
+ - Consent: a `consent` field starts unticked and must be ticked to submit. Import a source's
100
+ pre-ticked box as unticked, and flag it. Import confirmshaming copy as written, then flag it
101
+ with a neutral rewrite.
102
+ - `autocomplete` is not a field setting. The hosted and Next renderers set it where they know
103
+ it (phone). For a self-hosted form with its own markup, keep the source's values (`email`,
104
+ `given-name`, `tel`, `postal-code`).
105
+ - A form result (success, error) must be announced. The Next `BcmsForm` uses a live region; check
106
+ that a custom form does too.
107
+
108
+ ## Structure and links
109
+
110
+ - Heading levels follow the outline: one H1, no skipped level. How a heading level is chosen and
111
+ stored is in [portable-text.md](portable-text.md). Size is a style token, never a reason to
112
+ pick a level.
113
+ - Lists, tables and quotes use their real Portable Text blocks, never lines of text with
114
+ bullets or spaces.
115
+ - Link text says where it goes. Replace "click here", "read more" and "learn more" on their own
116
+ with the target ("Read the care guide"). Where the card layout forces a short visible label,
117
+ add the target as visually hidden text or an `aria-label` in code.
118
+ - Icon-only links and buttons (social icons, close, menu, carousel arrows) have an `aria-label`
119
+ or hidden text. A dot or a map pin that links needs one too. Draft the label and flag it; never
120
+ leave it empty.
121
+ - Navigation, footer and announcement bar belong in Global Layout (`update_layout`). Check the
122
+ skip link, the landmark roles (`header`, `nav`, `main`, `footer`) and that the mobile menu
123
+ opens by keyboard and returns focus.
124
+ - Set the page `locale`. Mark a foreign phrase inside text with `lang` in code.
125
+ - DOM order is reading order; never reorder with CSS. Targets are at least 24 by 24 CSS pixels.
126
+
127
+ ## Colour, contrast and motion
128
+
129
+ - Contrast: 4.5:1 for body text, 3:1 for large text (24px, or 19px bold) and for borders of
130
+ controls and icons that carry meaning. Check each theme and each variant the editor can pick,
131
+ including text over photos.
132
+ - Take colour from brand tokens and section styles, never from a copied hex
133
+ ([media-and-brand.md](media-and-brand.md)). A brand colour that fails contrast is reported
134
+ with the pair and ratio; never change the kit unasked.
135
+ - Information is never colour alone: a status dot has a word beside it; a check mark has
136
+ "Included" beside it.
137
+ - Respect reduced motion: scroll stories stack, parallax goes still, Lottie shows its fallback.
138
+ - Carousels, marquees and auto-advancing tabs need a pause control and stop when focused.
139
+
140
+ ## Check list before done
141
+
142
+ Run `list_seo_issues` first and fix every `a11y.` code (rows 1, 3, 4 and 5 for hosted pages).
143
+ Then run these on the rendered pages (desktop and a 390 px mobile width), not on the stored data.
144
+ Cover the home page, one page per template, one entry per collection, and every form page.
145
+
146
+ | # | Check | How |
147
+ |---|---|---|
148
+ | 1 | Every informative image has alt; every empty alt is intentional | read each image's `altText` in the page output; list empties |
149
+ | 2 | No alt is a file name, "image of" or "logo" | scan the same list |
150
+ | 3 | Video and audio: transcript on the asset, captions reported | `get_media`, then the player |
151
+ | 4 | One H1, no skipped heading levels | outline the headings in order |
152
+ | 5 | Link text is meaningful out of context | list all links; fix vague ones |
153
+ | 6 | Icon-only controls have a name | inspect each; draft and flag |
154
+ | 7 | Every form field has a visible label, required is stated in text, consent is unticked | `get_form`, then the page |
155
+ | 8 | A refused submission names the field and the fix | send one bad test submission |
156
+ | 9 | Contrast meets 4.5:1 and 3:1 in every theme | measure real colour pairs |
157
+ | 10 | Whole page works by keyboard: tab order, visible focus, menu, dialog, accordion, tabs | tab through it |
158
+ | 12 | Motion stops or stills under reduced motion | emulate the preference |
159
+ | 13 | Page has a title and a language | `list_seo_issues` for title; check `locale` |
160
+ | 14 | Embeds have a title; moving content can pause | inspect each |
161
+
162
+ ## What to report
163
+
164
+ Report accessibility as its own short block in the final report:
165
+
166
+ - Checked: the pages and the rows above you ran.
167
+ - Written: drafted alt (`alt-written`), labels and transcripts. Left empty on purpose: decorative
168
+ images, and each asset marked `decorative`.
169
+ - Open: `alt-missing`, captions missing, contrast failures with the measured ratio, controls you
170
+ could not test (a third-party embed, a payment step).
171
+ - Not claimed: conformance. No tool certifies it; the audit covers stored blocks only.
@@ -10,23 +10,39 @@ Doctrine (components vs collections, section anatomy, variants): §3 and §4 of
10
10
  reference is what the code does with what you write. Componentizing a derived site end to end:
11
11
  [componentize.md](componentize.md).
12
12
 
13
+ ## Contents
14
+
15
+ - [1. The prop contract](#1-the-prop-contract) · [2. Instances](#2-instances) ·
16
+ [3. Sections, families and variants](#3-sections-families-and-variants) ·
17
+ [4. Publishing](#4-publishing) · [5. Rendering components in your site](#5-rendering-components-in-your-site)
18
+ - [6. Preview source](#6-preview-source) · [7. The values popover](#7-the-values-popover-dashboard) ·
19
+ [8. Groups](#8-groups) · [9. Verify](#9-verify)
20
+
13
21
  ## 1. The prop contract
14
22
 
15
23
  A component declares `props`. Each prop is `{ key, label, type, target: { blockId, path },
16
24
  defaultValue? }`.
17
25
 
18
- - `type` is one of exactly 11: `text`, `richtext`, `image`, `url`, `boolean`, `number`,
19
- `select`, `group`, `table`, `slot`, `form`.
26
+ - `type` is one of exactly 18: `text`, `richtext`, `image`, `url`, `boolean`, `number`,
27
+ `select`, `group`, `table`, `slot`, `form`, then `date`, `color`, `file`, `link`, `icon`,
28
+ `multiselect` and `button`.
29
+ - The last seven store what the same-named model field stores: `date` is `YYYY-MM-DD`, `color` a
30
+ hex, `file` `{ url, name?, size? }`, `link` an href string or `{ href, rel?, newTab? }`, `icon` an icon name or an image
31
+ value, `multiselect` a `string[]` drawn from `config.options`. A bad `defaultValue` is a 400.
32
+ - **A `button` prop is one unit.** Its value is `{ label, href, variant?, size?, arrow?, icon?,
33
+ newTab? }` and its `target.path` is `props` on a `button` block. For a new component never
34
+ declare a label prop plus a url prop: the two drift apart on the first edit.
20
35
  - `target.blockId` + `target.path` must point at a real block and property inside the
21
36
  component's own `blockJson`. Why: rendering copies each prop's value to that target; a target
22
37
  that does not exist renders the default and the edit goes nowhere.
23
38
  - Every prop takes `helpText` and the component a `description` ("Hero with a heading,
24
39
  supporting text and two buttons."). Leave them out and the platform writes them; write your
25
40
  own by the rules in [content-modeling.md](content-modeling.md) ("Writing help text").
41
+ - A prop takes `showIf` (`{ match, rules: [{ field, op, value? }] }`): hidden until SIBLING props match. `op` is `eq`, `neq`, `in` or `isSet` (no `value`); model fields take the same four.
26
42
  - `update_component` replaces the whole `props` array. Read with `get_component`, change, write
27
43
  all props back.
28
- - `select` needs `config.options`; `slot` holds one nested component instance; `form` holds one
29
- Form document.
44
+ - `select` and `multiselect` need `config.options`; `slot` holds one nested component instance;
45
+ `form` holds one Form document.
30
46
  - `slot` and `form` are reference-shaped: `target.path` is `props` (the whole envelope) on an
31
47
  empty `component` or `form` block, and their values are `{ componentId, overrides? }` and
32
48
  `{ formId }`. Neither may appear inside a `group` or `table` row.
@@ -14,6 +14,16 @@ Doctrine: §1–§5 and §14 of `bettercms://playbook/schema` (schema shape and
14
14
  [write-limits.md](write-limits.md). Planning a whole site by business type:
15
15
  [site-blueprints.md](site-blueprints.md).
16
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
+
17
27
  ## 1. Read the site before modeling
18
28
 
19
29
  Inventory route files and rendered routes, source data schemas, repeated records, page templates,
@@ -33,6 +43,12 @@ collection: the model and its entries both point at the page (`content_models.pa
33
43
  `create_page` takes its route pattern and its collection through `update_page { dynamic:
34
44
  { previewPath, modelId } }`: `/blog/{slug}`, and an existing collection with no page yet.
35
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`. BetterCMS-rendered pages read it on every request;
50
+ a route the repo builds does not draw this block, so list there with the content API.
51
+
36
52
  | Source pattern | BetterCMS destination |
37
53
  |---|---|
38
54
  | Home, about, contact, legal, editorial landing page | Singleton page with its own content |
@@ -43,6 +59,21 @@ collection: the model and its entries both point at the page (`content_models.pa
43
59
  | Data from an external runtime API | Keep the adapter and its owner; never fabricate entries |
44
60
  | Sitewide nav, footer, promo bar | Global Layout, never copied page fields |
45
61
 
62
+ **Thresholds.** Three or more pages sharing a URL pattern and a card or detail layout are a
63
+ collection. Two are a collection when the set will grow (jobs, case studies). Otherwise they are
64
+ singletons.
65
+
66
+ **Promote repeater items to a collection** when any one holds; keep them inline when none does
67
+ (pricing bullets, a hero proof line, quotes on a one-off campaign page):
68
+
69
+ | Signal | Example |
70
+ |---|---|
71
+ | The item has its own URL | A case-study card linking to its detail page |
72
+ | The same item shows on two or more pages | A testimonial on home and on a product page |
73
+ | It carries facts that must not drift between copies | Price, stock, rating, job status, a person's photo |
74
+ | Another entry points at it | A post's author, an event's speaker |
75
+ | Editors will filter, sort or count the items | Jobs by team, events by date |
76
+
46
77
  ## 3. Four structure layers, four meanings
47
78
 
48
79
  Name the layer every time you say "folder". The answer to "where does X live" differs per layer.
@@ -84,8 +115,29 @@ sitemap), RSS (dated by `publishedAt`), `llms.txt` (one link per published item)
84
115
  one page or section owns.
85
116
  - A reference for reusable entities and internal links where the model supports it; an external
86
117
  URL for links that leave the site.
118
+ - **Resolve references before creating anything.** Build the target collection and its entries
119
+ first, then the model that points at them. Match each source item to an existing entry by
120
+ stable facts (a slug, a product handle, a name plus the start of the quote) and create only what
121
+ is missing. Store the entry id, never an href or a title. A write naming a target that does not
122
+ exist is refused with 400 (ref_missing). Flag an item with no identity (a quote with no name);
123
+ never import it.
87
124
  - A repeater for small, same-shaped items (cards, FAQ rows, links, social accounts); a nested
88
125
  component or slot when an item has its own layout or variants.
126
+ - **Lists are arrays; tags are taxonomy.** Features, steps, benefits and "what's included" are an
127
+ `array` (`itemType: "text"`), or a repeater when each item carries its own state (included or
128
+ not, an icon). Never a group holding one field. A label set that filters or classifies entries
129
+ is a shared collection referenced with `multi-reference` (§2), not free text.
130
+ - **Never freeze a feed.** A list of entries on the source stays a listing: copying the nine
131
+ visible cards into a repeater stops it updating. On a page BetterCMS renders, that is the
132
+ `collection` block: on the collection's own dynamic page it lists that collection, and with
133
+ `source: { modelId }` it lists any collection on any hosted page (§2). It takes `limit` (1 to
134
+ 200), `order` (`newest` or `oldest`) or `sort`, `filters`, `pinnedIds`, `excludeCurrent` and
135
+ `emptyText`; in an article template a filter value may be `{{field}}` of the entry shown, which
136
+ is a "related" list. Pagination and grouping stay a query in the site's code. Always set the
137
+ empty state, never write card titles or images into the listing, and put "read next" on the
138
+ template once, not on each entry.
139
+ - **`document` is the one long body.** A page schema or a collection may hold it, one at
140
+ most, and only as a top-level field: a group or a repeater cannot contain it. Label it Body. Short formatted copy stays `richtext`.
89
141
  - **Model a call to action as a single `button` field.** It stores `{ label, href }` plus the
90
142
  optional look keys `variant`, `size`, `arrow`, `icon` and `newTab`, and the site draws it with
91
143
  `BcmsButton` ([astro.md](astro.md), [next.md](next.md)). Never a text
@@ -97,11 +149,17 @@ sitemap), RSS (dated by `publishedAt`), `llms.txt` (one link per published item)
97
149
  so code still reading them keeps rendering; the live copy changes on publish. On a route the
98
150
  site's app renders at request time the merge is refused (409 `CODE_RENDERED_ROUTE`): change the
99
151
  code to draw the CTA with `BcmsButton`, and the next build adds the button field. Inside a component
100
- a CTA is a `button` block whose `props.text` and `props.href` two props target.
152
+ a CTA is a `button` block whose `props` one `button` prop targets ([components.md](components.md) §1).
153
+ - A sponsored, untrusted or user-generated link takes `rel` (`nofollow`, `sponsored` or `ugc`) on the
154
+ button, or a `link` field value written as `{ href, rel?, newTab? }`; code that opts into the object reads `.href`.
155
+ - A `date` field with a time takes `timezone` (`site` or an IANA name such as `Europe/Paris`) in its config;
156
+ values must then end in `Z` or an offset, and a bracketed zone suffix is dropped on save.
101
157
  - Never model SEO columns ([seo.md](seo.md)), Form questions (they live on the Form document) or
102
158
  Global Layout values as content fields.
103
159
  - Keep source ids, slugs, locale, status and previous URLs as migration metadata, stable across
104
160
  re-imports.
161
+ - Lock a field whose value another system owns (a store, job board or feed id): `readOnly: "developer"` with a `lockReason`.
162
+ 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.
105
163
  - **Prefer a `select` over a `boolean` for a state that may grow** (`draft | review | live`, not
106
164
  `isLive`). Why: no tool retypes a field, so a third state later costs a new field and a value
107
165
  migration (§6).
@@ -117,38 +175,48 @@ fail, because the next redesign makes them lie.
117
175
 
118
176
  Short labels named for what the editor sees: Heading, Body, Primary button, Label, Link, Image,
119
177
  Alt text — or the name the site's code already uses (`cents` → Cents), never the copy itself
120
- (`.00`). Every field gets help text (below). Group by what renders
178
+ (`.00`). Most fields need no help line from you (below). Group by what renders
121
179
  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
122
180
  button label), reorder, and stable item ids. An empty optional field removes its element without
123
181
  leaving a gap; a long value wraps, and clamps only where the design intends it.
124
182
 
125
183
  ### Writing help text
126
184
 
127
- Every field, prop, component, collection and Global section carries help an editor reads under
128
- its name: `helpText` on a field or prop, `description` on a component, a collection and a Layout
129
- Section (`update-section`). Write it the way a Sanity schema writes `description`:
185
+ The platform writes a role line on every element, so leaving `helpText` out is the default, not
186
+ a gap. It says so: `helpText: { source: "auto" }` in the response, `helpSource: "auto"` on the
187
+ stored field. Text you send is stored as yours (`helpSource: "agent"`) and is never rewritten;
188
+ `editor_setup` rewrites only its own lines and never a person's.
189
+
190
+ Ask one question per field: does the editor need this line to type the right value? Write your
191
+ own `helpText` (on a field or a prop) only for one of four reasons:
130
192
 
131
- - **Purpose, placement, constraint.** What it is for, where it shows, and a limit when one is
132
- real. One or two sentences, plain words, sentence case.
133
- - **Never the label or the value again.** `e.g. "Starter Kit"` under an input holding "Starter
134
- Kit" tells the editor nothing, and the API refuses help that echoes the value. An example may
135
- follow a real sentence when it shows a format.
193
+ | Reason | Good |
194
+ |---|---|
195
+ | A format or unit the label cannot show | Price in cents. · Length in minutes. · A .vtt caption file. |
196
+ | Where the value shows, when the canvas cannot show it | Shown in search results and link previews. · Read by screen readers, not shown. |
197
+ | A real constraint or consequence | Changing this breaks existing links. · Must be later than the start date. |
198
+ | A fallback | Leave empty to use the page title. |
136
199
 
137
- | Element | Good | Bad |
200
+ Everything else is left out:
201
+
202
+ | Never | Bad | Do instead |
138
203
  |---|---|---|
139
- | Hero heading | The page's main heading. One per page; keep it under about 60 characters. | Heading |
140
- | Button label | Text on the button. Two to four words works best. | e.g. "Get started" |
141
- | Meta description | Shown in search results under the page title. Aim for 120–160 characters. | The meta description. |
142
- | Logo | Logo shown in the header. An SVG or transparent PNG works best. | Image |
143
- | Component | Hero with a heading, supporting text, two buttons and an image. | Hero |
144
-
145
- Leave it out and the platform writes a line from the element's role, and says so
146
- (`helpText: { source: "auto" }` in the response, `helpSource: "auto"` on the stored field).
147
- Text you send is stored as yours (`helpSource: "agent"`) and is never rewritten; `editor_setup`
148
- rewrites only its own lines and never a person's. `get_next_steps` lists anything left without
149
- help as `help-text-missing`, and `get_binding_report` and `get_site_composition` carry the
150
- coverage as `help`: under 100% is a finding. Write your own line wherever the generated one
151
- is generic and you know more: the audience, the tone, who signs off, what breaks the layout.
204
+ | 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 |
205
+ | 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) |
206
+ | An example of the value | Badge: "e.g. Most popular" | A `placeholder` where the control takes one (component props do) |
207
+ | Strategy | Secondary button: "A lower-commitment option that builds trust." | Nothing. The label says which button it is |
208
+ | 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 |
209
+ | Generic good practice | "Add alt text for screen readers." on every image | Nothing |
210
+
211
+ One sentence, plain words, sentence case. If more than about one field in five carries your own
212
+ line, you are over-writing. A component, a collection and a Layout Section (`update-section`)
213
+ still take a `description` that says what the thing is ("Hero with a heading, supporting text,
214
+ two buttons and an image"), never its name again.
215
+
216
+ `get_next_steps` lists `help-text-missing` for a field with no help line at all, or one that
217
+ only echoes its label, and `get_binding_report` and `get_site_composition` carry the coverage as
218
+ `help`. Clear the finding by letting the platform's line stand or by writing one of the four
219
+ kinds above, never with filler.
152
220
 
153
221
  ## 6. Changing a schema safely
154
222
 
@@ -217,6 +285,67 @@ authorization and the component readiness and Visual Approval gates.
217
285
  count, codegen and build result, one editor check and one rendered-page check, warnings, and any
218
286
  change the platform could not make (an open dependency, never a success).
219
287
 
288
+ ## 7. Reading a schema written in the V2 language
289
+
290
+ A plan or Kit sample may arrive in the V2 schema language. It is an intermediate model only:
291
+ production cannot store it. Translate before the first write.
292
+
293
+ - **Keys.** V2 names are camelCase and scoped per object (`hero.heading`). Production keys are
294
+ flat, one namespace per model, prefixed by section: `"hero_heading"`.
295
+ - **Labels and help.** V2 `title` is `label`; V2 `description` is `helpText` (§5 decides whether
296
+ it is written at all).
297
+ - **Limits.** V2 `validation` becomes numbers in `config` (§6 step 4).
298
+
299
+ | V2 type | Model field (`create_content_model`, `add_field`) | Component prop (`create_component`) |
300
+ |---|---|---|
301
+ | `string` | `text` with `richText: false` | `text` |
302
+ | `text` | `longtext` | `text` |
303
+ | `richtext` | `richtext` for section copy; `document` for the one body | `richtext` |
304
+ | `number`, `rating` | `number` | `number` |
305
+ | `boolean` | `boolean` | `boolean` |
306
+ | `slug` | `slug` (an entry's own slug stays on the entry) | `text` |
307
+ | `url`, `link` | `link` | `link`; `url` for a media or embed address |
308
+ | `email`, `phone` | `email`, `phone` | `text` |
309
+ | `date`, `datetime` | `date`, `datetime` | `date` (`YYYY-MM-DD`); a datetime stays `text` |
310
+ | `color` | `color` | `color` |
311
+ | `select` | `select` | `select` with `config.options` |
312
+ | `multiselect`, `tags` | `array` (`itemType: "text"`); a taxonomy is a collection plus `multi-reference` | `multiselect` with `config.options` |
313
+ | `image` | `image` | `image` |
314
+ | `file`, `video` | `file`; a hosted video URL is a `link` | `file`; `url` for a hosted video |
315
+ | `icon` | `text` with `richText: false` | `icon` (an icon name or an image) |
316
+ | `button` | `button` | `button`: one prop targeting the `props` of a `button` block (§4) |
317
+ | `reference` | `reference`; `multi-reference` for many | none: keep it on the model |
318
+ | `code` | `longtext` | `text` |
319
+ | `json` | `json` | `text` |
320
+ | `geopoint` | `location` (`{ lat, lng, label? }`) | `group` of two `number` props |
321
+ | `table` | a Portable Text table inside `richtext` or `document`, or a `repeater` of rows | `table` |
322
+ | `object` | `group` | `group` |
323
+ | `array` of objects | `repeater` | `table` |
324
+ | `array` of strings | `array` (`itemType: "text"`) | `table` |
325
+ | `array` of several item shapes | `modular` with `config.blockSlugs` (create the block models first) | separate components |
326
+ | `slot` | `sections` or `component-ref` | `slot` (one instance) |
327
+ | a form `reference` | a `form` block inside the section | `form` |
328
+
329
+ V2 features with no production shape are dropped and reported, never faked: typed table columns,
330
+ per-breakpoint values, per-field localization, a slot with several
331
+ children (several sections or nested components instead).
332
+
333
+ | The plan says | Production word |
334
+ |---|---|
335
+ | Block | Component (the type); section or component instance (the placed band) |
336
+ | Site-wide block, global | Global Layout (`update_layout`) |
337
+ | CMS item | Entry |
338
+ | Collection page | An entry shown through its collection's dynamic page |
339
+ | Static page | `singleton` page |
340
+ | Collection | Content model plus its `dynamic` page |
341
+ | Variant, layout | Components sharing one `sectionType` ([components.md](components.md)) |
342
+ | Listing | `collection` block (§4) |
343
+ | Picks | `multi-reference` |
344
+ | Polymorphic array, modules | `modular` field |
345
+
346
+ Tools and reports use the production word. Labels and help an editor reads never say
347
+ "document": the `document` field is labelled Body.
348
+
220
349
  ## Acceptance
221
350
 
222
351
  - Every imported route and collection has one clear home; one dynamic template per collection.