@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.
- package/README.md +1 -0
- package/SKILL.md +1 -1
- package/dist/index.js +908 -487
- 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,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.
|