@bettercms-ai/mcp 0.68.2 → 0.70.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.
@@ -55,7 +55,11 @@ build. Pass `apiUrl` and `workspace` as options, or let the snapshot supply them
55
55
 
56
56
  A section root your own code renders spreads `sectionRootAttrs(block)` from `@bettercms-ai/astro`
57
57
  (`data-bcms-block`, `data-bcms-style` and the block's style), so the editor can move it and offer
58
- its spacing handles ([components.md](components.md) §5).
58
+ its spacing handles ([components.md](components.md) §5). It adds `data-bcms-hide` when `style.hideOn` is set: put `RESPONSIVE_CSS` (exported here) in your global CSS.
59
+
60
+ `<BcmsBlocks>` does not render a block outside its `window`, read against the `now` prop (default:
61
+ render time). Pass `showScheduled` on a draft preview to draw it, marked `data-bcms-window`
62
+ ([publishing.md](publishing.md) §6).
59
63
 
60
64
  `bind` and `bindProps` from `@bettercms-ai/astro/bindings` produce the same attributes as the Next
61
65
  helpers. Use them inside component instances so addresses resolve to the instance
@@ -207,3 +207,50 @@ still renders hard-coded copy is not connected. Client components never receive
207
207
  - `get_binding_report`'s `help.percent` is 100: every bound field also tells its editor what it
208
208
  is for. Binding a leaf makes it editable; help text makes it usable
209
209
  ([content-modeling.md](content-modeling.md), "Writing help text").
210
+
211
+ ## Reading the binding report and the conversion receipt
212
+
213
+ `get_binding_report` per slot (`slot` is `current` or `staging`; on a promote-gated project read
214
+ `current`, the tree the editor frames). Every figure, `unaddressable` included, is measured per
215
+ slot. `sha` is the build the report describes; `refreshRequired` is true when that is not the
216
+ build the slot serves. `builtRoutes` lists the routes the live build has HTML for (null = cannot
217
+ say). `layoutIncomplete` lists Layout paths the last content apply could not write (their publish
218
+ waits); `layoutUnread` lists Layout fields no route reads (`layoutUnreadJudged` false = not
219
+ judged).
220
+
221
+ - **Before any release** there is no report: pages 0, mode null, `refreshRequired` true.
222
+ `refreshReason` `never-generated` means only a release BetterCMS builds and serves writes a
223
+ report, so a headless site never gets one, redeploy or not; `outdated` means the next release
224
+ rewrites it.
225
+ - **`unmatched` reasons**: `not-declared`, `ambiguous-text`, `no-element`, `declared-only`,
226
+ `needs-wrap`, `refused`, `layout-owned`, `page-owned`, `layout-locator-stale`,
227
+ `locale-unvarianted`, `svg-unavailable`, `partial`, `unstamped` (a page list the content apply
228
+ left as built; `kind` reads `list <bound>/<total>`), `mismatch` (a text-match stamp whose element
229
+ shows more than the value, or no longer the text it was matched on, `data-bcms-from`; never
230
+ written) and `report-outdated`.
231
+ - A clean report proves each non-empty field has an element carrying its path, nothing more. A Global
232
+ Section counts only on the routes that render it (`layoutRoutes`).
233
+ - **`unaddressable`** is the copy the report cannot see: visible live text no field owns, by
234
+ route and bucket, biggest first (null = unmeasured: diff the visible text against the entry
235
+ values). Over `count / visible > 0.02`, fix it in the source (wrap the run in an element the
236
+ codemod can bind, or declare `<BcmsField path=…>`), deploy and read again (playbook §11).
237
+ - `canvas.lane` is the editor's live-preview lane (`bridge`, `draft-route`; `none` sends you to
238
+ `get_next_steps`). `coverage.error` `nothing-bound` means no element of this build is bound, so
239
+ its percentage describes another site.
240
+
241
+ `submit_conversion_receipt` answers `complete` (`sourceComplete` is the receipt's own verdict;
242
+ `liveChecks` gives the last release's `structureUnstamped` route count and `reportSha`) and
243
+ `pendingTotal`, and echoes the first 40 pending rows with their `fix`:
244
+ `{ action, file, line, col?, snippet, why?, kind? }`. `action` is one of `wrap-span`,
245
+ `declare-attr`, `bind-expression`, `declare-richtext`, `bind-data`, `manual`; `snippet` and `why`
246
+ say what to change. On a `declare-richtext` with `kind: document`, bind the one element wrapping
247
+ every block of the Body, never the paragraph holding its first. A 404 `unknown-brief` means that
248
+ digest was never issued here. The same run owes the `--forms` receipt, submitted as a second call
249
+ under the same `briefDigest`, and the `styleEdits` of `convert --verify-styles` inside this
250
+ receipt; one listing a changed class, className or style is refused with 409 `style-edits`.
251
+
252
+ The complete brief (`get_conversion_brief { complete: true }`) also carries `forms` (id, status,
253
+ fields, submitUrl). After the binding pass, run the codemod's forms mode (`npx
254
+ @bettercms-ai/convert --forms`, with the brief and a receipt file) to wire them into the
255
+ repository's own `<form>` elements. Read `forms.pending`, and publish every form `forms.notes` names in the Forms tab: a draft
256
+ form rejects every submission with a 403.
@@ -25,8 +25,9 @@ Inside a section, pick the smallest shape that holds the children:
25
25
 
26
26
  - **Small identical items** (FAQ rows, feature bullets, logos): a `group` or `table` prop.
27
27
  - **One replaceable child with its own layout or variants** (a hero's media treatment): a `slot`
28
- prop. It holds exactly one instance, and never sits inside a `group` or `table` row.
29
- - **Several different children**: the section's ordered children, not a list of slots.
28
+ prop. It holds one instance, and never sits inside a `group` or `table` row.
29
+ - **Several different children**: the section's ordered children when the set is fixed; a `slot`
30
+ prop with `config.multiple` when the editor picks and orders them.
30
31
 
31
32
  Never let a component contain itself, directly or through a child.
32
33
 
@@ -40,8 +41,9 @@ Never let a component contain itself, directly or through a child.
40
41
  prose body and lists of object rows are sections. Read `pending` for what it will not offer:
41
42
  `NO_GROUP_ROOT` (keys are still the derive lane's own, such as `h1-welcome`: rename them into
42
43
  families first), `NOT_A_SECTION` (a lone scalar or page metadata; `detail: ENTRY_METADATA` is
43
- a template's frontmatter, `detail: DECLINED` a group the user declined) and `IMAGE_AS_TEXT`
44
- (a text field holding an image path; it plans once the field is an image). A nav, footer or
44
+ a template's frontmatter: author, images, tags and dates stay entry fields; `detail: DECLINED`
45
+ is a group the user declined) and `IMAGE_AS_TEXT` (a text field holding an image path; it plans
46
+ once the field is an image). A nav, footer or
45
47
  modal group is `NOT_A_SECTION` with `detail: LAYOUT_CHROME`: chrome is the Layout on every
46
48
  site, never a page band. Author the real sections among those with `create_component` and
47
49
  `set_page_content` instead.
@@ -10,26 +10,62 @@ 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
+ or a list of them with `config.multiple`; `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.
49
+ - **A `slot` with `config.multiple: true` holds a list.** Its value is
50
+ `[{ componentId, overrides? }, ...]` and its `target.path` is a container's child list:
51
+ `props.children` on a `section` block, or `props.columns.<n>` on a `columns` block (never
52
+ `props` on a `component` block). Each item is drawn there as a child component, in order.
53
+ Until a placement sets the slot, the children the definition holds in that list are shown;
54
+ `[]` shows none.
55
+ - `config.maxItems` defaults to 12 and cannot pass 50. `config.minItems` and
56
+ `config.componentIds` (which components may be listed) are checked on every write.
57
+ - An item may also carry `style` (use it for `hideOn`) and `window` with `from` / `until`
58
+ as UTC to the second (`2026-11-27T00:00:00Z`). A `defaultValue` must be an array and takes
59
+ no `window`.
60
+ - A write is a 400 whose message starts with the code: `slot_too_many`, `slot_too_few`,
61
+ `slot_item_invalid`, `slot_component_not_allowed`, `slot_component_unknown` (not a
62
+ component of this project), `component_cycle` (a component would end up inside itself,
63
+ through any chain), `component_too_deep` (more than 10 levels), `slot_target_invalid`,
64
+ `slot_not_multiple` (a list sent to a single slot). A value that is already stored is not
65
+ refused when the page is saved again.
66
+ - A child's copy is not click-to-edit on the canvas. Change it in the placement's
67
+ `overrides.<slot key>` list with `update_page`.
68
+ - Publish every listed component: a draft child renders nothing, like any draft component.
33
69
  - **A `form` prop stores an id, nothing more.** Modelling the questions again as props gives you
34
70
  two answers to "what does this form ask?", and they diverge at the first edit on either side.
35
71
  The save schema is `.strict()`, so posting the hydrated form object back is a 400, not a silent
@@ -65,6 +101,12 @@ source? } }`.
65
101
  component with a `sectionType`. A true one-off is a `section` block whose `props.children` hold
66
102
  its blocks.
67
103
  - Components that share a `sectionType` are one family: its variants.
104
+ - **A built-in section is a starting point, not a thing you place.** `list_components` shows the
105
+ locked `builtin:*` blueprints; `create_component` with `from: "builtin:hero-centered"` copies
106
+ one into the project as an ordinary draft component, then edit and publish it like any other.
107
+ Whatever you send (`name`, `slug`, `props`, `blockJson`, `sectionType`, `category`) wins over
108
+ the copy. `list_components` `sourceKey` says which built-in a component came from. A copy
109
+ never follows later changes to the built-in. An unknown id is 404 `BUILTIN_NOT_FOUND`.
68
110
  - **Give every component a library `category` as well.** Why: `sectionType` groups a family, the
69
111
  category is how the component library lists it — an uncategorised component is created fine and
70
112
  is then hard to find in the picker, which is where a human looks for it.
@@ -112,7 +154,7 @@ Handle by code, never by retrying the same call:
112
154
  |---|---|---|
113
155
  | `COMPONENT_BLUEPRINT_MATERIALIZATION_REQUIRED` | You tried to publish a platform built-in | Create a project-local component from it, publish that |
114
156
  | `COMPONENT_PROJECT_SCOPED_RELEASE_REQUIRED` | The component is workspace-global | Create a project-local variant, publish that |
115
- | `COMPONENT_IMPLEMENTATION_NOT_READY` | A REPO-managed component lacks exact evidence and human approval, or a variant-group member's family contract is unpublished. Never a `create_component` component outside a variant group | Report `readiness`; the owner resolves it in the dashboard |
157
+ | `COMPONENT_IMPLEMENTATION_NOT_READY` | A REPO-managed component lacks exact evidence and human approval, or a variant-group member (the provisioned navigation and footer) has a family contract that is unpublished. Never a `create_component` component outside a variant group: that one is dashboard-managed and publishes on the preflight alone | Report `readiness`; the owner resolves it in the dashboard |
116
158
  | `COMPONENT_ORCHESTRATION_V2_REQUIRED` | The project runs implementation in Agent Dock | Continue there; stop here |
117
159
  | `COMPONENT_PUBLISH_CONFLICT` | The component or its evidence changed mid-publish | Re-read with `get_component`, then publish once more |
118
160
  | `COMPONENT_PROJECT_CONTEXT_MISMATCH` | The component's `projectId` differs from the request's project scope | Call with the component's own project |
@@ -295,4 +337,13 @@ stored list.
295
337
  siblings.
296
338
  2. `get_site_composition`: every page is sections; every component you meant to publish is
297
339
  published.
340
+ - Per page: `kind` is `blocks`, `fields-only` or `empty`. `nonComponentBlocks` are loose
341
+ top-level bands the editor's section lane cannot outline.
342
+ - Per placement: the component's `status` is `published`, `draft` or `missing`. `inPicker` is
343
+ false for a component with no `sectionType`: "Add a section" does not offer it.
344
+ `emptyRequiredProps` is reported only here.
345
+ - Site-wide: `components` counts published and draft, `awaitingEvidence` and
346
+ `awaitingApproval`. A person approves in the dashboard, one component at a time. Then the
347
+ `notInPicker` and `unused` ids, and `layout.placements`: the chrome placed in the Global
348
+ Layout draft.
298
349
  3. `get_binding_report` on a page that uses the component: instance values bind.