@bettercms-ai/mcp 0.20.1 → 0.22.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/SKILL.md +71 -4
- package/dist/index.js +236 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/SKILL.md
CHANGED
|
@@ -31,6 +31,65 @@ Within a page's own schema, destructure into a tree: plain fields, **group** (on
|
|
|
31
31
|
object), **repeater** (a repeatable array). Never a 1-item repeater, never a repeater of a
|
|
32
32
|
single text (that is `array`).
|
|
33
33
|
|
|
34
|
+
### A page is a list of SECTIONS. Never a list of loose blocks.
|
|
35
|
+
|
|
36
|
+
The top level of a page's `blockJson` holds only `component` blocks or `section` blocks —
|
|
37
|
+
never a bare `heading`/`text`/`image`/`button`/`spacer`.
|
|
38
|
+
|
|
39
|
+
**Why it matters, concretely:** the visual editor derives one section per TOP-LEVEL block. A
|
|
40
|
+
hero authored as four loose blocks (headline, lede, two CTAs) becomes four separate sections,
|
|
41
|
+
each with its own move / duplicate / delete controls and its own "Add section here" gap — and
|
|
42
|
+
because each top-level block gets its own block box, the two call-to-action buttons stop being
|
|
43
|
+
inline siblings and **stack onto separate lines**. That is not a styling bug to chase in CSS;
|
|
44
|
+
it is the page telling you it was authored flat.
|
|
45
|
+
|
|
46
|
+
```jsonc
|
|
47
|
+
// ✅ one section, four children — one set of controls, CTAs side by side
|
|
48
|
+
{ "type": "section", "id": "hero",
|
|
49
|
+
"style": { "bg": "surface", "paddingTop": 96, "paddingBottom": 96, "contentWidth": "default" },
|
|
50
|
+
"props": { "children": [
|
|
51
|
+
{ "type": "heading", "id": "hero-title", "props": { "text": "Strategic Planning.", "level": 1 } },
|
|
52
|
+
{ "type": "text", "id": "hero-lede", "props": { "html": "<p>One place to plan, track and ship.</p>" } },
|
|
53
|
+
{ "type": "button", "id": "hero-cta", "props": { "text": "Start a project", "href": "/contact" } },
|
|
54
|
+
{ "type": "button", "id": "hero-cta2", "props": { "text": "See our work", "href": "/work", "variant": "secondary" } }
|
|
55
|
+
] } }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Two CTAs are two sibling `button` blocks.** Not a `columns` block — `columns` renders
|
|
59
|
+
`grid-template-columns: repeat(N, 1fr)`, so each CTA would stretch to half the container.
|
|
60
|
+
Buttons are inline-level and flow side by side on their own. This is what the shipped
|
|
61
|
+
`builtin:hero-centered` section does.
|
|
62
|
+
|
|
63
|
+
### `section` block or `component`? Decide by REUSE.
|
|
64
|
+
|
|
65
|
+
| | use it when | the catch |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| **`component`** (+ `sectionType`) | the band appears on more than one page, or needs layout variants — a Hero that is "Centered" on Home and "Two-column" on About | its children render **without field bindings**, so their text is **not click-to-edit on the canvas**. It is edited through the component's declared `props` in the section dock, where an unset prop shows empty and inherits the definition's default. Authors who want the current copy visible in the dock should set the prop explicitly rather than relying on the inherited default. |
|
|
68
|
+
| **`section` block** | a genuinely one-off band on a single page | no family name and no variant switcher, and the editor's "Add a section" picker cannot insert one — only components |
|
|
69
|
+
|
|
70
|
+
`component` is the shape the product's reuse machinery is built around: variants, the section
|
|
71
|
+
picker and the family name all key off `componentId`. So reach for it whenever a band actually
|
|
72
|
+
recurs — and when you do, **declare a prop for every string, link and image a marketer will
|
|
73
|
+
ever touch**. A component with un-propped editable copy is the defect, not the component.
|
|
74
|
+
|
|
75
|
+
### Don't hand-write a hero. Start from a built-in section.
|
|
76
|
+
|
|
77
|
+
There are built-in section components — `hero-centered`, `hero-split`, `feature-grid-three`,
|
|
78
|
+
`feature-grid-two`, `faq-list`, `stats-row`, `logo-cloud-row`, `testimonial-quote`,
|
|
79
|
+
`case-study-story`, `cta-banner`, `cta-split`, `pricing-three`, `contact-simple` — available to
|
|
80
|
+
every project (`list_components` returns them; they have no `projectId`). Every one is already
|
|
81
|
+
rooted in a `section` block with its editable leaves declared as props, and `Hero` ships two
|
|
82
|
+
variants, so the shapes above are worked examples you can read.
|
|
83
|
+
|
|
84
|
+
Two sanctioned recipes, and which one you want follows the reuse rule above:
|
|
85
|
+
|
|
86
|
+
1. **Inline it** — copy the builtin's `section` blockJson onto the page as a `section` block and
|
|
87
|
+
substitute your values. Standard path for a one-off band; children stay click-to-edit.
|
|
88
|
+
2. **Reference it** — place `{ type: "component", props: { componentId, overrides } }`. Use this
|
|
89
|
+
when the band recurs across pages or needs the variant switcher.
|
|
90
|
+
|
|
91
|
+
Authoring a hero's JSON from scratch is the slow way to reinvent recipe 1 with more mistakes.
|
|
92
|
+
|
|
34
93
|
The full decision tree, section anatomy, variant rules and build order live in the
|
|
35
94
|
`propose_schema` prompt and in the `bettercms://playbook/schema` resource.
|
|
36
95
|
|
|
@@ -52,10 +111,18 @@ Tools: `list_forms`, `get_form`, `create_form`, `update_form`.
|
|
|
52
111
|
## Components (reusable Webflow-style symbols)
|
|
53
112
|
Tools: `list_components`, `get_component`, `create_component`, `update_component`.
|
|
54
113
|
|
|
55
|
-
1. Design the `blockJson` tree — an array of blocks `{ type, id, props }`.
|
|
56
|
-
`heading {text, level}`, `text {
|
|
57
|
-
`
|
|
58
|
-
|
|
114
|
+
1. Design the `blockJson` tree — an array of blocks `{ type, id, props }`. The full set:
|
|
115
|
+
`heading {text, level}`, `text {html}`, `richtext {html}`, `image {src, alt}`,
|
|
116
|
+
`button {text, href, variant?}`, `spacer {height}`, `video {url}`,
|
|
117
|
+
`columns {columns: block[][], gap}`, `section {children: block[]}`,
|
|
118
|
+
`slider {slides}`, `tabs {tabs}`, `navbar {links, logo?, cta?}`, `footer {columns, copyright?}`,
|
|
119
|
+
`form {formId}`, `component {componentId, overrides?}`, `collection {…}`.
|
|
120
|
+
`section`, `columns`, `slider` and `tabs` nest child blocks. Every block needs a stable
|
|
121
|
+
unique `id`.
|
|
122
|
+
|
|
123
|
+
⚠️ `text` and `richtext` carry **`html`**, never `text`. `props.html` is required with no
|
|
124
|
+
default, so `{ text: "…" }` is a **400 on every save** — not a silent strip. This file said
|
|
125
|
+
`text {text}` until 2026-08-20; if you learned it from an older copy, unlearn it.
|
|
59
126
|
2. Optional `props` — overridable fields `{ key, label, target: { blockId, path }, type,
|
|
60
127
|
defaultValue? }` (type: `text|richtext|image|url|boolean`) so each instance can be
|
|
61
128
|
customized.
|
package/dist/index.js
CHANGED
|
@@ -314,7 +314,9 @@ import { BetterCMS } from "@bettercms-ai/sdk";
|
|
|
314
314
|
|
|
315
315
|
// src/tools.ts
|
|
316
316
|
import { z } from "zod";
|
|
317
|
-
|
|
317
|
+
|
|
318
|
+
// ../types/src/component.ts
|
|
319
|
+
var SECTION_DOCTRINE = "STRUCTURE (separate from schema): a page is composed of SECTIONS. NEVER build a page out of loose top-level heading/text/image/button/spacer blocks \u2014 they cannot be moved, duplicated or swapped as a unit, the visual editor cannot outline or name them, and every one of them becomes its own section in the editor. A hero of a headline, a lede and two CTAs is ONE section, not four. TWO SHAPES, and the choice is about REUSE. (1) A band that appears on more than one page, or that needs layout variants, is a COMPONENT with a `sectionType` \u2014 see create_component. Components sharing a `sectionType` are that section's VARIANTS (one Hero: 'Centered' for the home page and 'Two-column' for about, same prop keys so a swap keeps the content). This is also the only shape the editor's 'Add a section' picker can insert, and the only one that gets a family name and a variant switcher. (2) A genuinely one-off band on a single page is a `section` BLOCK whose `props.children` hold its blocks. THE TRADEOFF, stated in the present tense because it is real today: a component's children render WITHOUT field bindings, so their text is NOT click-to-edit on the canvas \u2014 it is edited through the component's declared `props` in the section dock. A `section` block's children stay click-to-edit. So when you choose a component, DECLARE A PROP for every string, link and image a marketer will ever touch; a component with un-propped editable copy is the defect, not the component. In the dock an unset prop shows EMPTY and inherits the definition's default, so set props explicitly when you want the current copy visible there. Do not hand-write a band's JSON: start from a built-in section component (list_components returns them with no projectId \u2014 hero-centered, hero-split, feature-grid-three, cta-banner and nine more), each already rooted in a `section` block with its editable leaves declared as props. INLINE its blockJson as a `section` block for a one-off band; REFERENCE it as a `component` when the band recurs or needs variants. Two consecutive call-to-action buttons are two sibling `button` blocks inside the same section \u2014 never a `columns` block, which is a `repeat(N,1fr)` grid and would stretch each CTA to half the container. Buttons are inline-level and flow side by side on their own.";
|
|
318
320
|
|
|
319
321
|
// ../types/src/layout-lucide-icons.ts
|
|
320
322
|
var LAYOUT_SECTION_ICONS = Object.freeze([
|
|
@@ -2279,6 +2281,7 @@ var LAYOUT_SECTION_ICONS = Object.freeze([
|
|
|
2279
2281
|
var LAYOUT_SECTION_ICON_SET = new Set(LAYOUT_SECTION_ICONS);
|
|
2280
2282
|
|
|
2281
2283
|
// src/tools.ts
|
|
2284
|
+
import { BetterCMSError } from "@bettercms-ai/sdk";
|
|
2282
2285
|
var FRAMEWORK_CHOICES = ["astro", "next", "react-ts", "other"];
|
|
2283
2286
|
var FRAMEWORK_LABELS = {
|
|
2284
2287
|
astro: "Astro \u2014 recommended default, static by default and fastest to publish",
|
|
@@ -2319,6 +2322,46 @@ async function askFramework(deps) {
|
|
|
2319
2322
|
}
|
|
2320
2323
|
return { prompt: FRAMEWORK_PROMPT };
|
|
2321
2324
|
}
|
|
2325
|
+
var AUTHORING_CHOICES = ["components", "fields"];
|
|
2326
|
+
var AUTHORING_LABELS = {
|
|
2327
|
+
components: "Components \u2014 reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema. Best for marketing and landing sites",
|
|
2328
|
+
fields: "Fields \u2014 a typed field schema per page. Best for blogs, catalogues and directories, where many rows share one shape"
|
|
2329
|
+
};
|
|
2330
|
+
var AUTHORING_PROMPT = [
|
|
2331
|
+
"Ask the user which authoring architecture this site should use, then call set_authoring_preference again with their answer as `preference`:",
|
|
2332
|
+
...AUTHORING_CHOICES.map((c, i) => ` ${i + 1}. ${c} \u2014 ${AUTHORING_LABELS[c]}`),
|
|
2333
|
+
"",
|
|
2334
|
+
"Answering 'components' does not convert anything \u2014 there is no field-to-block converter. It means you author the sections yourself: create_component, then publish_component (an unpublished component renders as NOTHING on the live site), then set_page_content placing `component` blocks. Once a page has blocks, list_extraction_candidates and extract_component fold the repeats.",
|
|
2335
|
+
"",
|
|
2336
|
+
"Do not choose on their behalf. This is asked once per project."
|
|
2337
|
+
].join("\n");
|
|
2338
|
+
async function askAuthoring(deps) {
|
|
2339
|
+
if (!deps.elicit) return { prompt: AUTHORING_PROMPT };
|
|
2340
|
+
try {
|
|
2341
|
+
const res = await deps.elicit({
|
|
2342
|
+
message: "Which authoring architecture should this site use?",
|
|
2343
|
+
requestedSchema: {
|
|
2344
|
+
type: "object",
|
|
2345
|
+
properties: {
|
|
2346
|
+
preference: {
|
|
2347
|
+
type: "string",
|
|
2348
|
+
title: "Authoring architecture",
|
|
2349
|
+
description: "How this site's pages are composed. Asked once per project.",
|
|
2350
|
+
enum: [...AUTHORING_CHOICES],
|
|
2351
|
+
enumNames: AUTHORING_CHOICES.map((c) => AUTHORING_LABELS[c])
|
|
2352
|
+
}
|
|
2353
|
+
},
|
|
2354
|
+
required: ["preference"]
|
|
2355
|
+
}
|
|
2356
|
+
});
|
|
2357
|
+
const picked = res.action === "accept" ? res.content?.preference : void 0;
|
|
2358
|
+
if (typeof picked === "string" && AUTHORING_CHOICES.includes(picked)) {
|
|
2359
|
+
return { preference: picked };
|
|
2360
|
+
}
|
|
2361
|
+
} catch {
|
|
2362
|
+
}
|
|
2363
|
+
return { prompt: AUTHORING_PROMPT };
|
|
2364
|
+
}
|
|
2322
2365
|
var fieldType = z.enum([
|
|
2323
2366
|
"text",
|
|
2324
2367
|
"richtext",
|
|
@@ -2619,7 +2662,14 @@ function buildToolDefs(deps) {
|
|
|
2619
2662
|
options: z.array(z.string()).optional().describe("choices when type is 'select', 'radio' or 'checkboxes'"),
|
|
2620
2663
|
hidden: z.boolean().optional().describe("not rendered; pairs with defaultValue to capture context"),
|
|
2621
2664
|
defaultValue: z.string().optional(),
|
|
2622
|
-
showIf: z.object({ field: z.string(), equals: z.string() }).optional().describe("show this field only when another field equals a value")
|
|
2665
|
+
showIf: z.object({ field: z.string(), equals: z.string() }).optional().describe("show this field only when another field equals a value"),
|
|
2666
|
+
validation: z.object({
|
|
2667
|
+
emailPolicy: z.enum(["any", "business"]).optional().describe("'email' fields only"),
|
|
2668
|
+
min: z.number().optional().describe("'number' fields only \u2014 inclusive floor"),
|
|
2669
|
+
max: z.number().optional().describe("'number' fields only \u2014 inclusive ceiling"),
|
|
2670
|
+
phoneFormat: z.enum(["any", "e164"]).optional().describe("'phone' fields only"),
|
|
2671
|
+
pattern: z.string().optional().describe("'text' / 'textarea' / 'url' fields only \u2014 a regex")
|
|
2672
|
+
}).optional().describe("per-field rules the API enforces on submit; each key is only valid on the field types listed")
|
|
2623
2673
|
});
|
|
2624
2674
|
const formSettingsShape = {
|
|
2625
2675
|
description: z.string().optional(),
|
|
@@ -2642,11 +2692,26 @@ function buildToolDefs(deps) {
|
|
|
2642
2692
|
key: z.string().min(1),
|
|
2643
2693
|
label: z.string().min(1),
|
|
2644
2694
|
target: z.object({ blockId: z.string().min(1), path: z.string().min(1) }),
|
|
2645
|
-
|
|
2695
|
+
// MUST stay at parity with componentPropDefSchema on the server. `update_component`
|
|
2696
|
+
// REPLACES the whole `props` array, so a type this enum omits cannot be echoed back: an
|
|
2697
|
+
// agent that reads a component and writes it back DESTROYS every prop of that type.
|
|
2698
|
+
type: z.enum(["text", "richtext", "image", "url", "boolean", "number", "select", "group", "table", "slot"]),
|
|
2646
2699
|
// 'slot' holds ONE nested component instance; config.componentIds restricts what may
|
|
2647
2700
|
// fill it. Absent here until now, so a slot allowlist was unreachable from stdio even
|
|
2648
2701
|
// once the enum allowed the type.
|
|
2649
|
-
config: z.object({
|
|
2702
|
+
config: z.object({
|
|
2703
|
+
componentIds: z.array(z.string()).optional(),
|
|
2704
|
+
// 'slot' allowlist
|
|
2705
|
+
options: z.array(z.string()).optional(),
|
|
2706
|
+
// 'select' — required, choices
|
|
2707
|
+
// 'group'/'table' sub-shape. Typed loosely here (the server validates the full
|
|
2708
|
+
// recursive shape) so a 3-level tree does not need a 3-level Zod mirror in the client.
|
|
2709
|
+
fields: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
2710
|
+
min: z.number().optional(),
|
|
2711
|
+
// 'number'
|
|
2712
|
+
max: z.number().optional(),
|
|
2713
|
+
step: z.number().optional()
|
|
2714
|
+
}).passthrough().optional(),
|
|
2650
2715
|
defaultValue: z.unknown().optional()
|
|
2651
2716
|
});
|
|
2652
2717
|
const getLayoutInput = z.object({
|
|
@@ -2718,7 +2783,9 @@ function buildToolDefs(deps) {
|
|
|
2718
2783
|
componentId: z.string().min(1).describe("component id (from list_components)"),
|
|
2719
2784
|
name: z.string().optional(),
|
|
2720
2785
|
category: componentCategory.optional(),
|
|
2721
|
-
sectionType: sectionType.nullable().optional().describe(
|
|
2786
|
+
sectionType: sectionType.nullable().optional().describe(
|
|
2787
|
+
"null demotes it to an ordinary component \u2014 it disappears from the 'Add a section' picker, and instances already placed keep rendering but lose their section chrome and variant switcher"
|
|
2788
|
+
),
|
|
2722
2789
|
allowedOn: allowedOn.optional().describe("REPLACES the placement allowlist"),
|
|
2723
2790
|
description: z.string().optional(),
|
|
2724
2791
|
blockJson: z.array(blockObject).optional().describe("REPLACES the block tree"),
|
|
@@ -2977,6 +3044,25 @@ function buildToolDefs(deps) {
|
|
|
2977
3044
|
return ok("Created project.", await data(c, "POST", `/management/projects`, { ...a, framework }));
|
|
2978
3045
|
}
|
|
2979
3046
|
),
|
|
3047
|
+
def(
|
|
3048
|
+
"set_authoring_preference",
|
|
3049
|
+
"Set the site's authoring architecture",
|
|
3050
|
+
"Record which authoring architecture this site uses \u2014 'components' (reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema \u2014 best for marketing and landing sites) or 'fields' (a typed field schema per page \u2014 best for blogs, catalogues and directories). ASK THE USER; do not pick for them. Called without `preference`, this tool asks them directly (or hands you the question to ask). deploy_project, deploy_from_upload and promote_project all refuse with 409 AUTHORING_DECISION_REQUIRED until it is set, and that refusal carries this project's real page counts to show the user. Answering 'components' does NOT convert anything \u2014 there is no field-to-block converter; it means you author the sections yourself: create_component, then publish_component (an unpublished component renders as NOTHING on the live site), then set_page_content placing `component` blocks. Asked once per project; re-callable if the user changes their mind.",
|
|
3051
|
+
// Optional in the schema for exactly the reason `framework` is above: a required arg is
|
|
3052
|
+
// rejected by the SDK before the handler runs, which would kill the elicitation below
|
|
3053
|
+
// and leave the model guessing. Optional here, answered by a human there. The backend
|
|
3054
|
+
// gate refuses the deploy regardless, so nothing ships on a guess.
|
|
3055
|
+
z.object({ preference: z.enum(AUTHORING_CHOICES).optional().describe("REQUIRED in effect \u2014 the architecture the USER chose. Ask them; never default it.") }).shape,
|
|
3056
|
+
async (c, a) => {
|
|
3057
|
+
let preference = a.preference;
|
|
3058
|
+
if (preference === void 0) {
|
|
3059
|
+
const asked = await askAuthoring(deps);
|
|
3060
|
+
if ("prompt" in asked) return fail(asked.prompt);
|
|
3061
|
+
preference = asked.preference;
|
|
3062
|
+
}
|
|
3063
|
+
return ok("Recorded the authoring architecture.", await data(c, "PATCH", `/management/projects/current/authoring-preference`, { preference }));
|
|
3064
|
+
}
|
|
3065
|
+
),
|
|
2980
3066
|
def(
|
|
2981
3067
|
"clone_project",
|
|
2982
3068
|
"Clone a project",
|
|
@@ -3031,7 +3117,7 @@ function buildToolDefs(deps) {
|
|
|
3031
3117
|
def(
|
|
3032
3118
|
"update_page",
|
|
3033
3119
|
"Edit a page",
|
|
3034
|
-
"Edit a page: title, slug, SEO metaTitle/metaDescription, publish status (draft|published), and `blockJson` (its block composition \u2014 passing it REPLACES the whole array, so read get_page first). It does NOT change the field SCHEMA \u2014 use add_page_field / set_page_content for that. Renaming the slug keeps content intact. Publishing copies the draft blocks live in the same call.",
|
|
3120
|
+
"Edit a page: title, slug, SEO metaTitle/metaDescription, publish status (draft|published), and `blockJson` (its block composition \u2014 passing it REPLACES the whole array, so read get_page first). It does NOT change the field SCHEMA \u2014 use add_page_field / set_page_content for that. Renaming the slug keeps content intact. Publishing copies the draft blocks live in the same call. " + SECTION_DOCTRINE,
|
|
3035
3121
|
z.object({ pageId: z.string().min(1), title: z.string().optional(), slug: z.string().optional(), blockJson: z.array(blockObject).optional().describe("REPLACES the page's block composition"), metaTitle: z.string().optional(), metaDescription: z.string().optional(), status: z.enum(["draft", "published"]).optional() }).shape,
|
|
3036
3122
|
async (c, a) => ok("Updated page.", await data(c, "PATCH", `/management/pages/${s(a.pageId)}/meta`, { title: a.title, slug: a.slug, blockJson: a.blockJson, metaTitle: a.metaTitle, metaDescription: a.metaDescription, status: a.status }))
|
|
3037
3123
|
),
|
|
@@ -3210,7 +3296,7 @@ function buildToolDefs(deps) {
|
|
|
3210
3296
|
name: "create_page",
|
|
3211
3297
|
config: {
|
|
3212
3298
|
title: "Create a page",
|
|
3213
|
-
description: "Create a page with its own typed schema. Supports pageType 'singleton' (exactly one entry \u2014 Home, About, Contact) and 'dynamic' (many entries sharing the schema \u2014 Blog posts, Products). Project-scoped from the key. Additive \u2014 does not delete or overwrite existing pages. FIRST read the page's real markup and DECOMPOSE it into a destructured tree: each visual section becomes a nested field \u2014 a fixed grouped block \u2192 type 'group', a repeating list of items (cards, testimonials, features, FAQs) \u2192 type 'repeater' \u2014 each carrying its own child `fields`. Do NOT flatten sections into many flat top-level fields. Build the full nested tree, then call this once.",
|
|
3299
|
+
description: "Create a page with its own typed schema. Supports pageType 'singleton' (exactly one entry \u2014 Home, About, Contact) and 'dynamic' (many entries sharing the schema \u2014 Blog posts, Products). Project-scoped from the key. Additive \u2014 does not delete or overwrite existing pages. FIRST read the page's real markup and DECOMPOSE it into a destructured tree: each visual section becomes a nested field \u2014 a fixed grouped block \u2192 type 'group', a repeating list of items (cards, testimonials, features, FAQs) \u2192 type 'repeater' \u2014 each carrying its own child `fields`. Do NOT flatten sections into many flat top-level fields. Build the full nested tree, then call this once. That tree is the page's SCHEMA \u2014 what it holds. " + SECTION_DOCTRINE,
|
|
3214
3300
|
inputSchema: createPageInput.shape
|
|
3215
3301
|
},
|
|
3216
3302
|
handler: guard(
|
|
@@ -3567,7 +3653,7 @@ function buildToolDefs(deps) {
|
|
|
3567
3653
|
name: "create_component",
|
|
3568
3654
|
config: {
|
|
3569
3655
|
title: "Create a reusable component",
|
|
3570
|
-
description: "Create a reusable component from a blockJson tree. CONFIRM the structure with the user first. blockJson is an array of blocks (heading
|
|
3656
|
+
description: "Create a reusable component from a blockJson tree. THIS IS ALSO HOW A PAGE GETS ITS SECTIONS: set `sectionType` (e.g. 'Hero') and the component becomes a placeable section, selectable in the editor's 'Add a section' picker. Components sharing a `sectionType` are its layout VARIANTS \u2014 one Hero with a 'Centered' and a 'Two-column' variant, same prop keys, so a swap keeps the content. CONFIRM the structure with the user first. blockJson is an array of blocks \u2014 the same set create_page accepts (heading, text/richtext, image, button, spacer, video, columns, section, slider, tabs, navbar, footer, form, component, collection), NOT a narrower one; `section` nests child blocks in props.children and `columns` in props.columns. `props` declares overridable fields. Returns the new id \u2014 render with `<BcmsBlocks>`. Always lands as a DRAFT: it is not on the live site until someone publishes it from the dashboard. " + SECTION_DOCTRINE,
|
|
3571
3657
|
inputSchema: createComponentInput.shape
|
|
3572
3658
|
},
|
|
3573
3659
|
handler: guard(
|
|
@@ -3750,8 +3836,13 @@ Then declare what an editor may change, via \`props\`:
|
|
|
3750
3836
|
{ key: "ctaHref", label: "CTA link", target: { blockId: "cta", path: "props.href" }, type: "url" }
|
|
3751
3837
|
]
|
|
3752
3838
|
|
|
3753
|
-
|
|
3754
|
-
|
|
3839
|
+
\u{1F534} **On a components-first page, \`props\` is the ONLY editing surface.** Click-to-edit binds
|
|
3840
|
+
\`heading\`/\`text\`/\`button\`/\`image\` blocks; it does **not** bind a \`component\` block, because
|
|
3841
|
+
a component's blocks belong to the shared definition, not to the instance. So a page built
|
|
3842
|
+
from \`component\` blocks renders correctly and emits ZERO fields on the canvas \u2014 editing goes
|
|
3843
|
+
through the declared props allowlist to per-instance overrides. **A component with no props
|
|
3844
|
+
is a section nobody can change.** Declare a prop for every string, link and image a marketer
|
|
3845
|
+
would ever reasonably want to touch; leave out only structure and styling.
|
|
3755
3846
|
|
|
3756
3847
|
## 4. Variants
|
|
3757
3848
|
|
|
@@ -3814,9 +3905,98 @@ nothing, on a page that returns 200. If a section is missing from the live site,
|
|
|
3814
3905
|
|
|
3815
3906
|
**A workspace-level component (\`projectId: null\`) never reaches a live site.** It resolves
|
|
3816
3907
|
in preview and is blank in production. Always pass the project's id.
|
|
3908
|
+
|
|
3909
|
+
## 9. Check which project you are bound to, BEFORE you build
|
|
3910
|
+
|
|
3911
|
+
\`create_project\` succeeds on a project-scoped connection and hands back a real new project
|
|
3912
|
+
\u2014 but every write that follows still lands in the project your grant is bound to.
|
|
3913
|
+
\`projectId\` is forwarded as a header for a workspace-scoped grant; a **project-scoped grant
|
|
3914
|
+
accepts it and ignores it**, silently. No error, no warning, wrong project.
|
|
3915
|
+
|
|
3916
|
+
So call \`get_project\` (no arguments) first \u2014 it reports the project you are actually
|
|
3917
|
+
writing to. If that is not where the work belongs, stop and tell the user: only they can
|
|
3918
|
+
approve a grant for the other project, no tool can switch it.
|
|
3919
|
+
|
|
3920
|
+
If you must probe, probe with a \`create_content_model\` \u2014 models are deletable
|
|
3921
|
+
(\`delete_content_model\`, soft-delete) and **there is no \`delete_component\`**. A component
|
|
3922
|
+
written to the wrong project can only be demoted (\`sectionType: null\`) and left unpublished.
|
|
3923
|
+
|
|
3924
|
+
## 10. You imported a site, or you are about to deploy one
|
|
3925
|
+
|
|
3926
|
+
\`deploy_project\`, \`deploy_from_upload\` and \`promote_project\` all answer **409
|
|
3927
|
+
AUTHORING_DECISION_REQUIRED** until a human has chosen this project's architecture. This is
|
|
3928
|
+
asked ONCE per project, ever. It is not an error to retry or route around: read the message
|
|
3929
|
+
out, let the user pick, call \`set_authoring_preference\`, then deploy again.
|
|
3930
|
+
|
|
3931
|
+
It exists because an imported site arrives **field-driven whether anyone chose that or not**
|
|
3932
|
+
\u2014 a crawl-based import (Webflow, a starter, a template) emits pages with a typed field schema
|
|
3933
|
+
and an empty block tree, because that is all a crawl can infer. Nobody decided it. On a
|
|
3934
|
+
marketing site it is the wrong answer, and \xA71 already says why converting later means
|
|
3935
|
+
rewriting content. So the platform stops once, at the last moment it is still cheap.
|
|
3936
|
+
|
|
3937
|
+
**Answering \`components\` does not convert anything.** There is no field-to-block converter,
|
|
3938
|
+
and \`extract_component\` cannot stand in for one: it scans \`blockJson\`, which is empty on
|
|
3939
|
+
exactly the pages that would need converting. What it means is that you author the sections,
|
|
3940
|
+
in this order:
|
|
3941
|
+
|
|
3942
|
+
1. create_component per section (they land as DRAFTS)
|
|
3943
|
+
2. publish_component each one \u2014 unpublished renders as NOTHING, on a page that 200s
|
|
3944
|
+
3. set_page_content place them as \`component\` blocks on the page
|
|
3945
|
+
4. list_extraction_candidates / extract_component
|
|
3946
|
+
now that blocks exist, fold any section repeated 3+ times
|
|
3947
|
+
|
|
3948
|
+
**Answering \`fields\` is a real answer, not a deferral.** A blog, a catalogue or a directory
|
|
3949
|
+
is schema-first by design (\xA71) and should stay that way. Say so and move on.
|
|
3950
|
+
|
|
3951
|
+
Either way: ask, do not choose. The 409 carries this project's actual page counts \u2014 how many
|
|
3952
|
+
are field-driven, block-driven, and how many place a reusable component \u2014 so quote those to
|
|
3953
|
+
the user rather than describing the choice in the abstract.
|
|
3954
|
+
|
|
3955
|
+
## 11. The canvas: what makes an imported site EDITABLE
|
|
3956
|
+
|
|
3957
|
+
A deploy makes a site LIVE. It does not make it editable \u2014 those are different states, and the
|
|
3958
|
+
gap between them is the single most common disappointment after an import.
|
|
3959
|
+
|
|
3960
|
+
**The canvas is the real site when it can be.** When a page's draft matches its published copy
|
|
3961
|
+
structurally, the visual editor frames the project's OWN deployed build and paints unpublished
|
|
3962
|
+
text over it. Structural drafts (new sections, unpublished pages, changed components) render on
|
|
3963
|
+
the platform's own renderer instead \u2014 and that renderer previews in a GENERIC theme unless the
|
|
3964
|
+
deploy artifact declares \`bcms-presentation.json\` at its root. Put the file in \`public/\`
|
|
3965
|
+
(the build lands it at the artifact root) declaring the site's presentation \u2014 container width,
|
|
3966
|
+
type scale, nav position and background, footer surface \u2014 as DTCG \`{"$type": ..., "$value": ...}\`
|
|
3967
|
+
entries. Redeclare it on every deploy; absent means "declared nothing" and previews fall back
|
|
3968
|
+
to platform defaults that will not look like this site.
|
|
3969
|
+
|
|
3970
|
+
**Editing binds by VALUE.** The editor matches CMS field values against the text the site
|
|
3971
|
+
renders. Three consequences, each load-bearing:
|
|
3972
|
+
|
|
3973
|
+
1. Content that exists ONLY in the build can never be click-to-edit. Bring it in, per
|
|
3974
|
+
route, in this order (get_next_steps reports the state until it is done):
|
|
3975
|
+
|
|
3976
|
+
a. create_page one per route, slug matching the route
|
|
3977
|
+
b. add_page_field the fields its content needs (or create_component +
|
|
3978
|
+
publish_component + component blocks, per your \xA710 answer)
|
|
3979
|
+
c. set_page_content values EXACTLY equal to the text the site renders \u2014
|
|
3980
|
+
binding matches by value, so a paraphrase binds nothing
|
|
3981
|
+
d. update_page status 'published' \u2014 the canvas binds the PUBLISHED copy
|
|
3982
|
+
2. A value that renders in more than one place stays uneditable on the canvas (deliberate:
|
|
3983
|
+
binding it would edit all of them at once). It remains editable in the side panel.
|
|
3984
|
+
3. Keep chrome semantic \u2014 \`<nav>\`, \`<footer>\`, page content inside \`<main>\`, mastheads
|
|
3985
|
+
as a top-level \`<header>\`. Chrome is edited through the project Layout, not the page,
|
|
3986
|
+
and semantic landmarks are how the editor keeps a nav edit from being written into page
|
|
3987
|
+
content. Div-built chrome outside \`<main>\` is still excluded; div-built chrome with no
|
|
3988
|
+
\`<main>\` anywhere loses that protection.
|
|
3989
|
+
|
|
3990
|
+
**Hosting decides whether a canvas exists at all.** A site deployed here is framed through a
|
|
3991
|
+
same-origin proxy \u2014 that is what the canvas requires. A site hosted elsewhere (your own Vercel,
|
|
3992
|
+
your own server) has NO canvas today: the SDK's draft mode with \`stega: true\` embeds invisible
|
|
3993
|
+
per-field provenance in fetched strings, which prepares the content for editing surfaces, but do
|
|
3994
|
+
not promise a canvas for an externally-hosted site.
|
|
3817
3995
|
`;
|
|
3818
3996
|
|
|
3819
3997
|
// src/prompts.ts
|
|
3998
|
+
var STRUCTURE_RULE = `### Page structure (non-negotiable)
|
|
3999
|
+
${SECTION_DOCTRINE}`;
|
|
3820
4000
|
var SCHEMA_PROPOSAL_FLOW = `### Whole-project design (confirm-first) \u2192 \`create_component\` / \`create_page\` / \`create_content_model\`
|
|
3821
4001
|
Design the WHOLE project from its brief or its code, and **confirm the shape with the user
|
|
3822
4002
|
BEFORE creating anything**. Never silently guess.
|
|
@@ -4067,6 +4247,8 @@ the host has a stale cached MCP \u2014 tell the user to \`rm -rf ~/.npm/_npx\` a
|
|
|
4067
4247
|
|
|
4068
4248
|
${PAGE_FLOW}
|
|
4069
4249
|
|
|
4250
|
+
${STRUCTURE_RULE}
|
|
4251
|
+
|
|
4070
4252
|
After creating, report the page id, slug, type, field count, and the project it landed in.
|
|
4071
4253
|
On 409 slug_taken, offer an alternative slug; on 401/403, the MCP key needs (re)authorizing.`
|
|
4072
4254
|
}
|
|
@@ -4143,8 +4325,12 @@ On 409 slug_taken, offer an alternative slug; on 401/403, the MCP key needs (re)
|
|
|
4143
4325
|
type: "text",
|
|
4144
4326
|
text: `Build a BetterCMS site end-to-end.${request ? ` The user wants: "${request}".` : ""}
|
|
4145
4327
|
|
|
4328
|
+
${SCHEMA_PROPOSAL_FLOW}
|
|
4329
|
+
|
|
4146
4330
|
${BUILD_SITE_FLOW}
|
|
4147
4331
|
|
|
4332
|
+
${STRUCTURE_RULE}
|
|
4333
|
+
|
|
4148
4334
|
${SEO_FLOW}
|
|
4149
4335
|
|
|
4150
4336
|
Confirm each stage with the user before writing. On 401/403, the MCP key needs (re)authorizing.`
|
|
@@ -4172,6 +4358,8 @@ Confirm each stage with the user before writing. On 401/403, the MCP key needs (
|
|
|
4172
4358
|
|
|
4173
4359
|
${LANDING_PAGES_FLOW}
|
|
4174
4360
|
|
|
4361
|
+
${STRUCTURE_RULE}
|
|
4362
|
+
|
|
4175
4363
|
On 401/403, the MCP key needs (re)authorizing.`
|
|
4176
4364
|
}
|
|
4177
4365
|
}
|
|
@@ -4197,6 +4385,44 @@ On 401/403, the MCP key needs (re)authorizing.`
|
|
|
4197
4385
|
|
|
4198
4386
|
${SEO_FLOW}
|
|
4199
4387
|
|
|
4388
|
+
On 401/403, the MCP key needs (re)authorizing.`
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
]
|
|
4392
|
+
})
|
|
4393
|
+
);
|
|
4394
|
+
server.registerPrompt(
|
|
4395
|
+
"import-site",
|
|
4396
|
+
{
|
|
4397
|
+
title: "Make an imported site editable (guided)",
|
|
4398
|
+
description: "After deploying an existing site: bring its content into the CMS so the visual editor can bind it, declare its presentation manifest, and publish everything that renders. A deploy makes a site LIVE, not EDITABLE \u2014 this flow closes that gap.",
|
|
4399
|
+
argsSchema: {
|
|
4400
|
+
request: z2.string().optional().describe("scope, e.g. 'all routes' or 'just the home page'")
|
|
4401
|
+
}
|
|
4402
|
+
},
|
|
4403
|
+
({ request }) => ({
|
|
4404
|
+
messages: [
|
|
4405
|
+
{
|
|
4406
|
+
role: "user",
|
|
4407
|
+
content: {
|
|
4408
|
+
type: "text",
|
|
4409
|
+
text: `Make my imported site editable in BetterCMS.${request ? ` Scope: "${request}".` : ""}
|
|
4410
|
+
|
|
4411
|
+
Read bettercms://playbook/schema section 11 first, then work this order:
|
|
4412
|
+
|
|
4413
|
+
1. AUTHORING GATE \u2014 if any deploy answered 409 AUTHORING_DECISION_REQUIRED, ask ME
|
|
4414
|
+
components-or-fields (do not choose), then set_authoring_preference.
|
|
4415
|
+
2. CONTENT INTO THE CMS, per route: create_page (slug = route) -> add_page_field (or
|
|
4416
|
+
create_component + publish_component + component blocks) -> set_page_content with values
|
|
4417
|
+
EXACTLY equal to the rendered text (binding matches by value; a paraphrase binds nothing)
|
|
4418
|
+
-> update_page status 'published' (the canvas binds the PUBLISHED copy).
|
|
4419
|
+
3. PRESENTATION MANIFEST \u2014 add bcms-presentation.json to public/ (container width, type
|
|
4420
|
+
scale, nav position/background, footer surface as DTCG {"$type","$value"} entries) and
|
|
4421
|
+
redeploy, or structural draft previews render in a generic theme, not this site's design.
|
|
4422
|
+
4. VERIFY \u2014 call get_next_steps and fix what it lists (it knows about missing manifests,
|
|
4423
|
+
content still only in the build, and placed-but-unpublished components), or tell me why
|
|
4424
|
+
an item is being left.
|
|
4425
|
+
|
|
4200
4426
|
On 401/403, the MCP key needs (re)authorizing.`
|
|
4201
4427
|
}
|
|
4202
4428
|
}
|