@bettercms-ai/mcp 0.63.2 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +1 -0
  2. package/SKILL.md +1 -1
  3. package/dist/index.js +908 -487
  4. package/dist/index.js.map +1 -1
  5. package/dist/skills/bettercms/SKILL.md +175 -0
  6. package/dist/skills/bettercms/references/astro.md +67 -0
  7. package/dist/skills/bettercms/references/bindings.md +201 -0
  8. package/dist/skills/bettercms/references/componentize.md +106 -0
  9. package/dist/skills/bettercms/references/components.md +228 -0
  10. package/dist/skills/bettercms/references/content-experiments.md +62 -0
  11. package/dist/skills/bettercms/references/content-modeling.md +185 -0
  12. package/dist/skills/bettercms/references/editor-chrome.md +88 -0
  13. package/dist/skills/bettercms/references/external-content.md +78 -0
  14. package/dist/skills/bettercms/references/finish-site.md +123 -0
  15. package/dist/skills/bettercms/references/forms-and-interactions.md +203 -0
  16. package/dist/skills/bettercms/references/frameworks.md +149 -0
  17. package/dist/skills/bettercms/references/getting-started.md +106 -0
  18. package/dist/skills/bettercms/references/localization.md +81 -0
  19. package/dist/skills/bettercms/references/media-and-brand.md +142 -0
  20. package/dist/skills/bettercms/references/next.md +93 -0
  21. package/dist/skills/bettercms/references/other-frameworks.md +66 -0
  22. package/dist/skills/bettercms/references/portable-text.md +206 -0
  23. package/dist/skills/bettercms/references/programmatic-seo.md +98 -0
  24. package/dist/skills/bettercms/references/publishing.md +146 -0
  25. package/dist/skills/bettercms/references/seo.md +156 -0
  26. package/dist/skills/bettercms/references/shopify-headless.md +52 -0
  27. package/dist/skills/bettercms/references/site-blueprints.md +250 -0
  28. package/dist/skills/bettercms/references/site-review.md +73 -0
  29. package/dist/skills/bettercms/references/write-limits.md +84 -0
  30. package/package.json +2 -2
