@bettercms-ai/mcp 0.63.1 → 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 +924 -492
  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,123 @@
1
+ ---
2
+ title: "Finish an imported site"
3
+ description: "Take an imported or converted site to fully editable: the gap ledger in .bettercms/ledger.json, the repair order, and the proof loop per route."
4
+ ---
5
+
6
+ # Finish an imported or converted site
7
+
8
+ **Outcome.** Every intended value editable, sections reusable, design faithful, and launch
9
+ verified with evidence. "Fully editable" is a measured result, never an inference from imported
10
+ schemas. New to the project, or no repo on disk yet: [getting-started.md](getting-started.md).
11
+
12
+ ## Inputs and baseline
13
+
14
+ Require the project, source repository or import snapshot, site URL, import manifest, hosting
15
+ mode and the user's goal. Confirm the bound project with `get_project` (hosting mode,
16
+ `authoringPreference`), read `bettercms://playbook/schema`, and inspect the repository and the
17
+ previous import report. If any are unavailable, continue with a read-only gap analysis and name
18
+ the missing evidence.
19
+
20
+ Capture a baseline before editing: routes, screenshots at representative widths, content model
21
+ and entry counts, component and global-layout inventory (`get_site_composition`, `get_layout`),
22
+ media and form inventory, `list_seo_issues`, `get_deploy_status`, `get_binding_report`, and the
23
+ source commit. Keep existing URLs and behaviour unless the user asked for a change.
24
+
25
+ ## The finish line for a convert or build job
26
+
27
+ A conversion is done only when the `convert_site` prompt's loop is: `submit_conversion_receipt`
28
+ answers `complete: true` (no path left in `paths.pending`), and after the deploy
29
+ `get_binding_report` shows `unmatched` at 0, `coverage.pending` empty and, once
30
+ `set_binding_mode` has flipped the site to declared and a second release has run, mode
31
+ `declared`. A build job works from `bettercms://playbook/schema` (the `build_site` prompt carries
32
+ the same flow on a local MCP) and is done when the site is deployed, `get_component_readiness`
33
+ is clean for every component it placed, and `get_next_steps` lists nothing left to do from code.
34
+ Both end in the verify loop of SKILL.md. One converted page, or a receipt with paths still
35
+ pending, is not a finished site; report the numbers, not an impression.
36
+
37
+ Git belongs to the owner. Verification releases go out from the working tree, so the job never
38
+ needs a commit to prove itself. After the final report, ask whether they want the changes
39
+ committed and pushed, committed only, or left uncommitted, and mention that an unpushed
40
+ conversion is lost on the next rebuild from their repository.
41
+
42
+ ## The gap ledger: `.bettercms/ledger.json`
43
+
44
+ Keep the ledger as a file in the repo, not in chat: it survives a context reset, a new session
45
+ and a second agent, and "Continue BetterCMS setup" re-reads it. It holds no secrets. One row per
46
+ actionable gap:
47
+
48
+ | Field | Meaning |
49
+ |---|---|
50
+ | `id`, `category`, `severity` | Stable issue identity and priority |
51
+ | `route`, `sourcePath`, `cmsId` | Where the issue lives |
52
+ | `expected`, `observed` | Evidence, not a vague opinion |
53
+ | `repair`, `owner`, `status` | Planned action and accountable party |
54
+ | `verification` | Link, receipt or screenshot proving the fix |
55
+
56
+ ```json
57
+ { "projectId": "…", "updatedAt": "…", "gaps": [
58
+ { "id": "pricing-cta-href", "category": "bindings", "severity": "high", "route": "/pricing",
59
+ "sourcePath": "src/components/Cta.astro", "cmsId": null,
60
+ "expected": "CTA href editable", "observed": "hard-coded href", "repair": "bind via data-bcms-props",
61
+ "owner": "agent", "status": "open", "verification": null } ] }
62
+ ```
63
+
64
+ Statuses: `open`, `in-progress`, `verified`, `requires-decision`, `blocked-by-platform`,
65
+ `excluded` (with a reason). Never erase a gap after a failed repair: keep the prior observation
66
+ and add the new result. Update the file as each row moves, so a reset loses at most one row.
67
+
68
+ ## Repair order, and the reference for each step
69
+
70
+ | # | Step | Reference |
71
+ |---|---|---|
72
+ | 1 | Routing and data shape: page / collection / entry classification and folders; keep public URLs | [content-modeling.md](content-modeling.md) |
73
+ | 2 | Sections and reuse: every band a section; dedupe compatible patterns into variants and overrides; nav and footer as globals (`get_componentize_plan`, `componentize_sections` on a components project) | [components.md](components.md), [componentize.md](componentize.md) |
74
+ | 3 | Editor contract: field types, labels, validation, Portable Text accept lists, nested groups | [portable-text.md](portable-text.md) |
75
+ | 4 | Design: media, logo roles, fonts and brand tokens, without changing visual intent | [media-and-brand.md](media-and-brand.md) |
76
+ | 5 | Bindings: the site reads BetterCMS data and each intended leaf or attribute is bound; build, preview and runtime agree | [bindings.md](bindings.md), [frameworks.md](frameworks.md) |
77
+ | 6 | Behaviour: forms, links, menus, accordions, modals, consent, custom code | [forms-and-interactions.md](forms-and-interactions.md) |
78
+ | 7 | Discoverability: native SEO, structured data, robots and sitemap, redirects, internal links | [seo.md](seo.md) |
79
+
80
+ Load the step's reference when the step starts, and let it go when the step's rows are verified.
81
+
82
+ ## Proof loop
83
+
84
+ For each route class and component family, test one safe draft edit for every field class:
85
+ plain text, rich text, image, alt text, link, repeatable group, component slot, and form
86
+ reference where present. Confirm the correct element changes in preview and other instances do
87
+ not change by accident. Restore test values. Verify a default change reaches only instances
88
+ without an override.
89
+
90
+ Build with the repository's own build script. Fetch live or staging URLs and check HTTP status
91
+ and the rendered copy, not dashboard status. Check responsive screenshots, keyboard use, form
92
+ status, assets, SEO output and broken links. Record the freshness of `get_site_composition`,
93
+ `get_binding_report` and `list_seo_issues`: a binding report whose `refreshReason` is
94
+ never-generated or outdated is not proof, and on a headless site no BetterCMS release ever
95
+ writes one. Per-route checks fan out well to subagents; have each return its verdict and the
96
+ failing evidence only.
97
+
98
+ **Coverage:** `verified intended fields / discovered intended fields`, plus route, section, asset
99
+ and form counts. A gap is excluded only with a reason, and 100% is never reported while
100
+ exclusions hide core editorial content.
101
+
102
+ Cases that must be tested:
103
+
104
+ - A hero repeated on two pages: one instance override changes only one page; a definition
105
+ default changes the other.
106
+ - A collection detail template with at least two entries and an entry-specific image and SEO
107
+ value.
108
+ - Long and empty copy at desktop and mobile widths.
109
+ - A rich-text custom block, a missing asset, a broken internal link, and a form with an
110
+ unverified destination.
111
+ - A component whose CMS field saves while hard-coded source still renders.
112
+
113
+ ## Launch decision
114
+
115
+ 1. **Completed:** the routes and components repaired, with evidence.
116
+ 2. **Remaining:** gaps that need a product change, an integration credential, a licence
117
+ decision, an editor decision or a manual approval.
118
+ 3. **Recommendation:** ready, ready with named non-critical exceptions, or not ready, with
119
+ reasons and the rollback point.
120
+
121
+ A clean draft audit is not the user's publish authorization. On headless, verify the external
122
+ host's deployed code and content; on managed and hybrid, verify the release with
123
+ `get_deploy_status` and a cache-busted public response ([publishing.md](publishing.md)).
@@ -0,0 +1,203 @@
1
+ ---
2
+ title: "Forms and interactions"
3
+ description: "Attaching, publishing and verifying forms, and interactive sections: menus, accordions, modals and consent."
4
+ ---
5
+
6
+ # Forms and interactive components
7
+
8
+ **Outcome.** Interactive sections keep working after componentization, and editors change the
9
+ intended content without touching unsafe implementation controls.
10
+
11
+ Doctrine: `new_form` prompt, `CM-72`. Component props and slots: [components.md](components.md)
12
+ (its §1 shows where a `form` prop actually renders).
13
+
14
+ ## Contents
15
+
16
+ - [Inventory behaviour](#inventory-behaviour) · [Pattern contracts](#pattern-contracts)
17
+ - [Forms: the rules](#forms-the-rules) · [Editor UX](#editor-ux) · [Acceptance](#acceptance)
18
+ - Forms as code: [pipeline](#1-pipeline) · [field types](#2-field-types-and-server-checks) ·
19
+ [MCP versus dashboard](#3-mcp-versus-dashboard) · [self-hosted wiring](#4-wiring-a-self-hosted-site) ·
20
+ [per-rung facts](#5-per-rung-facts) · [traps](#6-traps)
21
+
22
+ ## Inventory behaviour
23
+
24
+ For each interaction record its markup, state management, dependencies, events, destinations,
25
+ responsive behaviour, accessibility semantics and persistence. Then sort what it exposes:
26
+
27
+ - **Content-configurable:** labels, headings, body, images, links, items, variants.
28
+ - **Behaviour-configurable:** a closed choice such as initial open state, autoplay, modal
29
+ size, display frequency, or which form.
30
+ - **Code-owned:** search algorithms, authentication, payments, booking, complex validation,
31
+ server actions, experiments.
32
+
33
+ Code-owned behaviour never becomes a free text field, and never a promise that editors can
34
+ change it safely.
35
+
36
+ ## Pattern contracts
37
+
38
+ | Pattern | Editor model | Runtime checks |
39
+ |---|---|---|
40
+ | Navigation / mobile menu | global layout component; repeatable link groups; logo; explicit CTA | keyboard and touch, focus return, active route, breakpoint, skip link |
41
+ | Accordion / FAQ | one section with a repeatable Question + narrowed rich-text Answer | button trigger, `aria-expanded`, panel association, focus, single or multi open |
42
+ | Tabs | repeatable label + panel, or a nested component per panel when layouts differ | stable ids, arrow-key navigation, selected state, responsive fallback |
43
+ | Modal / dialog | trigger + modal content in a slot or section; size as a variant | focus trap, Escape, focus return, scroll lock, accessible name, reduced motion |
44
+ | Pop-up / promo bar | sitewide or page component; message, CTA, variant, closed display rules | frequency and dismiss persistence, mobile layout, consent |
45
+ | Carousel | repeatable items or nested slides; limited motion controls | pause, keyboard, announcements, reduced motion, no hidden content loss |
46
+ | Cookie consent | global component with policy links and category labels | consent storage, script gating, change-choice path; legal settings need owner review |
47
+ | Search / filter | labels and options editable; query and index logic code-owned | empty, loading and error states, URL state, keyboard, result accuracy |
48
+
49
+ For a complex one-off component, keep the working code and expose only an intentional prop
50
+ contract. A nested child is edited through a `slot` prop or its own instance; the edit must
51
+ address the child instance, never the parent definition. A `slot` holds exactly one nested
52
+ component; for several heterogeneous children use the section's ordered children. Prevent
53
+ circular nesting.
54
+
55
+ ## Forms: the rules
56
+
57
+ Detect each form's fields, labels, types, required states, validation, consent copy, spam
58
+ controls, endpoint, uploads, confirmation and error states, notifications and integrations.
59
+
60
+ - **A form is a Form document, and a section references it.** Create it as a draft
61
+ (`create_form`) and place it with a `form` block `{ formId }` inside a section. To make WHICH
62
+ form editable per placement, declare a `form` prop on the component targeting that block's
63
+ `props`. The prop stores `{ formId }` and nothing else: never re-model the questions as
64
+ component props, or the same fields live in two places and drift at the first edit.
65
+ - **Draft until published.** A draft form renders nothing live and every public submission is
66
+ refused with 403. Publish it with `publish_form` (it needs the publish permission) only after
67
+ its destination and consent behaviour are tested.
68
+ - **Spam controls** are per form: Turnstile on or off and a honeypot field name. The Turnstile
69
+ secret key is dashboard-only.
70
+ - **Self-hosted renderers** render only forms whose `status` is `"published"` and post to
71
+ `${apiUrl}/api/v1/forms/public/:formId/submissions` through the SDK's `submitForm`, never to the
72
+ forms worker directly and never with an API key in the browser.
73
+ - **Integrations** are configured in the dashboard. A submission classified as spam is stored
74
+ and triggers none of them.
75
+
76
+ Classify each form:
77
+
78
+ 1. **Native:** BetterCMS owns fields, validation, submissions and delivery.
79
+ 2. **Connected provider:** BetterCMS owns the presentation or the reference; a provider owns
80
+ delivery. Keep provider ids; the owner sets up the credentials.
81
+ 3. **Custom action:** keep the existing code path until an adapter passes the same tests.
82
+
83
+ A form rendering is not a working integration. Require a test submission that shows the
84
+ expected record (`list_form_submissions`), the notification, the failure state, spam handling
85
+ and consent handling.
86
+
87
+ ## Editor UX
88
+
89
+ Label fields the way editors think: `Question`, `Answer`, `Add FAQ item`; `Open modal`,
90
+ `Modal title`, `Modal body`, `Close label`. CSS selectors, callbacks, endpoint URLs and secret
91
+ keys stay out of editor fields. Expose curated variants and behaviour switches whose effect is
92
+ obvious. An empty optional field should remove its element cleanly.
93
+
94
+ ## Acceptance
95
+
96
+ - Interactions work with keyboard, touch, screen-reader semantics, mobile layouts and reduced
97
+ motion where relevant.
98
+ - Editing a label, body or item changes the intended instance without breaking its state.
99
+ - Nested elements keep their order, ids and edit scope.
100
+ - Every form is `draft`, `ready for verification` or `verified live`; only the last has an
101
+ end-to-end submission receipt.
102
+ - Consent-dependent scripts do not run before consent is given.
103
+
104
+ ## Forms as code
105
+
106
+ The pipeline with the code that enforces each hop.
107
+
108
+ ### 1. Pipeline
109
+
110
+ | # | Hop | Code |
111
+ |---|---|---|
112
+ | 1 | Define: `create_form` / `update_form`, or the dashboard. Postgres `forms` row, `status` defaults to `draft` | `packages/db/src/schema/forms.ts:35` |
113
+ | 2 | Publish: `publish_form`, or the dashboard Forms tab. Both need publish permission | `src/routes/management/forms.ts:276`, `src/routes/admin/forms.ts:529`, `:530` |
114
+ | 3 | Place: a `form` block `{ formId }` inside a section | `packages/types/src/block.ts:253`, `:257` |
115
+ | 4 | Render: hosted renderer, Next `BcmsForm`, or Astro `BcmsForm.astro` | `packages/next/src/form.tsx:74` |
116
+ | 5 | Submit: `POST /api/v1/forms/public/:formId/submissions` with `{ data, "cf-turnstile-response"? }` | `src/routes/delivery/forms-public.ts:48`, `src/lib/validation/schemas/forms.ts:297`, `:298` |
117
+ | 6a | Ingress: rate limit per client IP (10), before anything else | `src/routes/delivery/forms-public.ts:61`, `src/lib/forms/submit-gate.ts:23` |
118
+ | 6b | Ingress: form lookup, 404 when missing; draft form, 403 | `src/routes/delivery/forms-public.ts:65`, `:71` |
119
+ | 6c | Ingress: Turnstile; 403 on a failed token; skipped when not configured | `src/routes/delivery/forms-public.ts:80`, `:89` |
120
+ | 6d | Ingress: field validation, 400 `{ code: "FORM_FIELD_INVALID", errors, issues }` | `src/lib/forms/submit-gate.ts:75` |
121
+ | 6e | Ingress: spam classification (honeypot) | `src/lib/forms/spam.ts:40` |
122
+ | 6f | Ingress: rate limit per workspace (100 per 60 s), one bucket shared with the API-key route | `src/lib/forms/submit-gate.ts:80` |
123
+ | 7 | Store: forms worker writes the submission to D1 (`FORMS_DB`); CRM intents to `crm_intents` | `workers/bcms-forms-worker/src/index.ts:2`, `workers/bcms-forms-worker/migrations/0004_crm_intents.sql:1` |
124
+ | 8 | Inbox side effects (non-spam only): contact upsert, integrations, notification | `src/routes/delivery/forms-public.ts:120`, `:144`, `:158` |
125
+ | 9 | CRM delivery drain: `POST /api/v1/internal/crm/drain` | `src/index.ts:976`, `src/routes/internal/crm.ts:4` |
126
+ | 10 | Read: `list_form_submissions`, dashboard inbox | `packages/mcp/src/tools.ts:1600` |
127
+
128
+ ### 2. Field types and server checks
129
+
130
+ Thirteen types exist (`src/lib/validation/schemas/forms.ts:61`, `:62`, `:65`). No file upload.
131
+
132
+ | Type | The server checks |
133
+ |---|---|
134
+ | `email` | Address shape; optional `emailPolicy: "business"` rule |
135
+ | `number` | Numeric; optional `min` / `max` rules |
136
+ | `select`, `radio`, `checkboxes` | Value is one of the listed options; a field saved with an empty options list accepts nothing |
137
+ | `consent` | Must be truthy |
138
+ | `phone` | Digits, spaces, `+ ( ) -`; stricter with `phoneFormat: "e164"` |
139
+ | `text` / `textarea` | At most 1000 / 10000 characters; optional `pattern` rule |
140
+ | `url` | Starts with `http://` or `https://` |
141
+ | `date` | `YYYY-MM-DD` |
142
+ | `checkbox`, `hidden` | Nothing beyond `required` |
143
+
144
+ Any field with `required` is checked for presence. Every value must be a plain value or a flat
145
+ list of them; a nested object is refused with rule `scalar`. Each refusal is
146
+ `FORM_FIELD_INVALID` with `issues: [{ field, rule, message }]`. Keys that are not fields are not
147
+ refused.
148
+
149
+ The API-key submit route runs the same validation, spam check and workspace limit, but no per-IP
150
+ limit (`src/routes/delivery/forms.ts:106`).
151
+
152
+ Definition refusals (`create_form` / `update_form`) carry `FORM_DEFINITION_INVALID`: duplicate or
153
+ over-long keys, too many fields or options, and a `showIf` that names a field not on the form.
154
+ That last one is refused on create and kept as sent on update.
155
+
156
+ Rules:
157
+
158
+ - Send only the form's field keys (plus the honeypot and Turnstile token), each as a plain value.
159
+ - Read `issues[].field` and `issues[].rule` to fix a refused submission; retrying unchanged returns
160
+ the same refusal.
161
+
162
+ ### 3. MCP versus dashboard
163
+
164
+ | Operation | Where |
165
+ |---|---|
166
+ | List, read, create, update, delete forms | MCP: `list_forms`, `get_form`, `create_form`, `update_form`, `delete_form` |
167
+ | Read and delete submissions | MCP: `list_form_submissions`, `delete_form_submission` |
168
+ | Publish | MCP: `publish_form`. Unpublish: dashboard Forms tab |
169
+ | Integrations (Google Sheets, Slack, webhook, email, HubSpot, Salesforce) | Dashboard |
170
+ | Turnstile on/off, honeypot field name, notification emails | MCP: `create_form` / `update_form` (`turnstileEnabled`, `honeypotField`, `notifyEmails`). The Turnstile secret key: dashboard only; sending `turnstileSecretKey` through the API is refused with 403 `FORM_SECRET_DASHBOARD_ONLY` |
171
+
172
+ `update_form` with `fields` replaces the whole array: send every field you want to keep.
173
+
174
+ ### 4. Wiring a self-hosted site
175
+
176
+ - **Next**: `getForm('Name')` (or an id) reads the form from `bcms-content.json`; render
177
+ `<BcmsForm form={form} />`. It renders the honeypot input under the form's own
178
+ `honeypotField` name, reads the `cf-turnstile-response` token when a Turnstile widget is on the
179
+ page, and submits through the SDK `submitForm`.
180
+ - **Astro**: `BcmsForm.astro` renders the same hidden honeypot input and sends
181
+ `cf-turnstile-response` when present.
182
+ - Neither component checks `status`. Render a form only when `form.status === "published"`.
183
+ - The honeypot name is per form. Never hardcode it; read `form.honeypotField`.
184
+
185
+ ### 5. Per-rung facts
186
+
187
+ | Situation | Answer |
188
+ |---|---|
189
+ | `FORMS_WORKER_URL` set but blank on the API | 503 `FORMS_WORKER_NOT_CONFIGURED`. A blank never falls back to the default worker |
190
+ | Worker without `FORMS_SHARED_SECRET` | 503 `FORMS_WORKER_SECRET_UNSET` on data routes |
191
+ | No Turnstile secret for the form or env | Token not verified; the submission continues |
192
+ | Rate-limit store unavailable | Limits allow the request |
193
+ | Management delete of a submission not in that form | 404 `FORM_SUBMISSION_NOT_FOUND` |
194
+
195
+ ### 6. Traps
196
+
197
+ | Symptom | Cause | Rule |
198
+ |---|---|---|
199
+ | Every submission 403 | Form still draft | Call `publish_form` |
200
+ | Submissions 503 on a dev or preview API | Worker URL blank or worker secret unset | Report the code to the owner; do not change the site |
201
+ | Lead stored but no contact, integration or notification | Classified as spam (honeypot filled); side effects run for inbox leads only | Keep the honeypot input hidden and empty |
202
+ | Submission 400 `FORM_FIELD_INVALID` rule `scalar` | A field value sent as an object | Send plain values; checkboxes as a flat list |
203
+ | Webhook integration never fires | Target is not a public http(s) host | Use a public https endpoint |
@@ -0,0 +1,149 @@
1
+ ---
2
+ title: "Framework wiring"
3
+ description: "Hosting modes, the env map, TypeGen with bettercms-codegen, value shapes, revalidation tags, the media host, static snapshots and the verify steps shared by every framework."
4
+ ---
5
+
6
+ # Framework wiring, hosting modes and media
7
+
8
+ Doctrine (what a headless or imported site needs first): §10 and §13 of
9
+ `bettercms://playbook/schema`. This is the code side shared by both adapters. The repo root has
10
+ `astro.config.*`: also read [astro.md](astro.md); `next.config.*`: [next.md](next.md); neither:
11
+ [other-frameworks.md](other-frameworks.md) (`@bettercms-ai/sdk` `createClient`), and classify the
12
+ codebase first ([bindings.md](bindings.md) §6). Read the adapter reference before writing code; env names differ between the two.
13
+
14
+ ## Hosting modes
15
+
16
+ A project's `hostingMode` is `managed`, `hybrid` or `headless`.
17
+
18
+ | Mode | Who builds and serves production | Rule |
19
+ |---|---|---|
20
+ | managed | BetterCMS | `deploy_project` / `create_deploy_upload` are the deploy path |
21
+ | hybrid | BetterCMS, and the client also reads content headlessly | Treat exactly like managed for builds and hosting |
22
+ | headless | The client's own host (Vercel, Netlify, Cloudflare Pages) | Never call `deploy_project` or `create_deploy_upload`; never add `bcms-rebuild.yml`. Deploy = push to the repo the host builds |
23
+
24
+ Headless refusals, by code:
25
+
26
+ | Code | Cause | Fix |
27
+ |---|---|---|
28
+ | `HEADLESS_NO_BUILD` | A build, release or deploy was sent for a headless project | Stop; push to the provider instead |
29
+ | `HEADLESS_NEEDS_CUSTOM_HOST` | GitHub connect without a provider host | Add Vercel, Netlify or Cloudflare Pages under Custom hosting in the dashboard, then connect |
30
+ | `HEADLESS_ARTIFACT_TARGET` | The custom host expects builds from BetterCMS | Switch the host to build from the repo |
31
+
32
+ **Server rendering is one change across four systems, or none.** An SSR build (`output: 'server'`
33
+ with `@astrojs/node`) emits `dist/client` + `dist/server` and no `index.html` at the root. A
34
+ managed project serves that from its build directory only when the Hosting page is set to
35
+ **Run as a server app** — a dashboard setting no tool can change. Until it is set, the server never
36
+ runs: only prerendered pages and assets are served, and every other route answers 404 while the
37
+ build, the release and `get_deploy_status` read green. The deploy workflow refuses an SSR build
38
+ pointed to by `BCMS_BUILD_DIR` (`STATIC_HOST_GOT_SSR_ARTIFACT`) and warns when it packages one it
39
+ found itself. So: switch Hosting first (ask the owner), then ship the build, then fetch the URL.
40
+
41
+ ## Env map
42
+
43
+ | Package | Variable | Purpose |
44
+ |---|---|---|
45
+ | `@bettercms-ai/astro` | `PUBLIC_BCMS_API_URL`, `PUBLIC_BCMS_WORKSPACE`, `PUBLIC_BCMS_PROJECT_ID` | Where reads go |
46
+ | `@bettercms-ai/astro` | `PUBLIC_BCMS_MEDIA_URL`, `PUBLIC_BCMS_STUDIO_URL` | Media host, editor origin |
47
+ | `@bettercms-ai/astro` | `BCMS_API_KEY`, `BCMS_DRAFT_SECRET` | Server-only key; draft cookie signing |
48
+ | `@bettercms-ai/next` (snapshot bin) | `BETTERCMS_API_URL`, `BETTERCMS_API_KEY`, `BETTERCMS_WORKSPACE` | Pull `bcms-content.json` |
49
+ | `@bettercms-ai/next` | `BETTERCMS_PREVIEW=1` | Read live instead of the snapshot in a preview build |
50
+ | `@bettercms-ai/codegen` | `BETTERCMS_API_URL`, `BETTERCMS_API_KEY` | Fetch models for typegen |
51
+ | `@bettercms-ai/mcp` | `BETTERCMS_API_URL` | A rung name `prod`, `dev`, `local`, or a full URL |
52
+
53
+ Rule: Astro names start `PUBLIC_BCMS_`/`BCMS_`; Next and codegen names start `BETTERCMS_`. Never
54
+ copy one adapter's `.env` into the other. Keys never go in a `PUBLIC_` variable.
55
+
56
+ ## Types
57
+
58
+ Run `bettercms-codegen` after every model or field change and commit the generated file. Why:
59
+ templates typed against an old schema compile and then read `undefined` for a renamed field.
60
+
61
+ 1. **Generate into the compiled tree:**
62
+ `npx @bettercms-ai/codegen --out src/bettercms.generated.ts`. The default output is
63
+ `bettercms.generated.ts` at the repo root, which a `tsconfig.json` whose `include` is `src/**`
64
+ never type-checks. `--bindings-out <path>` also writes the Live Preview bindings module,
65
+ `--components-out <path>` typed React `RichText`/`Image` components.
66
+ 2. **Auth:** `BETTERCMS_API_KEY` (or `--key`) must be a management key; `BETTERCMS_API_URL` (or
67
+ `--api-url`) picks the rung. The key is a secret: a CI secret or a local `.env` that git
68
+ ignores, never a `PUBLIC_` variable.
69
+ 3. **Commit, and let CI check.** Output is sorted and byte-stable, so a committed file diffs
70
+ cleanly. In CI, run the same command, then `git diff --exit-code` on the file: a failure means
71
+ the schema moved and nobody regenerated. Generating only in CI (not committed) also works, but
72
+ then every local typecheck needs the key.
73
+ 4. **Never edit the file by hand**; it says so in its header, and the next run overwrites it.
74
+ 5. **Without a key** (an agent session, a quick check), `get_content_types` returns the same
75
+ generated types over the MCP. Paste them only as a stopgap; the CLI is the repeatable path.
76
+
77
+ ## Value shapes templates read
78
+
79
+ - **Richtext**: `{ format, value, html }`. Render `value` (Portable Text) or `html`; write rules
80
+ are in [portable-text.md](portable-text.md).
81
+ - **Image**: an object `{ id, url, name, altText }`. Read `.url` and `.altText`, never treat the
82
+ field as a string.
83
+ - **Repeater / zoned array**: stored as `{ repeatable: [...] }`. Read `field.repeatable`, not
84
+ `field` as an array. A bare array written to such a field is wrapped on write.
85
+
86
+ ## Revalidation
87
+
88
+ - On publish BetterCMS sends a webhook signed with `X-BetterCMS-Signature` (HMAC-SHA256 of the raw
89
+ body, using the webhook secret configured on the project in the dashboard).
90
+ - The backend invalidates tags `bcms:{workspaceSlug}:all` and `bcms:{workspaceSlug}:{type}:{slug}`.
91
+ The SDK tags reads `bcms:{workspace}:{resource}:{id}`.
92
+ - Rule: tag every cached read with those exact strings. A tag you invent (`post:${slug}`) is never
93
+ revalidated and the page stays stale until the cache window expires.
94
+
95
+ ## Media host
96
+
97
+ A stored image value is `{ id, url, name, altText }`. `url` is absolute and already right for the
98
+ plan: platform CDN, API origin, or the project's own CDN base. Rendering `url` as-is works on
99
+ every plan.
100
+
101
+ Resized images are a second lane. `@bettercms-ai/image-url` and `BcmsImage.astro` mint
102
+ `<mediaUrl>/media/<id>?w=…`, and the backend serves that for every plan, including a project that
103
+ keeps media in its own bucket.
104
+
105
+ - Hand the builder the asset object or its `id`, never the `url` string. A URL with no `/media/`
106
+ segment (own-bucket plan) resolves its last path segment as the id and 404s.
107
+ - `mediaUrl` is the origin that owns the asset ids: `mediaBaseUrl` from `get_project`, always.
108
+ The package default is right only when `mediaBaseUrl` is the production CDN; any other value —
109
+ the API origin, another rung — goes in `PUBLIC_BCMS_MEDIA_URL`. On a backend too old to return
110
+ `mediaBaseUrl`, read one `get_media` url: if it contains `/media/`, its origin is the value; if
111
+ it does not (own bucket), use this project's API URL.
112
+ - `set_media_delivery` changes future mints only, and only between the CDN and the API origin;
113
+ stored URLs keep resolving. It is the owner's call. After it, set `PUBLIC_BCMS_MEDIA_URL` to the
114
+ same origin.
115
+ - A stored `url` that itself answers 404 is storage or the wrong rung, not the template. Report
116
+ it; never rewrite URLs in content.
117
+ - A media host from another project, a document or the conversation is never a value for
118
+ `mediaUrl`.
119
+
120
+ ## Static builds
121
+
122
+ A static build reads content from the `bcms-content.json` snapshot. Produce the snapshot before
123
+ the build, every build. Details per adapter are in the references.
124
+
125
+ Producing it is half the job. A source file must READ it — `readPages`, `getEntry`, or the
126
+ generated reader. A build that fetches the snapshot and never reads it still renders the
127
+ hard-coded copy, so every edit saves, publishes and changes nothing on the page. Grep the source
128
+ for a reader before calling a site wired.
129
+
130
+ Build with the repo's own `build` script (`pnpm build`, `bun run build`), never the framework CLI
131
+ directly: a repo's script often adds a type check or a post-process step the CLI skips.
132
+
133
+ ## Verify
134
+
135
+ 1. Fetch the live URL, cache-busted, and confirm a 200 whose body contains copy you just
136
+ published. Every other check below is a control-plane read and can be green on a dead site.
137
+ 2. The site builds with the snapshot present and renders published content.
138
+ 3. Publish a small change; the page updates (revalidation reached the right tag) or the host
139
+ rebuilds (headless). Editable means that round trip was shown, not that fields exist.
140
+ 4. `get_binding_report` for a changed route: `coverage.pending` empty. `refreshReason:
141
+ never-generated` means no BetterCMS release wrote one — on a headless site it never will, so
142
+ step 1 is the proof there. `outdated` means the report predates the current shape; the next
143
+ release rewrites it.
144
+ 5. **Smoke-test both render paths** on the dev server or a preview deploy: load a list page,
145
+ click through to a detail page with the site's own link (never paste the URL), and read the
146
+ browser console: clean, no `ReferenceError`, no hard reload to `/`. Then reload that detail
147
+ page from the URL bar. Why: a client-side route transition loads lazy chunks that read env and
148
+ the snapshot through a different code path than the server render, and that is where a
149
+ missing public variable or a server-only import surfaces.
@@ -0,0 +1,106 @@
1
+ ---
2
+ title: "Getting started"
3
+ description: "Set up or resume a BetterCMS project: connect the MCP, pick or create the project, import or build the site, then publish and verify; the resume check for 'Continue BetterCMS setup'."
4
+ ---
5
+
6
+ # Getting started on a BetterCMS project
7
+
8
+ The dashboard's launcher prompt points here. Its **Context** block holds what the dashboard
9
+ already knows: project name and id, framework, source repo, and the job. Trust it, and ask only
10
+ for what it leaves out.
11
+
12
+ ## Communication style
13
+
14
+ Be succinct. Say what you did ("Converted 12 routes; 3 values still unmatched on /pricing"), ask
15
+ direct questions, and skip narrating what you are about to do. Give numbers, not adjectives.
16
+
17
+ Before starting, tell the user they can pause and resume anytime by saying
18
+ **"Continue BetterCMS setup"**.
19
+
20
+ **RESUME TRIGGER.** On "Continue BetterCMS setup", read `get_project`, `get_next_steps` and
21
+ `get_binding_report` for the project and the gap ledger at `.bettercms/ledger.json` if it exists,
22
+ find the first phase below whose check is not met, say which one in one line, and resume there.
23
+ Never redo a phase whose check already holds.
24
+
25
+ ## An existing site (imported from GitHub or a ZIP)
26
+
27
+ ### Phase 1 — Connect and confirm
28
+
29
+ Check: the MCP answers for this project id, and this folder is its repo.
30
+
31
+ 1. Read the Connecting section of SKILL.md before your first tool call: a first-call 401 is the
32
+ sign-in, and a project missing from `list_projects` means the wrong workspace.
33
+ 2. `get_project` with the id from Context. Pass `projectId` on every project tool.
34
+ 3. Confirm this folder holds the site's `package.json` and source. If it does not:
35
+ - No repo on disk yet: clone it with `npx @bettercms-ai/cli@latest init --project-id <id>`
36
+ in the folder where the repo should live. It signs in, clones or downloads the source into
37
+ a new folder (named for the project unless you pass `--dir <path>`), installs dependencies,
38
+ pre-fills `.env`, and sets up these skills and the hosted MCP in the user's editors
39
+ (`npx -y @bettercms-ai/install@latest --global --mcp`). If it warned that the agent setup
40
+ failed, run the command the warning prints, inside the clone.
41
+ - Either way, stop and ask the user to restart you **inside** the repo folder: an MCP entry
42
+ added while you run loads only when the agent starts, and every step below works on that
43
+ folder's source.
44
+
45
+ ### Phase 2 — The questions the MCP requires, and no others
46
+
47
+ Check: `get_project` shows `authoringPreference` set and the user has answered media delivery.
48
+
49
+ Ask exactly these two, each through its tool, and never answer them yourself:
50
+
51
+ 1. **Authoring**: `set_authoring_preference` (components or fields). Recommend components for a
52
+ marketing site. It is asked once per project and can be re-answered.
53
+ 2. **Media delivery**: `set_media_delivery` (the BetterCMS CDN, recommended, or the API origin).
54
+
55
+ Everything else has a default. Do not ask about it; decide, then report what you decided:
56
+
57
+ - **Scope is the whole site**: every page, section, heading, paragraph, list item, link and image
58
+ the site renders. Not the home page first, not "the main copy".
59
+ - **Sections, their names and componentizing**: [components.md](components.md).
60
+ - **Collections vs singletons**: [content-modeling.md](content-modeling.md).
61
+ - **Field types and rich text**: [portable-text.md](portable-text.md).
62
+
63
+ ### Phase 3 — Convert and componentize
64
+
65
+ Check: `submit_conversion_receipt` returned complete and `get_componentize_plan` proposes
66
+ nothing more.
67
+
68
+ 1. Explore the repo (the "Read when" column of SKILL.md's reference table names the signals) and
69
+ read `get_site_composition`.
70
+ 2. If `list_env_vars` reports `missing` variables, run `npx @bettercms-ai/cli@latest env push`
71
+ in the repository: it sets them from the user's own .env files without the values passing
72
+ through you. If they are not found there and the site loads its content from another CMS,
73
+ move that content into BetterCMS ([external-content.md](external-content.md)). Never ask the
74
+ user for a key or read one: a site left depending on missing keys renders empty or crashes.
75
+ 3. Run the `convert_site` prompt end to end. Its finish line is the only one; repeat until the
76
+ receipt is complete, never stop at a partial one.
77
+ 4. When the answer was components: `get_componentize_plan`, confirm it with the user, apply it
78
+ with `componentize_sections`, then the repository half and `submit_componentize_receipt`
79
+ ([componentize.md](componentize.md)).
80
+ 5. Work the rest through [finish-site.md](finish-site.md) (gap ledger, repair order, proof loop).
81
+
82
+ ### Phase 4 — Publish and verify
83
+
84
+ Check: the deployed site passes the numbers below on every route.
85
+
86
+ 1. Deploy the way this project deploys: `deploy_project` on BetterCMS hosting; on a headless
87
+ project, push to the repo its host builds (never `deploy_project` there).
88
+ 2. `get_binding_report` is the finish line: zero unmatched, an empty `coverage.pending`, and
89
+ `unaddressable` below 2% on each route. `refreshReason: "never-generated"` on headless means
90
+ fetch the live site instead.
91
+ 3. Run the verify loop in SKILL.md against the live URL, then the two-path smoke test (click
92
+ through to a detail page, clean console, direct reload) in [frameworks.md](frameworks.md)
93
+ Verify step 5.
94
+ 4. Send a coverage report with its denominators, per route: values bound out of values found
95
+ (bound + unmatched), unaddressable text out of visible text, and routes converted out of
96
+ `builtRoutes`. A percentage with no denominator is not a report.
97
+ 5. Anything only the dashboard can do: name the exact click.
98
+
99
+ ## A new site (nothing built yet)
100
+
101
+ 1. Phase 1 above, then ask the authoring question (`set_authoring_preference`) before any schema
102
+ write.
103
+ 2. Read `bettercms://playbook/schema`. Plan routes and collections first
104
+ ([site-blueprints.md](site-blueprints.md)), then build sections ([components.md](components.md)).
105
+ 3. When a repo exists, clone it with the `init` command above, restart inside it, and continue
106
+ from Phase 3.