@bettercms-ai/mcp 0.63.2 → 0.64.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/SKILL.md +1 -1
- package/dist/index.js +912 -489
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +175 -0
- package/dist/skills/bettercms/references/astro.md +67 -0
- package/dist/skills/bettercms/references/bindings.md +201 -0
- package/dist/skills/bettercms/references/componentize.md +106 -0
- package/dist/skills/bettercms/references/components.md +228 -0
- package/dist/skills/bettercms/references/content-experiments.md +62 -0
- package/dist/skills/bettercms/references/content-modeling.md +185 -0
- package/dist/skills/bettercms/references/editor-chrome.md +88 -0
- package/dist/skills/bettercms/references/external-content.md +78 -0
- package/dist/skills/bettercms/references/finish-site.md +123 -0
- package/dist/skills/bettercms/references/forms-and-interactions.md +203 -0
- package/dist/skills/bettercms/references/frameworks.md +149 -0
- package/dist/skills/bettercms/references/getting-started.md +106 -0
- package/dist/skills/bettercms/references/localization.md +81 -0
- package/dist/skills/bettercms/references/media-and-brand.md +142 -0
- package/dist/skills/bettercms/references/next.md +93 -0
- package/dist/skills/bettercms/references/other-frameworks.md +66 -0
- package/dist/skills/bettercms/references/portable-text.md +206 -0
- package/dist/skills/bettercms/references/programmatic-seo.md +98 -0
- package/dist/skills/bettercms/references/publishing.md +146 -0
- package/dist/skills/bettercms/references/seo.md +156 -0
- package/dist/skills/bettercms/references/shopify-headless.md +52 -0
- package/dist/skills/bettercms/references/site-blueprints.md +250 -0
- package/dist/skills/bettercms/references/site-review.md +73 -0
- package/dist/skills/bettercms/references/write-limits.md +84 -0
- package/package.json +2 -2
|
@@ -0,0 +1,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).
|