@@ -0,0 +1,175 @@
1
+ ---
2
+ name: bettercms
3
+ description: "BetterCMS: build, convert, import, edit, publish and verify BetterCMS sites. Use whenever the user names BetterCMS or its MCP, or the repo has @bettercms-ai/* packages, data-bcms- attributes, bcms-content.json or bcms-rebuild.yml, even when they only say 'make it editable', 'import my site', 'convert this repo', 'add a section', 'fix the schema' or 'publish'. Covers setup, the finish-site workflow, content modeling, components, click-to-edit bindings, Portable Text, media, forms, SEO and launch review. Moving off another CMS: bettercms-migrate. ABM campaigns: bettercms-abm."
4
+ license: MIT
5
+ metadata:
6
+ author: bettercms
7
+ version: "0.3.0"
8
+ ---
9
+
10
+ # BetterCMS
11
+
12
+ The BetterCMS MCP serves its own doctrine, so this skill carries only what the server cannot see:
13
+ your repo's framework wiring, binding attributes, env names, stored value shapes and the traps
14
+ that only show up in code. Read doctrine from the MCP, never from memory:
15
+
16
+ - Schema, sections, variants, publish order: resource `bettercms://playbook/schema` (§1–§14).
17
+ - Content sidebar organisation: resource `bettercms://playbook/structure`.
18
+ - Guided flows: the `studio` prompt routes to the rest (`convert_site` prompt,
19
+ `build_site` prompt, `componentize_site` prompt, `propose_schema` prompt, `new_page` prompt,
20
+ `new_component` prompt, `new_form` prompt, `seo_optimize` prompt, `import-site` prompt,
21
+ `field-ui` prompt, `edit_layout` prompt for the Global Layout, `generate_landing_pages` prompt
22
+ for page families from data).
23
+ - Import and modelling rule ids: `CM-01`…`CM-83` in the content-modeling standard.
24
+
25
+ ## Start here
26
+
27
+ Setting up a project, or asked to "Continue BetterCMS setup"? Read
28
+ [references/getting-started.md](references/getting-started.md) first: it owns the setup phases
29
+ and the resume check. For any other task, read the one reference the table below names.
30
+
31
+ ## Connecting (before your first MCP call)
32
+
33
+ Listing tools needs no sign-in; the first tool **call** does.
34
+
35
+ 1. **A 401 on the first call is the sign-in, not an error.** The client opens a browser sign-in
36
+ from the `WWW-Authenticate` header. Tell the user to approve it, then retry that same call.
37
+ 2. **If `list_projects` lacks the project id you were given, stop and say so.** Consent binds to
38
+ the workspace the dashboard had active; ask the user to re-authenticate in the right one.
39
+ Never continue in whichever project IS listed: that writes into someone else's site.
40
+ 3. **Pass `projectId` on every project tool, and never ask the user to re-scope.** A
41
+ workspace-wide connection reaches every project by naming it per call.
42
+ 4. **A refusal for access: call `whoami` first.** It names the principal, the workspace, the
43
+ scope (project or workspace) and, where visible, the grants (publish, code and deploy,
44
+ sidebar schema), so you can tell the user which one is missing.
45
+ 5. **Unsure how a feature works: `search_docs` before guessing.** A tool that fails oddly, a
46
+ capability you needed and did not have, or docs that misled you: `give_feedback`, and tell the
47
+ user you sent it.
48
+
49
+ ## Invariants
50
+
51
+ These hold on every task. The reference in brackets has the detail and the codes.
52
+
53
+ 1. **Confirm the bound project before any write**: §9 of the schema playbook, then `get_project`.
54
+ Its `authoringPreference` (components, fields, or null while owed) is asked once, can be
55
+ re-answered, and is the user's answer, never yours.
56
+ 2. **Never create SEO fields** (§5b). Page and entry meta are native columns. [seo.md]
57
+ 3. **A page is a list of sections**, never loose top-level blocks. [components.md]
58
+ 4. **Nothing is live until it is published, and a page is not its content.** A draft form,
59
+ component or entry renders nothing live. Read `warnings` on every write: `ENTRY_STILL_DRAFT`
60
+ and `ENTRY_SEED_FAILED` mean content did not ship. [publishing.md]
61
+ 5. **Name the folder system every time you say "folder"**: Content sidebar folders never touch a
62
+ URL, URL page folders are part of the path, media folders organise assets.
63
+ [content-modeling.md]
64
+ 6. **Headless projects are built by their host.** Never `deploy_project` or
65
+ `create_deploy_upload` there, never add `bcms-rebuild.yml`; deploy = push to the repo the
66
+ provider builds. [frameworks.md]
67
+ 7. **Every refusal has a code. Report it; do not retry blind.** The same call returns the same
68
+ code. Change the input or stop.
69
+ 8. **Media is served by the backend that owns the asset ids.** `mediaUrl` is `mediaBaseUrl` from
70
+ `get_project`; never hardcode, re-point or re-host media, and never read a bucket or key.
71
+ [frameworks.md]
72
+ 9. **Bind the leaf that renders the value, one address per element.** Attribute values ride
73
+ `data-bcms-props`; `data-bcms-kind` declares the container, and the MODEL picks the editor.
74
+ Inside an instance the address is `<instanceId>__overrides.<key>`; chrome uses
75
+ `data-bcms-layout-field`. [bindings.md]
76
+ 10. **Declared mode is a switch you flip** with `set_binding_mode` after the stamped build reports
77
+ clean: push, release, report clean, flip, release again. [bindings.md]
78
+ 11. **A field is not editable because it exists.** It is editable when its value renders from the
79
+ CMS on a bound leaf and an edit shows in preview and after publish.
80
+ 12. **Bulk work runs as code, not as a chain of tool calls.** Propose the mapping and dry-run it
81
+ before a high-volume write; use the MCP for state, targeted fixes and reports.
82
+
83
+ ## What you do, and what only the owner does
84
+
85
+ The MCP acts as the signed-in person. Some decisions are theirs by design: no tool makes them,
86
+ and saying you made one is a false report. Name each as an action in your final message.
87
+
88
+ | Only the owner | Where |
89
+ |---|---|
90
+ | Authoring preference and media delivery answers | you ask; `set_authoring_preference`, `set_media_delivery` |
91
+ | Visual Approval of a component, CI (GitHub Actions) validation | dashboard |
92
+ | Approving a generated job or a flagged ABM entry | the user's explicit yes before `approve_ai_job` / `approve_campaign_entries` |
93
+ | Brand colours and typography; URL page folders; unpublishing a form | dashboard |
94
+ | Hosting **Run as a server app**; form integrations; the Turnstile secret | dashboard |
95
+ | `llms.txt` and Markdown twin switches | dashboard, SEO permission |
96
+ | Publishing to production | the user's authorization, then the tools |
97
+
98
+ Secrets never go in a tool call, a `PUBLIC_` variable or a client bundle. A 403
99
+ `SCHEMA_ACCESS_REQUIRED` means the person lacks Admin or Developer access: stop and tell them.
100
+
101
+ ## Which reference to read
102
+
103
+ Read one reference per phase. Each is self-contained; a second one is needed only when the task
104
+ crosses into its topic. When this skill is not installed, the MCP serves the same files as
105
+ resources: `bettercms://skill/bettercms` for this file, and each reference under its file stem
106
+ (`bettercms://skill/seo` for seo.md).
107
+
108
+ | Task | Reference | Read when | Keywords |
109
+ |---|---|---|---|
110
+ | 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 |
111
+ | 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 |
112
+ | 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 |
113
+ | 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 |
114
+ | 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 |
115
+ | 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 |
116
+ | 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 |
117
+ | Components, props, variants, component publish codes | [components.md](references/components.md) | creating, swapping, publishing or validating a component | component, prop, variant, sectionType, publish component |
118
+ | 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 |
119
+ | 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 |
120
+ | 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 |
121
+ | Astro adapter | [astro.md](references/astro.md) | the repo has `astro.config.*` | astro.config, Astro, BcmsBlocks, getStaticPaths |
122
+ | Next.js adapter | [next.md](references/next.md) | the repo has `next.config.*` | next.config, Next.js, App Router, ISR, notFound, static export |
123
+ | Nuxt, SvelteKit, React Router, Vite: no adapter | [other-frameworks.md](references/other-frameworks.md) | neither `astro.config.*` nor `next.config.*`; `nuxt.config.*`, `svelte.config.*`, `react-router.config.*` | Nuxt, SvelteKit, React Router, Vite, createClient |
124
+ | Rich text and field types | [portable-text.md](references/portable-text.md) | writing a richtext value; converting HTML or Markdown | rich text, Portable Text, richtext, HTML, Markdown |
125
+ | 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 |
126
+ | 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 |
127
+ | 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 |
128
+ | 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 |
129
+ | 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 |
130
+ | 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 |
131
+ | 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 |
132
+ | A/B tests on section variants | [content-experiments.md](references/content-experiments.md) | an experiment plan or readout | A/B test, experiment, variant test |
133
+ | Headless Shopify storefront | [shopify-headless.md](references/shopify-headless.md) | Shopify owns commerce, BetterCMS the editorial | Shopify, Hydrogen, storefront, product |
134
+
135
+ Other sites move into BetterCMS with the `bettercms-migrate` skill; per-account campaign pages
136
+ are the `bettercms-abm` skill.
137
+
138
+ ## Context budget
139
+
140
+ A whole-site job outlives one context window. Spend it on the site, not on re-reading.
141
+
142
+ - Start from the receipt: `get_next_steps` returns `receipt.done`, `receipt.missing` and
143
+ `receipt.nextAction`. Plan the session from it, not from re-reading the site.
144
+ - Hold SKILL.md plus only the ONE reference the current phase needs; drop it when the phase ends.
145
+ - Ask the MCP for summaries before reading files: `get_site_composition`, `get_binding_report`
146
+ and `get_next_steps` answer "what is left" in a few hundred tokens, where built HTML or a whole
147
+ repo costs tens of thousands.
148
+ - Run bulk edits through the codemod (`npx @bettercms-ai/convert`), not file-by-file hand edits
149
+ in context; read its receipt, not the diff.
150
+ - Keep the gap ledger in `.bettercms/ledger.json` in the repo (shape in finish-site.md) and
151
+ re-read it after a reset instead of carrying it in chat.
152
+ - Where your agent supports subagents, fan out per-route verification and have each return only
153
+ its verdict and the failing evidence.
154
+ - Pass the `projectId` you already have; never re-list projects to find it again.
155
+ - After a reset, say "Continue BetterCMS setup": the resume check rebuilds state from the MCP and
156
+ the ledger.
157
+
158
+ ## Verify loop (after every change, before you say "done")
159
+
160
+ Stop at the first step that is not clean, fix, then restart. A report that does not exist yet is
161
+ "not yet generated", which is not the same as dirty; say which.
162
+
163
+ 1. **Fetch the live URL**, cache-busted: a 200 whose body contains copy you just published. The
164
+ steps below read the control plane, and all of them can be green on a site that answers 404.
165
+ 2. `get_binding_report` for every route you touched: `coverage.pending` empty, and
166
+ `mode: "declared"` on a declared site. `refreshReason: "never-generated"` means no BetterCMS
167
+ release wrote one (on headless none ever will, so step 1 is the proof); `"outdated"` means
168
+ the next release rewrites it.
169
+ 3. `get_site_composition`: every page you touched is made of sections, with the intended
170
+ components.
171
+ 4. `list_seo_issues`: no issue on a page you created or edited.
172
+ 5. `get_next_steps`: nothing left that belongs to your task; owner actions go in your final
173
+ message by name.
174
+
175
+ "Done" means the loop is clean, not that a tool call returned 200.
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: "Astro adapter"
3
+ description: "The @bettercms-ai/astro integration: options and env precedence, draft preview, shipped components, bindings and Astro-specific traps."
4
+ ---
5
+
6
+ # @bettercms-ai/astro
7
+
8
+ Read this when the repo has `astro.config.*`. Shared rules (hosting modes, env map, tags, value
9
+ shapes) are in [frameworks.md](frameworks.md).
10
+
11
+ ## Integration
12
+
13
+ Add `bettercms()` to `integrations` in `astro.config.*`. It:
14
+
15
+ - adds the `@bettercms-ai/astro/middleware` middleware (order `pre`);
16
+ - injects `/api/bcms/draft/enable` and `/api/bcms/draft/disable`;
17
+ - sets a signed `bcms-draft` cookie when draft mode is enabled.
18
+
19
+ Options win over env, env wins over the snapshot: `apiUrl` → `PUBLIC_BCMS_API_URL` → snapshot,
20
+ `workspace` → `PUBLIC_BCMS_WORKSPACE` → snapshot, `projectId` → `PUBLIC_BCMS_PROJECT_ID`,
21
+ `mediaUrl` → `PUBLIC_BCMS_MEDIA_URL` → the production CDN, `studioUrl` →
22
+ `PUBLIC_BCMS_STUDIO_URL` (no default).
23
+
24
+ `draft` defaults to `true`, which injects two ON-DEMAND routes (`/api/bcms/draft/{enable,disable}`).
25
+ A static build with no server adapter then fails with `NoAdapterInstalled`. On a static site pass
26
+ `bettercms({ draft: false })`; draft preview needs an adapter AND the Hosting page set to Run as a
27
+ server app (see Hosting modes).
28
+
29
+ `astro.config.*` runs before Astro loads `.env`, so `PUBLIC_BCMS_*` is empty there on a local
30
+ build. Pass `apiUrl` and `workspace` as options, or let the snapshot supply them.
31
+
32
+ ## Draft preview
33
+
34
+ - Set `BCMS_DRAFT_SECRET` on the server. Without it `/api/bcms/draft/enable` answers 500.
35
+ - The draft routes run on request, so the site needs an on-demand adapter (node, vercel, netlify,
36
+ cloudflare). A fully static build serves the published snapshot only.
37
+
38
+ ## Components shipped
39
+
40
+ | Import | Use |
41
+ |---|---|
42
+ | `@bettercms-ai/astro/components/BcmsBlocks.astro` | Server render of a block tree. Pass `components` or instances render nothing |
43
+ | `@bettercms-ai/astro/components/BcmsLiveBlocks.astro` | Same tree as a hydrated island that follows the editor's draft bridge |
44
+ | `@bettercms-ai/astro/components/BcmsField.astro` | `path`, `kind` (`richtext`/`image`), `as`; emits `data-bcms-field` + `data-bcms-kind` |
45
+ | `@bettercms-ai/astro/components/BcmsForm.astro` | Renders a delivered form and submits it ([forms-and-interactions.md](forms-and-interactions.md)) |
46
+
47
+ `BcmsLiveBlocks` rules:
48
+
49
+ - It needs a React-compatible renderer: add `preact({ compat: true })` next to `bettercms()`.
50
+ - Its CSS class names differ from `BcmsBlocks`. Swapping one for the other unstyles a page unless
51
+ your stylesheet covers both spellings.
52
+
53
+ ## Bindings
54
+
55
+ `bind` and `bindProps` from `@bettercms-ai/astro/bindings` produce the same attributes as the Next
56
+ helpers. Use them inside component instances so addresses resolve to the instance
57
+ ([bindings.md](bindings.md) §3).
58
+
59
+ ## Traps
60
+
61
+ | Symptom | Cause | Rule |
62
+ |---|---|---|
63
+ | Resized images 404 on a non-production site | `mediaUrl` still the production default | Set `PUBLIC_BCMS_MEDIA_URL` to that rung's API origin |
64
+ | Resized images still on the CDN after moving media | `mediaUrl` not changed | Set it to `mediaBaseUrl` from `get_project` |
65
+ | Component instances empty | `components` prop missing | Pass definitions to `BcmsBlocks` |
66
+ | Draft link 500 | `BCMS_DRAFT_SECRET` unset | Set it on the server env, redeploy |
67
+ | `ReferenceError: X is not defined` from a `[slug].astro` route | `getStaticPaths()` reads a `const` declared in the frontmatter; Astro runs it in its own scope, before the page, seeing only imports | Declare the client or reader call inside `getStaticPaths`, or import it from a module |
@@ -0,0 +1,201 @@
1
+ ---
2
+ title: "Bindings and inline editing"
3
+ description: "Click-to-edit attributes and addresses, declared mode and the binding report, stega in preview strings, and classifying an unfamiliar codebase."
4
+ ---
5
+
6
+ # Bindings, inline editing and codebase classification
7
+
8
+ Doctrine (what should be editable, the conversion loop): §11 and §13 of
9
+ `bettercms://playbook/schema`, and the `convert_site` prompt. This is the template side. Rich-text
10
+ values: [portable-text.md](portable-text.md). Adapter wiring and env: [frameworks.md](frameworks.md).
11
+
12
+ ## 1. Why the attributes matter
13
+
14
+ A `data-bcms-*` attribute is the only thing that locates a stored value on a rendered page. Five
15
+ readers resolve the same address: the Visual Editor canvas, the component Output pane and live
16
+ preview, publish-time value injection, `get_binding_report`, and componentization. A reader that
17
+ cannot resolve an address guesses, which works once.
18
+
19
+ ## 2. Bind the leaf, and know which vocabulary you are writing in
20
+
21
+ **Put the attributes on the element that renders the value and nothing else: the `<img>` itself,
22
+ the `<h1>` itself. Never a wrapper, never an element whose children you need.** A declared write
23
+ replaces what is inside the element it names. Bind a `<section>` or a card and the band's own
24
+ markup is what you overwrite; the page comes back as bare text where a layout used to be.
25
+
26
+ `data-bcms-kind` does not choose the editor; the MODEL does. The attribute says **"this element is
27
+ the declared CONTAINER for the field"**, so the canvas repaints it whole. Leave it off and the
28
+ canvas treats the element as one text-matched FRAGMENT of the value and injects an inline render
29
+ instead, which for a Body means the article does not come back until the next rebuild.
30
+
31
+ Two separate lists, and mixing them is the way to break a page:
32
+
33
+ | Attribute | The only values it takes |
34
+ |---|---|
35
+ | `data-bcms-kind` | `text` (or omit it) · `richtext` · `image` · `document` |
36
+ | the kind inside `data-bcms-props` | `text` · `richtext` · `image` · `url` · `number` · `array` |
37
+
38
+ A model's field TYPES (`select`, `boolean`, `date`, `number`) are **not** kinds. Stamping one
39
+ names a value no renderer mints, and the preview writer refuses it with `UNKNOWN_KIND`.
40
+
41
+ A `document` body: the WRAPPER carries `data-bcms-kind="document"`, and the per-block elements
42
+ inside carry their own `data-bcms-kind="richtext"`. Never `"richtext"` on the wrapper: that takes
43
+ the rich-text arm, which repaints the container from the field's derived PLAIN TEXT and loses the
44
+ article's blocks. Omitting the kind is legal (the hosted renderer emits it that way) and costs the
45
+ container repaint, so a declared template states it.
46
+
47
+ What the author gets, from the field's type: text is edited in place, an image opens the media
48
+ picker and patches the element's `src`, richtext and document open a Portable Text editor in the
49
+ frame, and a `url` never takes a caret: it is edited in the element toolbar, because a malformed
50
+ URL typed onto the page has nowhere to report itself.
51
+
52
+ ### Lists, labels and icons
53
+
54
+ An element bound as text or richtext may hold only inline markup (`span`, `a`, `strong`, `em`,
55
+ `b`, `i`, `small`, `sup`, `code`, `br`), classes and inline styles included. A publish writes the
56
+ value over everything else inside it, so:
57
+
58
+ - **A list item with a marker.** In `<li><span>✓</span>{item}</li>` the `<li>` renders the tick
59
+ and the item; bind it and the first save erases the tick. Wrap the value in its own `<span>`
60
+ and bind that. An `<li>` that renders nothing but the value is itself the leaf.
61
+ - **A link or button label beside an icon.** In `<a href="…"><svg/> Sign up</a>`, put the label
62
+ in its own `<span>` with the field, and the `href` on the anchor through `data-bcms-props`
63
+ (`cta.href|url|href`). A binding on the anchor's text deletes the icon.
64
+ - **SVG text is never bound.** The platform does not read inside an `<svg>`: no field is derived
65
+ from its text and no binding there is measured. An icon is media: upload it with
66
+ `create_media_upload` and bind the `<img>` as an `image`, or leave it in code.
67
+ - **Icon-only links in the nav and footer** take their field name from the anchor's
68
+ `aria-label`, then the SVG `<title>`. Give each one an `aria-label`, or every social link is
69
+ named `link` and an editor cannot tell them apart.
70
+
71
+ ### The site's design is not yours to change
72
+
73
+ **Never edit a `class`, `className` or `style`, and never rewrite markup to suit the CMS.** A rich
74
+ text value keeps inline markup exactly as the site wrote it: `Equip <span class="text-yellow-500
75
+ dark:text-yellow-400">ScrewFast</span>` is stored and published with that span and its classes.
76
+ Do not turn it into `<em>` with an `[&_em]:` selector on the parent, and do not turn
77
+ `<span class="font-bold">` into `<b>`. Before submitting a conversion receipt, run
78
+ `npx @bettercms-ai/convert --verify-styles --base <commit before the conversion> --root .` and put
79
+ its `styleEdits` in the receipt; any token it lists is a design change to restore.
80
+
81
+ ### Images
82
+
83
+ An image the page imports and renders is bound by the codemod, including one the page hands to a
84
+ section component (`<Hero src={heroImage} />` → the component's `<Image src={src}>`): the call site
85
+ reads `bcmsImage(...)` and passes `bcmsBindings`, and the `<Image>` gets
86
+ `data-bcms-field={bcmsBindings?.src} data-bcms-kind="image"`. Keep the `astro:assets` import as
87
+ the fallback; the build renders it and the publish writes the editor's image. Frontmatter images
88
+ (`cardImage: '@/images/blog/post-1.avif'`) become image fields on their own once the build's
89
+ images are in the Media Library, matched by file name. An image rendered inside a loop over a
90
+ list is not bound automatically: bind it in the list's data file.
91
+
92
+ ## 3. Addresses
93
+
94
+ The grammar is identical in `@bettercms-ai/next` and `@bettercms-ai/astro`; emit it with the SDK
95
+ helpers rather than by hand.
96
+
97
+ - `data-bcms-field="<address>"` + `data-bcms-kind` on the element whose content is the value.
98
+ - `data-bcms-props="<address>|<kind>|<domAttribute>;…"` for values that live in an attribute
99
+ (`alt`, `href`, a video `src`). One element carries one field address, so attribute values ride
100
+ this list. An `href`, an `src` and a `poster` are kind `url` (`cta.href|url|href`, not
101
+ `|text|`); an `alt` is `text`. The same address on two elements is edited as two copies of one
102
+ value (numbered `data-node-index`).
103
+ - `<BcmsField path kind as>` wraps hand-rendered entry values. `path` is dotted keys with any
104
+ number of `[n]` indices: `title`, `hero.title`, `columns[0].links[2].href`.
105
+ - Rich text is addressed per paragraph: `<base>#<_key>`. No write tool takes an address:
106
+ `update_content_entry` replaces the whole field (with `merge: true` it keeps the other
107
+ top-level fields, never other blocks of the same field). The platform's own path writer refuses a
108
+ `#<_key>` path (`KEYED_PATH_NOT_WRITABLE`), and only the editor patches one block. A `_key` is
109
+ part of an address, so never regenerate keys of blocks that already exist.
110
+ - **Inside a component instance** the address is `<instanceId>__overrides.<key>`. Block ids
111
+ inside a component belong to the definition and are shared by every instance, so a
112
+ definition-scoped address binds to nothing and two instances collide. Use the package `bind` /
113
+ `bindProps` helpers inside instances; they resolve the instance address for you.
114
+ - **The placement root carries `data-bcms-block="<blockId>"`.** `BcmsBlocks` emits it; without it
115
+ the Output pane and the section overlay fall back to an ancestor guess.
116
+ - **Chrome is its own lane.** A nav or footer value is
117
+ `data-bcms-layout-field="layout:<sectionId>:<fieldId>"`, never `data-bcms-field`; the page lane
118
+ cannot reach a Layout value.
119
+ - **Draft markers come from the SDK.** `BcmsDraftMarker` and the Astro middleware stamp
120
+ `<html data-bcms-draft="1">`; `BcmsDraftBridge` stamps `data-bcms-canvas="bridge"`. Mount them;
121
+ never write them by hand. Binding attributes are always rendered and inert for visitors.
122
+
123
+ ## 4. Declared mode, publish, and the report
124
+
125
+ - Until `set_binding_mode` `{ declaredBindings: true }` the platform text-matches, which holds
126
+ only while CMS values equal the built copy. Flip it only when the stamped build's report is
127
+ clean on every route: push, release, read `get_binding_report`, flip, release again, then
128
+ confirm `mode: "declared"` and `coverage.pending` empty.
129
+ - On a declared site every published value is written into the element that declares it, so the
130
+ bytes your template renders at build time are replaced on publish; never put markup you need to
131
+ keep inside a bound element. A value with no element never reaches the page, and one that
132
+ would corrupt its element is refused and named.
133
+ - Read the report, not the screen. Clean = `coverage.pending` empty and the mode you expect. Each
134
+ `unmatched` reason names a fix in the source. A `fallback` flag means the build carried no
135
+ `bcms-brief` meta and was measured against the latest pin. Only a release creates the report;
136
+ a content apply later updates its Layout rows.
137
+
138
+ ## 5. Preview strings (stega)
139
+
140
+ Preview responses requested with `stega=1` hide invisible characters inside prose strings so the
141
+ editor can map text back to fields. Two filters decide what is skipped, and you need both:
142
+
143
+ - by field NAME: `slug`, `url`, `href`, `src`, `id`, `email`, `color`, `icon`, `date`,
144
+ `datetime`, `locale`, `type`, `target`, `rel`, `_type`, `_key`;
145
+ - by VALUE shape, for fields it cannot recognise by name: anything starting `http:`, `https:`,
146
+ `mailto:`, `tel:`, `#` or `/`, and ISO dates.
147
+
148
+ Portable Text arrays and richtext/image envelopes are skipped whole. Never compare, slugify or
149
+ parse a preview string as-is, and never put one in an attribute or SEO output. Compare published
150
+ values, or strip it first with `stripStega` from `@bettercms-ai/sdk`. The editor strips it before
151
+ it saves.
152
+
153
+ | The string goes to | Strip first? | Why |
154
+ |---|---|---|
155
+ | Visible text a template renders | Never | The markers are how a click finds its field |
156
+ | A Portable Text renderer, `BcmsField`, an image helper | Never | Those values carry no markers; pass them as-is |
157
+ | `===`, `switch`, `includes`, sort order, a lookup key | Always | The hidden characters make equal strings unequal |
158
+ | A React/Astro `key`, an element `id`, a slug, a URL you build | Always | A marked id never matches its published twin |
159
+ | Any HTML attribute (`class`, `href`, `alt`, `aria-label`) | Always | Attributes are not text the editor maps |
160
+ | `<head>`: title, meta, JSON-LD | Always | Crawlers and share cards read the characters |
161
+ | A third-party call (analytics, search, maps, a date parser) | Always | They store or reject what they cannot read |
162
+
163
+ ## 6. Classify an unfamiliar codebase before binding it
164
+
165
+ Inspect `package.json`, the framework config, route tree, build command, output mode, content
166
+ loaders, deployment config and any existing BetterCMS adapter. Then classify:
167
+
168
+ 1. **First-class adapter**: Astro or Next.js with its BetterCMS package ([astro.md](astro.md),
169
+ [next.md](next.md)).
170
+ 2. **React/Vite or another SDK integration**: find who owns routing and data fetching, call the
171
+ SDK on the correct side of the server/client boundary, keep an explicit component registry,
172
+ and state what preview supports. The Astro and Next helpers do not transfer unchanged.
173
+ 3. **Static HTML/CSS/JS**: keep the working markup and add a safe content path where supported.
174
+ 4. **Other or opaque generated output**: preserve behaviour and produce an adapter plan (fetch
175
+ typed content, resolve sections and components, render Portable Text, bind editable values,
176
+ serve media, preview drafts, refresh on publish), marking every unimplemented capability. A
177
+ source crawl alone never proves full editability.
178
+
179
+ Use syntax-aware parsing and the build output, never blind string replacement; for bulk stamping
180
+ use the codemod (`npx @bettercms-ai/convert`). Trace the real data flow: where values are
181
+ fetched, normalised, passed as props and rendered. A component that looks like its CMS record but
182
+ still renders hard-coded copy is not connected. Client components never receive server keys.
183
+
184
+ ## 7. Edit-to-render test
185
+
186
+ 1. Pick a safe draft field and record its old value.
187
+ 2. Edit it through the dashboard or the MCP; confirm in preview that the intended leaf or
188
+ attribute changed.
189
+ 3. Confirm sibling instances, global layout and other routes behave according to their scope.
190
+ 4. Restore the value, or keep the change the user asked for.
191
+ 5. After an authorised publish, fetch the public URL cache-busted and check status and body.
192
+ Repeat for a dynamic entry route and a global element.
193
+
194
+ ## Acceptance
195
+
196
+ - The site builds with the repository's build command; routes and assets load.
197
+ - Every claimed editable field has a path: schema, reader, component prop, DOM binding, preview,
198
+ public output.
199
+ - No public secrets, wrong media hosts, unscoped instance ids or hard-coded shadow values.
200
+ - `get_binding_report` for each route is clean, and one value of each bound kind opens the right
201
+ editor in the Visual Editor.
@@ -0,0 +1,106 @@
1
+ ---
2
+ title: "Componentizing a site"
3
+ description: "Turn a derived, fields-only site into sections backed by components: reading the plan, confirming it, applying it, and the repository half with its receipt."
4
+ ---
5
+
6
+ # Componentizing a site, and deciding what each band becomes
7
+
8
+ Read with [components.md](components.md); § numbers below are that file's. Doctrine: §10 of
9
+ `bettercms://playbook/schema`.
10
+
11
+ ## 1. Classify every band first
12
+
13
+ Look at the source component, the rendered band, its CSS and its props on every route before
14
+ deciding. Matching copy or a shared class name does not make two bands one component.
15
+
16
+ | Band | Becomes |
17
+ |---|---|
18
+ | Navigation, footer, a truly sitewide bar | Chrome: a component in the Global Layout (§4) |
19
+ | Same structure and the same editor fields on several pages | One component; layout differences are variants of one `sectionType` |
20
+ | Different content contract (other fields, other order) | Its own family, even when it looks alike |
21
+ | Genuinely unique | Still a section: a one-off `section` block or a single-use component |
22
+ | Card, button, badge, quote inside a section | Part of that section: a `group`/`table` prop, or a nested component |
23
+
24
+ Inside a section, pick the smallest shape that holds the children:
25
+
26
+ - **Small identical items** (FAQ rows, feature bullets, logos): a `group` or `table` prop.
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.
30
+
31
+ Never let a component contain itself, directly or through a child.
32
+
33
+ ## 2. The componentize lane (a site whose pages were derived at import)
34
+
35
+ 1. `get_componentize_plan { summary: true }` first (no values; the full plan is hundreds of KB,
36
+ and `paged: true` reads it 10 pages at a time). It creates nothing and is computed live from
37
+ the PUBLISHED pages. Each field group is one section, and every page gets its OWN components
38
+ (`home-hero`, `about-hero`); a locale copy reuses its default page's. A collection template
39
+ (`pattern: "{slug}"`) is planned once and every prop reads the entry being rendered: only its
40
+ prose body and lists of object rows are sections. Read `pending` for what it will not offer:
41
+ `NO_GROUP_ROOT` (keys are still the derive lane's own, such as `h1-welcome`: rename them into
42
+ 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). Author the real
45
+ sections among those with `create_component` and `set_page_content` instead.
46
+ 2. Show the user the sections, how many components it creates and which pages it rewrites.
47
+ Confirm before writing. A group the user says is not a section: `componentize_sections
48
+ { digest, sections: [groupKey], pageIds, decline: true }` (`decline: false` undoes it). The
49
+ plan is done when it proposes nothing that was not declined.
50
+ 3. `componentize_sections` with the plan's `digest` and `dryRun: true`. Read
51
+ `components.wouldDuplicate` (§8). A 409 `stale-plan` means the site changed: read the plan
52
+ again and confirm again. Then run it for real.
53
+ 4. What it writes: each component as a DRAFT, and each page's draft blocks as an ordered list of
54
+ instances. By default each instance carries `props.bind` and the words stay in the page's
55
+ field group, so bindings and the coverage meter do not move. `copy: "instance"` moves the
56
+ words onto each placement and marks the old fields `origin: "componentized"`; nothing is
57
+ deleted. Running it twice is safe: an existing placement comes back as
58
+ `ALREADY_COMPONENTIZED`.
59
+ 5. `publish_component` for each, then publish the pages. An unpublished component renders
60
+ nothing on the live site.
61
+ 6. In the repo, run the codemod with `--componentize` and a `--receipt` file, commit and push the
62
+ files it wrote, and hand that receipt verbatim to `submit_componentize_receipt`. It records
63
+ each extracted section as the source of the component with the same slug, so no
64
+ `set_component_source` per component is needed. Deploy.
65
+ 7. Output validation, from step 3 of §6.
66
+
67
+ ## 3. Names and groups
68
+
69
+ Name a family by what it is (`Hero`, `CTA`, `Testimonials`) and each variant by how it looks
70
+ (`Hero / Split`, `CTA / Centered`). Never put a page name in a reusable definition; which pages
71
+ use it is a usage question (§8). The componentize lane files a section under its page's slug
72
+ (`home-hero`) but keeps the name the section's own (`Hero`). Keep related fields together in the
73
+ order an editor reads them:
74
+
75
+ ```text
76
+ Hero / Split
77
+ Heading · Description
78
+ Primary button: Label · Link · Style
79
+ Secondary button: Label · Link · Style
80
+ Image: Asset · Alt text
81
+ ```
82
+
83
+ Expose variants and style tokens, never raw class names, CSS strings or HTML.
84
+
85
+ **Name props from the code, not the copy.** An import labels a prop by its role (`Text`,
86
+ `Heading`) with its copy as help text (`e.g. "49"`), and older imports by the copy itself (`.00`).
87
+ Read the section's source and use its own names: `pricing.starterKit.cents` → `Cents`,
88
+ `billingFrequency` → `Billing frequency`. Give every prop help text showing its shape
89
+ (`e.g. "USD / monthly"`). Apply both in one `update_component` call: `propLabel` and
90
+ `propHelpText`, keyed by prop key. Repeated cards (plans, features, team members) are ONE `table`
91
+ prop whose rows are the cards (`Plans`: Name · Description · Price · Cents · Billing frequency ·
92
+ Features · Button), never a set of props per card, so an editor opens one plan at a time and can
93
+ add a third. `get_next_steps` lists `labels-are-copy` until every field is named.
94
+
95
+ ## 4. Proof, per family
96
+
97
+ A write that returned 200 proves nothing about the page. For each family, check in the preview
98
+ and after publish:
99
+
100
+ - one default edit reaches every instance without an override, and no other;
101
+ - one instance override changes only that placement;
102
+ - one variant swap keeps the content (read the swap warnings, §3);
103
+ - one nested child edit stays inside its own instance;
104
+ - one chrome edit reaches every route.
105
+
106
+ Then `get_binding_report` and `get_site_composition` (§9).