@bettercms-ai/mcp 0.70.0 → 0.71.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/dist/index.js +161 -52
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +10 -4
- package/dist/skills/bettercms/references/animation.md +235 -0
- package/dist/skills/bettercms/references/astro.md +2 -2
- package/dist/skills/bettercms/references/bindings.md +33 -3
- package/dist/skills/bettercms/references/components.md +20 -8
- package/dist/skills/bettercms/references/content-modeling.md +8 -3
- package/dist/skills/bettercms/references/detection.md +2 -2
- package/dist/skills/bettercms/references/finish-site.md +80 -2
- package/dist/skills/bettercms/references/media-and-brand.md +22 -1
- package/dist/skills/bettercms/references/next.md +4 -3
- package/dist/skills/bettercms/references/publishing.md +31 -0
- package/dist/skills/bettercms/references/worked-examples.md +4 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -36,7 +36,27 @@ var COMPONENT_PROP_TYPES = [
|
|
|
36
36
|
"color",
|
|
37
37
|
"file",
|
|
38
38
|
"icon",
|
|
39
|
-
"multiselect"
|
|
39
|
+
"multiselect",
|
|
40
|
+
// V2 schema language (F1.x). `object` is ONE nested value of named `config.fields`, stored as
|
|
41
|
+
// `{ [childKey]: value }` like the model field `object`; the six after it store what the
|
|
42
|
+
// same-named content-model FIELD stores (`location` is the V2 geopoint) and are judged by
|
|
43
|
+
// that field's validator.
|
|
44
|
+
"object",
|
|
45
|
+
"datetime",
|
|
46
|
+
"slug",
|
|
47
|
+
"email",
|
|
48
|
+
"phone",
|
|
49
|
+
"json",
|
|
50
|
+
"location",
|
|
51
|
+
// V2 leaves, each storing what the same-named content-model FIELD stores: a taxonomy string[],
|
|
52
|
+
// a rating number, a code value (`{ language?, code }` or a string).
|
|
53
|
+
"tags",
|
|
54
|
+
"rating",
|
|
55
|
+
"code",
|
|
56
|
+
// CMS items, as the same-named content-model FIELDS store them: an entry id (or a polymorphic
|
|
57
|
+
// `{ entryId, modelSlug }`), and a list of them. Delivery hydrates them like an entry's.
|
|
58
|
+
"reference",
|
|
59
|
+
"multi-reference"
|
|
40
60
|
];
|
|
41
61
|
|
|
42
62
|
// ../types/src/layout-lucide-icons.ts
|
|
@@ -2277,8 +2297,8 @@ var STRUCTURE_PLAYBOOK_URI = "bettercms://playbook/structure";
|
|
|
2277
2297
|
var STRUCTURE_DEFAULT_INSTRUCTION = `After you create collections or pages, organise them per the structure playbook: read ${STRUCTURE_PLAYBOOK_URI}, call suggest_content_structure (read-only), show the user its outline, then apply it with set_content_structure and the version it returned (the If-Match). A project that already has a structure keeps it: file only what you created, with move_to_folder, unless the user asks for a full re-organisation.`;
|
|
2278
2298
|
var SKILLS_ROUTING_INSTRUCTION = "Before writing code in a repo that uses BetterCMS, explore the repo, then read the installed `bettercms` skill (`npx @bettercms-ai/install --project -y` adds it if missing): its table turns what you found into the one reference the task needs \u2014 read only that. Without the skill installed, read the same files as resources: bettercms://skill/bettercms, then bettercms://skill/<reference>.";
|
|
2279
2299
|
var DOCK_UI_ROUTING_INSTRUCTION = "How fields and props look in the editor (groups, tabs, rows, icons, item names) is playbook \xA714 or the `field-ui` prompt: write it with update_content_model field* or update_component prop* plus ifMatch, read it back, and finish when get_next_steps reports no dock-* step.";
|
|
2280
|
-
var RECEIPT_FIRST_INSTRUCTION = "Start every project with get_next_steps: its `receipt` lists what is already done (`done`), what is still missing (`missing`, each item citing the count it reacted to) and the one `nextAction`. Work from that receipt instead of re-reading the site, call get_next_steps again after each batch of edits, and before you tell the user you are finished.
|
|
2281
|
-
var NEXT_STEPS_DESCRIPTION = `Call this FIRST on a project. Returns \`receipt\` \u2014 \`done\` (live release, bound elements, conversion coverage, composed pages, published components and Layout), \`missing\`, \`nextAction\` and \`notes\` (unjudged: unknown, not done) \u2014 plus \`data\`, the unfinished items: missing meta descriptions, drafts, empty collections, unnotified forms, writes awaiting approval, unstamped bindings or sections, \`authoring-undecided\` and the Layout lane (\`layout-write-blocked\`, \`layout-field-unread\`, \`layout-locator-stale\`, \`layout-rebuild-required\`, \`chrome-duplicated\`). Each item cites its count and may carry \`payload\` {rows, total}, at most 20 rows. Call it again after
|
|
2300
|
+
var RECEIPT_FIRST_INSTRUCTION = "Start every project with get_next_steps: its `receipt` lists what is already done (`done`), what is still missing (`missing`, each item citing the count it reacted to) and the one `nextAction`. Work from that receipt instead of re-reading the site, call get_next_steps again after each batch of edits, and before you tell the user you are finished. On an imported site you are finished only when `receipt.complete` is true: work `receipt.gate.blocking` in order, and record a gap you cannot close with editor_setup `exempt` (an allowed reason and a note), never in prose.";
|
|
2301
|
+
var NEXT_STEPS_DESCRIPTION = `Call this FIRST on a project. Returns \`receipt\` \u2014 \`done\` (live release, bound elements, conversion coverage, composed pages, published components and Layout), \`missing\`, \`nextAction\` and \`notes\` (unjudged: unknown, not done) \u2014 plus \`data\`, the unfinished items: missing meta descriptions, drafts, empty collections, unnotified forms, writes awaiting approval, unstamped bindings or sections, \`authoring-undecided\` and the Layout lane (\`layout-write-blocked\`, \`layout-field-unread\`, \`layout-locator-stale\`, \`layout-rebuild-required\`, \`chrome-duplicated\`). Each item cites its count and may carry \`payload\` {rows, total}, at most 20 rows. \`receipt.complete\` is the setup gate's verdict and \`receipt.gate\` its numbers: per built route the page, text, image and link coverage, \`blocking\` first, each with its \`fix\`. Call it again after each batch; you are finished only when \`complete\` is true. \`organise-content\`: pages and collections are in no folder \u2014 read ${STRUCTURE_PLAYBOOK_URI}, call suggest_content_structure and, once the user agrees, set_content_structure.`;
|
|
2282
2302
|
var LAYOUT_CHROME_INSTRUCTION = 'When the user asks to make the navigation, footer or other site chrome editable, or get_next_steps lists a `layout-*` or `chrome-duplicated` item: this is playbook \xA711 item 3. Chrome is the project Layout, never page fields: in the source, wrap each Section in `data-bcms-layout-section` (the slot, its base slug) and mark each value `data-bcms-layout-field="layout:<sections[slug].id>:<fieldId>"`, never `data-bcms-field` inside it, then push; the strip removes the page copies, never delete them by hand. On a site BetterCMS serves, publish_layout writes Layout VALUES into the live pages; a structure change waits for a Rebuild. Never place nav, footer or modal blocks or fields on a page: the Layout (or the site\'s own layout) draws chrome, so a page copy either duplicates it or renders nowhere (get_componentize_plan reports one as LAYOUT_CHROME).';
|
|
2283
2303
|
var CTA_BUTTON_INSTRUCTION = "A call to action is ONE `button` field (value {label, href, variant?, size?, arrow?, icon?, newTab?}), rendered with BcmsButton from @bettercms-ai/astro or @bettercms-ai/next: never a text field beside a link field. A page that already holds such a pair (primaryCtaText + primaryCtaHref) becomes one button with update_field `mergeHref`. Inside a component a CTA is a `button` block whose text and href two props target.";
|
|
2284
2304
|
var STRUCTURE_EXAMPLE_PAYLOAD = {
|
|
@@ -2519,7 +2539,7 @@ function allKeys(fields) {
|
|
|
2519
2539
|
})
|
|
2520
2540
|
]);
|
|
2521
2541
|
}
|
|
2522
|
-
var CLEARABLE = ["helpText", "showIf", "ui", "placement", "readOnly", "lockReason"];
|
|
2542
|
+
var CLEARABLE = ["helpText", "showIf", "ui", "placement", "readOnly", "lockReason", "helpMore", "advanced", "recommendedMax", "openMode"];
|
|
2523
2543
|
var SETTABLE = ["label", "required", ...CLEARABLE];
|
|
2524
2544
|
function mergeConfig(current, patch) {
|
|
2525
2545
|
const out = { ...current ?? {} };
|
|
@@ -2557,7 +2577,7 @@ function planFieldEdit(model, edit, callerIfMatch) {
|
|
|
2557
2577
|
}
|
|
2558
2578
|
const changes = SETTABLE.filter((k) => edit[k] !== void 0);
|
|
2559
2579
|
if (!retype && rename === void 0 && edit.config === void 0 && changes.length === 0) {
|
|
2560
|
-
return { ok: false, error: "Nothing to change: pass label, helpText, required, config, showIf, ui, placement, readOnly, lockReason, type or key." };
|
|
2580
|
+
return { ok: false, error: "Nothing to change: pass label, helpText, required, config, showIf, ui, placement, readOnly, lockReason, helpMore, advanced, recommendedMax, openMode, type or key." };
|
|
2561
2581
|
}
|
|
2562
2582
|
const echo = helpTextEcho(edit.helpText, { label: edit.label ?? current.label, defaultValue: current.defaultValue });
|
|
2563
2583
|
if (echo) return { ok: false, error: echo };
|
|
@@ -2673,7 +2693,8 @@ function listsTool(name, sets) {
|
|
|
2673
2693
|
}
|
|
2674
2694
|
|
|
2675
2695
|
// src/tool-text.ts
|
|
2676
|
-
var ADD_FIELD_DESCRIPTION = "Append ONE field to a content model (`modelId`) or to a page's schema (`pageId`) \u2014 additive, never overwrites; a key already in use is refused. For a section/zone add ONE 'group' (fixed block) or 'repeater' (repeating list) carrying its child `fields`, not the zone's inner fields as separate top-level fields. Pass `after` to insert it after an existing top-level field instead of at the end; `ui`, `helpText`, `showIf`, `placement` and `fieldsetId` declare how it presents in the editor \u2014 read them back with get_content_model or get_page. On a page a 'group' is stored as type 'array' with `config.zones.nonRepeatable`: entry and delivery data carry `<key>.nonRepeatable.<sub>`, while data-bcms-field paths are `<key>.<sub>`. Schema only \u2014 never places a section; use componentize_sections to turn a group into a section. `multiselect` stores a string[] of `config.options
|
|
2696
|
+
var ADD_FIELD_DESCRIPTION = "Append ONE field to a content model (`modelId`) or to a page's schema (`pageId`) \u2014 additive, never overwrites; a key already in use is refused. For a section/zone add ONE 'group' (fixed block) or 'repeater' (repeating list) carrying its child `fields`, not the zone's inner fields as separate top-level fields. Pass `after` to insert it after an existing top-level field instead of at the end; `ui`, `helpText`, `showIf`, `placement` and `fieldsetId` declare how it presents in the editor \u2014 read them back with get_content_model or get_page. On a page a 'group' is stored as type 'array' with `config.zones.nonRepeatable`: entry and delivery data carry `<key>.nonRepeatable.<sub>`, while data-bcms-field paths are `<key>.<sub>`. Schema only \u2014 never places a section; use componentize_sections to turn a group into a section. `multiselect` stores a string[] of `config.options`; `icon` is a BetterCMS icon name or an uploaded icon. To change a field that exists, use update_field. V2 keys (object, openMode, advanced, helpMore, recommendedMax, config.scale): \xA714 of bettercms://playbook/schema.";
|
|
2697
|
+
var V2_LEAF_TYPES_NOTE = "`tags`: string[] taxonomy (config.options suggestions, max, allowCustom). `rating`: 0..config.max (5), step 1|0.5. `code`: {language?, code} or a string (config.languages).";
|
|
2677
2698
|
var MOVE_STAGE_DESCRIPTION = "Move a content entry (`entryId`) or a page (`pageId`) to another editorial stage \u2014 this is how you take it OUT of review so it can be published. `workflowStage` is a stage key from get_workflow_board; pass `fromStage` to 409 if someone moved it first. Moving into the publish-gate stage is an approval and needs a publish-enabled connection. Publish afterwards with update_content_entry or update_page status:'published' \u2014 approving and publishing are deliberately two separate calls.";
|
|
2678
2699
|
var WINDOW_NOTE = "{from?, until?}, ISO 8601 with offset: shown only between the two instants; null clears (skill publishing).";
|
|
2679
2700
|
var WEBHOOK_EVENTS = [
|
|
@@ -2716,12 +2737,17 @@ var PARAM_TEXT = {
|
|
|
2716
2737
|
lockReason: "which system owns the value (\u2264 200 chars)",
|
|
2717
2738
|
lockClears: "as on add_field; null clears it",
|
|
2718
2739
|
helpText: "shown under the field: its purpose, where it shows, any constraint; never its label or value. Omitted: written for you.",
|
|
2740
|
+
helpMore: "longer help behind the \u24D8 next to the label, when one helpText line cannot carry it",
|
|
2741
|
+
advanced: "true = a rare or developer setting: it collects in a closed 'Advanced' section",
|
|
2742
|
+
recommendedMax: "a SOFT limit (characters, items or value): the editor warns past it, never refuses",
|
|
2743
|
+
openMode: "object/array/group/repeater: how the value (or each row) opens: 'inline', 'drill', 'popover' or 'dialog'",
|
|
2719
2744
|
fromBuiltin: "a builtin:* id from list_components: copies it into the project; fields you send win",
|
|
2720
2745
|
describes: "for editors: what it is and holds; omitted: written for you",
|
|
2721
2746
|
runtimeBundle: "instead of pageId: the server-app bundle cleanup",
|
|
2722
2747
|
pageRef: "page id or path ('/de/about'); a bare slug only when one page has it",
|
|
2723
2748
|
editorSetupForce: "re-run a setup already ready for the live build",
|
|
2724
2749
|
editorSetupCheck: "true = only report the state; starts nothing",
|
|
2750
|
+
editorSetupExempt: "instead of running: record setup-gate exceptions [{ route, check, reason, note }]; reason null removes one. Reasons: get_next_steps `receipt.gate`",
|
|
2725
2751
|
pageDynamic: "a dynamic (collection template) page only",
|
|
2726
2752
|
previewPath: "its route pattern, e.g. /blog/{slug}; null clears",
|
|
2727
2753
|
pageModelId: "an existing collection this page lists (when it has no model yet)",
|
|
@@ -2731,7 +2757,7 @@ var PARAM_TEXT = {
|
|
|
2731
2757
|
sort: "comma list, a leading - sorts descending, e.g. '-publishedAt,title'",
|
|
2732
2758
|
page: "1-based page of `limit` rows; the response's meta says totalPages",
|
|
2733
2759
|
locale: "language, a BCP-47 tag (zh-CN \u2192 zh-cn), one of the project's locales; null = the default",
|
|
2734
|
-
translationOf: "the SOURCE
|
|
2760
|
+
translationOf: "the SOURCE id this translates (never a translation); null unlinks. One per locale: the translation sets locale and translationOf",
|
|
2735
2761
|
pageScheduledAt: "ISO datetime to publish this page's draft at (needs publish permission); null cancels. An edit that omits scheduledAt CANCELS a pending schedule",
|
|
2736
2762
|
pageUnpublishAt: "ISO datetime at which the published page returns to draft; null cancels; omitting it never cancels it",
|
|
2737
2763
|
sourceUrl: "instead of data: a public http(s) URL the server fetches and stores (private or internal addresses are refused)",
|
|
@@ -2741,8 +2767,9 @@ var PARAM_TEXT = {
|
|
|
2741
2767
|
confirmedInput: "true only after the user explicitly approved adding this custom input (`ui.input`); else setting or changing one is refused input_needs_confirmation. Removing one needs no approval"
|
|
2742
2768
|
};
|
|
2743
2769
|
var INPUT_CONFIRMED_HEADER = "x-bcms-confirmed-input";
|
|
2744
|
-
var BUTTON_FIELD_NOTE = "'button' is ONE call to action, value {label, href, variant
|
|
2745
|
-
var
|
|
2770
|
+
var BUTTON_FIELD_NOTE = "'button' is ONE call to action, value {label, href, variant? (a Button family look), size?, arrow?, icon?, newTab?, rel?} (arrow:false, icon:null off): never a text field beside a link field.";
|
|
2771
|
+
var LINK_VALUE_NOTE = "A link or button href: a URL string or {type: url|page|entry|email|phone|anchor|file, href, title?, newTab?, rel?, section?, ref: page/entry id}.";
|
|
2772
|
+
var UPDATE_FIELD_DESCRIPTION = "Change ONE existing field of a content model (`modelId`) or of a page's schema (`pageId`), addressed by `path`: its key, or dot-separated keys through groups and repeaters ('hero.title'). Set its label, helpText, required, validation `config` (merged by key; null deletes one), showIf, ui, placement, readOnly, lockReason, or the V2 helpMore, advanced, recommendedMax and openMode (null clears those). The rest of the schema is written back unchanged at the version read; a 409 means it changed since, so read again. `type` changes how every stored value is read and needs allowRetype: true. `key` renames it: stored values stay under the old key, so the route answers 409 with the entry count unless allowFieldRemoval: true discards them \u2014 migrate the values first. Ask the user before a retype or a rename. A page's label + link pair (primaryCtaText + primaryCtaHref) becomes one button with `mergeHref`.";
|
|
2746
2773
|
var GET_SEO_DESCRIPTION = "Get the connected project's site-wide SEO settings \u2014 every key update_seo writes: seoDefaults (metaTitle, metaDescription, ogImage, twitterHandle, og, twitter, collectionSchemaTypes), siteSchema (the site's JSON-LD), robotsConfig, sitemapConfig, rssConfig, indexnowConfig and searchConfig.";
|
|
2747
2774
|
var UPDATE_SEO_DESCRIPTION = "Set the connected project's site-wide SEO defaults and config. Each key you send is REPLACED whole (not deep-merged), so read get_seo and send the complete object. `seoDefaults` applies to every page that does not override it, and its `collectionSchemaTypes` maps a collection's model id to the schema.org type of its entries. `siteSchema` is the site's own JSON-LD (Organization/WebSite, emitted on the home page). `robotsConfig`, `sitemapConfig` and `rssConfig` shape robots.txt, sitemap.xml and the feed; `indexnowConfig` pings search engines on publish; `searchConfig` is site search. A key the plan does not include answers 403 FEATURE_LOCKED. Rebuilds the site.";
|
|
2748
2775
|
var SEO_KEY_TEXT = {
|
|
@@ -2758,6 +2785,7 @@ var VARIANT_GROUP_NOTE = "When the component belongs to a family of looks, `vari
|
|
|
2758
2785
|
var UNPROPPED_COPY_NOTE = "A `warnings` entry UNPROPPED_COPY names copy no prop targets, which nobody can edit: add a prop with a `target` for each leaf it lists.";
|
|
2759
2786
|
var UPDATE_PAGE_IF_MATCH_NOTE = "get_page's `optimisticVersion`: the write applies only at that version, and a 409 means the page changed since, so read it again. Send it whenever you pass blockJson, which REPLACES the whole composition.";
|
|
2760
2787
|
var SLOT_MULTIPLE_NOTE = "'slot' with config.multiple holds a list [{componentId, overrides?}] (config.maxItems, default 12, at most 50): target.path 'props.children' on a section.";
|
|
2788
|
+
var PROP_V2_NOTE = "Props take the fields' V2 keys (\xA714): 'object' = ONE {child: value} from config.fields, \u226410 levels (field_depth_exceeded); datetime|slug|email|phone|json|location|tags|rating|code|reference|multi-reference as that field (a reference names contentModelId, or polymorphic + allowedModelSlugs; page reads hydrate its entries); helpMore, advanced, recommendedMax, openMode, config.scale, ui.layout rows|chips|gallery, ui.preview.fallback, showIf notEmpty|empty.";
|
|
2761
2789
|
var LAYOUT_LOCK_NOTE = "A Global field with `overridable:false` + `lockReason` (add-field `field`, update-field `patch`) refuses page overrides (LAYOUT_FIELD_NOT_OVERRIDABLE); get_layout scope page lists stored ones as `shadowed`.";
|
|
2762
2790
|
|
|
2763
2791
|
// src/entry-query.ts
|
|
@@ -2922,8 +2950,15 @@ var fieldType = z.enum([
|
|
|
2922
2950
|
"file",
|
|
2923
2951
|
"sections",
|
|
2924
2952
|
"multiselect",
|
|
2925
|
-
"icon"
|
|
2953
|
+
"icon",
|
|
2954
|
+
// V2: ONE nested object of `fields`, kept as `object` (not normalized to zones). `toField` recurses into it.
|
|
2955
|
+
"object",
|
|
2956
|
+
// V2 leaves: a taxonomy string[], a rating number, a code value. The pass-through branch carries their config.
|
|
2957
|
+
"tags",
|
|
2958
|
+
"rating",
|
|
2959
|
+
"code"
|
|
2926
2960
|
]);
|
|
2961
|
+
var OPEN_MODE_INVALID = "openMode_invalid: openMode must be one of 'inline', 'drill', 'popover', 'dialog'";
|
|
2927
2962
|
var slug = z.string().regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only");
|
|
2928
2963
|
var fieldKey = z.string().regex(/^[a-zA-Z0-9_-]+$/, "letters, numbers, underscores, and hyphens only");
|
|
2929
2964
|
var uiObject = z.strictObject({
|
|
@@ -2931,12 +2966,13 @@ var uiObject = z.strictObject({
|
|
|
2931
2966
|
preview: z.strictObject({
|
|
2932
2967
|
title: z.string().min(1).optional().describe("CHILD KEY whose value titles a collapsed row"),
|
|
2933
2968
|
subtitle: z.string().min(1).optional().describe("CHILD KEY whose value is the row's second line (e.g. 'level' \u2192 'Heading \xB7 h2')"),
|
|
2934
|
-
media: z.string().min(1).optional().describe("CHILD KEY whose value is the row's thumbnail (image/file/url all work)")
|
|
2969
|
+
media: z.string().min(1).optional().describe("CHILD KEY whose value is the row's thumbnail (image/file/url all work)"),
|
|
2970
|
+
fallback: z.string().min(1).max(60).optional().describe("a literal row label while the title child is empty ('Untitled question'), not a key")
|
|
2935
2971
|
}).optional().describe(
|
|
2936
2972
|
"how ONE ITEM summarises itself when collapsed. Every slot names a CHILD KEY of THIS field/prop \u2014 not a value, not a dotted path. Without it a repeater of testimonials reads 'Item 1, Item 2, Item 3'; with {title:'author', media:'avatar'} it reads the names with their faces."
|
|
2937
2973
|
),
|
|
2938
|
-
layout: z.enum(["list", "grid", "table"]).optional().describe(
|
|
2939
|
-
"how the ITEMS are arranged. Omit (\u2261 'list') for rows of text; 'grid' for items whose thumbnail is the thing you scan \u2014 a gallery, a logo wall, a team; 'table' for up to 3 scalar columns. Same placement rule as `preview`."
|
|
2974
|
+
layout: z.enum(["list", "grid", "table", "rows", "chips", "gallery"]).optional().describe(
|
|
2975
|
+
"how the ITEMS are arranged. Omit (\u2261 'list') for rows of text; 'grid' for items whose thumbnail is the thing you scan \u2014 a gallery, a logo wall, a team; 'table' for up to 3 scalar columns. Same placement rule as `preview`. V2 'rows' | 'chips' | 'gallery' are also valid on a primitive array."
|
|
2940
2976
|
),
|
|
2941
2977
|
reorderable: z.boolean().optional().describe(
|
|
2942
2978
|
"omit (\u2261 true) unless the order is PART OF THE MEANING. `false` LOCKS the list \u2014 a 'three steps' band that must stay three steps in that order, a nav whose order is semantic, a timeline. Valid only where there is a list to lock."
|
|
@@ -2947,7 +2983,7 @@ var uiObject = z.strictObject({
|
|
|
2947
2983
|
// A refine, not an enum: 236 names inlined into every schema that carries `ui` would be paid on
|
|
2948
2984
|
// every tools/list. The layoutIcon precedent; the server's z.enum is the guard.
|
|
2949
2985
|
icon: z.string().min(1).max(40).refine((v) => DOCK_ICON_SET.has(v), `not a dock icon, see ${DOCK_ICONS_URI}`).optional().describe(`the branch row's icon, a name from ${DOCK_ICONS_URI}. Nesting fields only.`),
|
|
2950
|
-
itemLabel: z.string().min(1).max(24).optional().describe("what ONE item is called ('Slide'): the add button, crumbs and empty state. Nesting fields only."),
|
|
2986
|
+
itemLabel: z.string().min(1).max(24).optional().describe("what ONE item is called ('Slide'): the add button, crumbs and empty state. Nesting fields only. V2 itemNoun goes here."),
|
|
2951
2987
|
input: z.object({
|
|
2952
2988
|
name: z.string().regex(/^[a-z][a-z0-9-]{0,47}$/),
|
|
2953
2989
|
writes: z.array(z.string().min(1).max(64)).max(16).optional()
|
|
@@ -2964,23 +3000,24 @@ var propUiObject = uiObject.extend({
|
|
|
2964
3000
|
collapsed: z.boolean().optional()
|
|
2965
3001
|
}).optional().describe("the group this prop sits in. The FIRST prop of a group defines {id, label, kind?, icon?, collapsed?}; later props send only {id}.")
|
|
2966
3002
|
});
|
|
3003
|
+
var choiceOption = z.union([z.string(), z.object({ label: z.string(), value: z.string(), icon: z.string().optional(), description: z.string().optional() })]);
|
|
2967
3004
|
var fieldShape = {
|
|
2968
3005
|
key: fieldKey.describe(
|
|
2969
3006
|
"machine field key. \u{1F534} UNIQUE ACROSS THE WHOLE MODEL \u2014 API IDs share ONE FLAT NAMESPACE, so a field nested inside a group must NOT reuse a key used by another field or group. Every section's heading cannot be 'title'. Prefix with the section: 'hero_title', 'pricing_title', 'faq_title'. Reusing a key is refused, and if it slips through it leaves permanent errors in the editor and breaks conditional-visibility rules, which reference fields by bare key. Keys derived from an imported site may contain hyphens ('hero-h1-welcome'): address them exactly as returned."
|
|
2970
3007
|
),
|
|
2971
3008
|
label: z.string().min(1).describe("human label shown in the editor"),
|
|
2972
|
-
type: fieldType.describe(`field type. ${BUTTON_FIELD_NOTE}`),
|
|
3009
|
+
type: fieldType.describe(`field type. ${BUTTON_FIELD_NOTE} ${LINK_VALUE_NOTE}`),
|
|
2973
3010
|
required: z.boolean().optional().describe(
|
|
2974
3011
|
"an entry cannot be saved without a value \u2014 set it only where the site genuinely cannot render without one"
|
|
2975
3012
|
),
|
|
2976
3013
|
richText: z.boolean().optional().describe(
|
|
2977
3014
|
"prose formatting. DEFAULTS TO TRUE for 'text': the field is created as rich text so editors can bold, link and format it on the canvas, and the API returns rich text (render with rich() from @bettercms-ai/sdk; plain() for titles and meta). Pass false for a value that is NOT prose and must stay a bare string \u2014 a URL/href, a slug, an id, an email, a phone number, a CSS class, an icon name. A link stored as rich text will not work as an href."
|
|
2978
3015
|
),
|
|
2979
|
-
options: z.array(
|
|
2980
|
-
"choices when type is 'select'. A plain string, or {label, value} when the stored value differs from what the editor shows (e.g. {label:'Sold out', value:'soldOut'})."
|
|
3016
|
+
options: z.array(choiceOption).optional().describe(
|
|
3017
|
+
"choices when type is 'select'. A plain string, or {label, value, icon?, description?} when the stored value differs from what the editor shows (e.g. {label:'Sold out', value:'soldOut'}); icon a dock name, description one line \u2264140."
|
|
2981
3018
|
),
|
|
2982
3019
|
config: z.record(z.string(), z.unknown()).optional().describe(
|
|
2983
|
-
"per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, array {itemType: 'text'|'number'|'date'}, date {includeTime, timezone}, modular {blockSlugs: ['quote','gallery'], minItems?, maxItems?} \u2014 blockSlugs is REQUIRED, non-empty, and each slug must name an existing kind:'block' model; richtext {inline:true, hostTag:'h2', accept:['h2','p','b','i','u','link'], features:['bold','italic','underline','link','headings'], allowMultipleParagraphs:false} \u2014 for a field bound to ONE element on the page: `hostTag` is that element's tag and `accept` must name it, so the editor's block-style control opens already showing 'Heading 2' instead of 'Paragraph'. Name exactly ONE heading level: publish refuses an <h3> value bound to an <h2> rather than nesting one heading inside the other. Keep 'p' in `accept` or the sanitiser strips a block demoted to Paragraph"
|
|
3020
|
+
"per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, array {itemType: 'text'|'number'|'date'}, date {includeTime, timezone}, modular {blockSlugs: ['quote','gallery'], minItems?, maxItems?} \u2014 blockSlugs is REQUIRED, non-empty, and each slug must name an existing kind:'block' model; richtext {inline:true, hostTag:'h2', accept:['h2','p','b','i','u','link'], features:['bold','italic','underline','link','headings'], allowMultipleParagraphs:false} \u2014 for a field bound to ONE element on the page: `hostTag` is that element's tag and `accept` must name it, so the editor's block-style control opens already showing 'Heading 2' instead of 'Paragraph'. Name exactly ONE heading level: publish refuses an <h3> value bound to an <h2> rather than nesting one heading inside the other. Keep 'p' in `accept` or the sanitiser strips a block demoted to Paragraph; limits: text/longtext {minLength,maxLength,pattern,patternMessage}, richtext {minLength,maxLength}, number {validation:{min,max}}, array {minItems,maxItems}, tags {min,max}, rating {min,max,step}"
|
|
2984
3021
|
),
|
|
2985
3022
|
fields: z.array(z.lazy(() => fieldObject)).optional().describe(
|
|
2986
3023
|
"NESTED child fields \u2014 REQUIRED for type 'group' (one nested object, a Non-Repeatable Zone like blog_hero \u2192 heading, description, hero_image) and type 'repeater' (a repeatable array of such objects, a Repeatable Zone / section-list like testimonials \u2192 quote, author). A section with repeating items is a 'repeater'; a fixed grouped block is a 'group'. Recurse to any depth \u2014 do NOT flatten zones into separate top-level fields."
|
|
@@ -2996,8 +3033,8 @@ var fieldShape = {
|
|
|
2996
3033
|
rules: z.array(
|
|
2997
3034
|
z.object({
|
|
2998
3035
|
field: z.string().describe("the BARE KEY of another field on this same model"),
|
|
2999
|
-
op: z.enum(["eq", "neq", "in", "isSet"]),
|
|
3000
|
-
value: z.union([z.string(), z.array(z.string())]).optional().describe("omit for 'isSet' (the source holds any value); required for every other op")
|
|
3036
|
+
op: z.enum(["eq", "neq", "in", "isSet", "notEmpty", "empty"]),
|
|
3037
|
+
value: z.union([z.string(), z.array(z.string())]).optional().describe("omit for 'isSet'/'notEmpty' (the source holds any value) and 'empty' (it holds none); required for every other op")
|
|
3001
3038
|
})
|
|
3002
3039
|
)
|
|
3003
3040
|
}).optional().describe(
|
|
@@ -3016,10 +3053,15 @@ var fieldShape = {
|
|
|
3016
3053
|
defaultValue: z.unknown().optional().describe("the value a new entry starts with"),
|
|
3017
3054
|
searchable: z.boolean().optional().describe("omit (\u2261 true) to index this field for site search; false excludes it"),
|
|
3018
3055
|
unique: z.boolean().optional().describe(
|
|
3019
|
-
"true: no two entries of this model may hold the same value (compared trimmed and case-insensitively). On a repeater child such as a variant SKU it also covers every row of every entry. Only on text, longtext, slug, email, phone, link and
|
|
3056
|
+
"true: no two entries of this model may hold the same value (compared trimmed and case-insensitively). On a repeater child such as a variant SKU it also covers every row of every entry. Only on text, longtext, slug, email, phone, link, number, select, reference and date; on a list it means no item repeats. A clash is refused with 409 `unique_conflict`."
|
|
3020
3057
|
),
|
|
3021
3058
|
readOnly: z.union([z.literal(true), z.literal("developer")]).optional().describe(PARAM_TEXT.readOnly),
|
|
3022
|
-
lockReason: z.string().max(200).optional().describe(PARAM_TEXT.lockReason)
|
|
3059
|
+
lockReason: z.string().max(200).optional().describe(PARAM_TEXT.lockReason),
|
|
3060
|
+
// V2 schema language. Editor-only; the playbook's §14 table maps each to what the editor draws.
|
|
3061
|
+
helpMore: z.string().max(2e3).optional().describe(PARAM_TEXT.helpMore),
|
|
3062
|
+
advanced: z.boolean().optional().describe(PARAM_TEXT.advanced),
|
|
3063
|
+
recommendedMax: z.number().int().positive().optional().describe(PARAM_TEXT.recommendedMax),
|
|
3064
|
+
openMode: z.enum(["inline", "drill", "popover", "dialog"], { error: OPEN_MODE_INVALID }).optional().describe(PARAM_TEXT.openMode)
|
|
3023
3065
|
};
|
|
3024
3066
|
var fieldObject = z.object(fieldShape);
|
|
3025
3067
|
var urlPatternArg = z.string().nullable().optional().describe(
|
|
@@ -3034,13 +3076,16 @@ var fieldsetsArg = z.array(
|
|
|
3034
3076
|
tab: z.string().min(1).optional(),
|
|
3035
3077
|
icon: uiObject.shape.icon,
|
|
3036
3078
|
collapsed: z.boolean().optional(),
|
|
3079
|
+
collapsible: z.boolean().optional(),
|
|
3080
|
+
columns: z.union([z.literal(1), z.literal(2)]).optional(),
|
|
3081
|
+
layout: z.enum(["stack", "breakpoints"]).optional(),
|
|
3037
3082
|
// Server-owned provenance, as get_content_model returns it: an echo parses and is dropped
|
|
3038
3083
|
// before the write (dropProvenance), never refused.
|
|
3039
3084
|
uiOrigin: z.unknown().optional(),
|
|
3040
3085
|
uiBefore: z.unknown().optional()
|
|
3041
3086
|
})
|
|
3042
3087
|
).max(30).optional().describe(
|
|
3043
|
-
"the model's editor cards: [{id, name, description?, kind?, tab?, icon?, collapsed
|
|
3088
|
+
"the model's editor cards: [{id, name, description?, kind?, tab?, icon?, collapsed?, collapsible?, columns?:1|2, layout?:'breakpoints'}], at most 30, ids unique, order = array index. kind 'group' is a disclosure, 'row' puts up to 3 short scalars on one line, 'tab' is a tab (at most 3, top-level fields only; untabbed fields stay pinned above the tabs; a group's `tab` names its tab). A field joins one with `fieldsetId`. Editor-only: never changes what is stored or delivered."
|
|
3044
3089
|
);
|
|
3045
3090
|
var dropProvenance = (sets) => sets?.map(({ uiOrigin: _origin, uiBefore: _before, ...set }) => set) ?? sets;
|
|
3046
3091
|
var RICH_TEXT_NOTE = "\u{1F534} A field of type 'text' is created as RICH TEXT unless you send `richText: false` on it. Send `richText: false` for every value that is not prose: a name used as a title, a URL, slug, id, SKU, email, phone, CSS class or icon name.";
|
|
@@ -3126,8 +3171,16 @@ function toField(f) {
|
|
|
3126
3171
|
...f.unique !== void 0 ? { unique: f.unique } : {},
|
|
3127
3172
|
// The developer lock: stripped here, an agent writing a model back would unlock every field.
|
|
3128
3173
|
...f.readOnly !== void 0 ? { readOnly: f.readOnly } : {},
|
|
3129
|
-
...f.lockReason !== void 0 ? { lockReason: f.lockReason } : {}
|
|
3174
|
+
...f.lockReason !== void 0 ? { lockReason: f.lockReason } : {},
|
|
3175
|
+
// V2 editor keys: on `base` so the group/repeater rebuild cannot drop them.
|
|
3176
|
+
...f.helpMore !== void 0 ? { helpMore: f.helpMore } : {},
|
|
3177
|
+
...f.advanced !== void 0 ? { advanced: f.advanced } : {},
|
|
3178
|
+
...f.recommendedMax !== void 0 ? { recommendedMax: f.recommendedMax } : {},
|
|
3179
|
+
...f.openMode !== void 0 ? { openMode: f.openMode } : {}
|
|
3130
3180
|
};
|
|
3181
|
+
if (f.type === "object") {
|
|
3182
|
+
return { ...base, type: "object", fields: toFields(f.fields) };
|
|
3183
|
+
}
|
|
3131
3184
|
if (f.type === "group") {
|
|
3132
3185
|
return { ...base, type: "array", config: { zones: { nonRepeatable: toFields(f.fields) } } };
|
|
3133
3186
|
}
|
|
@@ -3210,7 +3263,7 @@ var GIT_TEXT = {
|
|
|
3210
3263
|
var HOSTING_TEXT = {
|
|
3211
3264
|
list_env_vars: "The project's environment variables by NAME and scope (never a value), and `missing`: the ones the repository's .env.example declares that the project does not set. A site missing them builds and serves, then renders empty or crashes in the browser. Resolve `missing` by running `npx @bettercms-ai/cli@latest env push` in the repository: it uploads those values from the user's own .env files straight to BetterCMS, so they never pass through this conversation. Never read, print or ask the user to paste a value. When the values do not exist locally because the site reads its content from another CMS, move that content into BetterCMS instead (the bettercms skill's external-content reference). BetterCMS's own BCMS_* identifiers are supplied to every build and never need setting.",
|
|
3212
3265
|
list_hosting_connections: "The Vercel, Netlify and Cloudflare accounts connected to this workspace (id, provider, accountLabel, status; never a token), plus `connectUrl`, the dashboard page where a person connects one. Call it before deploy_to_host. If the provider the user wants has no connection with status `ok`, give the user `connectUrl` to connect it in their browser and stop until they have: you never handle OAuth or a host token yourself.",
|
|
3213
|
-
deploy_to_host: "Deploy this project to the user's own Vercel or Netlify account through their connection. BetterCMS builds the site and the host serves that build
|
|
3266
|
+
deploy_to_host: "Deploy this project to the user's own Vercel or Netlify account through their connection. BetterCMS builds the site and the host serves that build (skill publishing). The first call creates the site on the host and makes it the project's host; a later call for the same provider redeploys that site (`redeployed: true`). `connectionId` (from list_hosting_connections) defaults to the newest working one, `siteName` to the project's handle. A refusal carries a `code` and a `fixUrl` (not_connected, reconnect, pick_existing_site, grant_required, ADMIN_REQUIRED): each is fixed by the user at `fixUrl`, so relay it and stop. `deploymentQueued: false` means the project has no live build yet: deploy_project first, and that release reaches the host on its own. Never run the Vercel or Netlify CLI while a connection exists. Then poll get_host_deploy_status. Needs artifact:write, like deploy_project.",
|
|
3214
3267
|
get_host_deploy_status: "The connected project's recent deployments to Vercel, Netlify or Cloudflare, newest first: state (queued|deploying|live|failed), providerUrl, error, sha and targetName. Poll it after deploy_to_host until the newest one is `live`, whose `providerUrl` is the live URL to hand the user, or `failed`, whose `error` you read back to them."
|
|
3215
3268
|
};
|
|
3216
3269
|
function ok(summary, data) {
|
|
@@ -3372,7 +3425,7 @@ function buildToolDefs(deps) {
|
|
|
3372
3425
|
]).describe("block type; section/slider/tabs/columns nest child blocks"),
|
|
3373
3426
|
id: z.string().min(1).describe("stable unique block id"),
|
|
3374
3427
|
props: z.record(z.string(), z.unknown()).describe(
|
|
3375
|
-
"per-type props: heading {text, level}; text/richtext {html} (NOT {text}), and both also take level 1-6, rendering the html AS that <hN> with inline marks kept (one line of inline copy only) \u2014 use it for a title that carries a styled span; image {src, alt}; button {text, href, variant?
|
|
3428
|
+
"per-type props: heading {text, level}; text/richtext {html} (NOT {text}), and both also take level 1-6, rendering the html AS that <hN> with inline marks kept (one line of inline copy only) \u2014 use it for a title that carries a styled span; image {src, alt}; button {text, href, variant? (hosted pages; else a Button component), size? sm|md|lg, fullWidth?, arrow? boolean, icon? ui.icon name, newTab? boolean, rel? nofollow|sponsored|ugc}; spacer {height}; video {url}; form {formId}; component {componentId, overrides?}; navbar {links:[{label,href}], logo?, cta?}; footer {columns, copyright?}; section {children: block[]}; columns {columns: block[][], gap} \u2014 a column may hold ANY block; slider {slides:[{id,children}]}; tabs {tabs:[{id,label,children}]}; collection {cardComponentId?, source? {modelId}, limit?, filters?, sort?, pinnedIds?, excludeCurrent?}: a collection's published entries as cards (every prop: skill content-modeling)"
|
|
3376
3429
|
),
|
|
3377
3430
|
style: z.record(z.string(), z.unknown()).optional().describe(
|
|
3378
3431
|
"design tokens: theme, bg (none|surface|muted|accent|dark|custom), bgCustom hex, paddingTop/paddingBottom/paddingSides px, contentWidth (narrow|default|wide|full), align, corner, shadow, borderTop/borderBottom, border (all four sides, in the text colour), hideOn [mobile|tablet|desktop], never all three. A real marketing band is a `section` block carrying bg + padding + contentWidth."
|
|
@@ -3617,10 +3670,10 @@ function buildToolDefs(deps) {
|
|
|
3617
3670
|
config: z.object({
|
|
3618
3671
|
componentIds: z.array(z.string()).optional(),
|
|
3619
3672
|
// 'slot' allowlist
|
|
3620
|
-
options: z.array(
|
|
3673
|
+
options: z.array(choiceOption).optional(),
|
|
3621
3674
|
// 'select' — required, choices
|
|
3622
|
-
// 'group'/'table' sub-shape. Typed loosely here (the server validates the full
|
|
3623
|
-
// recursive shape) so a
|
|
3675
|
+
// 'group'/'table'/'object' sub-shape. Typed loosely here (the server validates the full
|
|
3676
|
+
// recursive shape, 10 levels at most) so a deep tree needs no Zod mirror in the client.
|
|
3624
3677
|
fields: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
3625
3678
|
min: z.number().optional(),
|
|
3626
3679
|
// 'number'
|
|
@@ -3641,15 +3694,20 @@ function buildToolDefs(deps) {
|
|
|
3641
3694
|
match: z.enum(["all", "any"]),
|
|
3642
3695
|
rules: z.array(z.object({
|
|
3643
3696
|
field: z.string().min(1),
|
|
3644
|
-
op: z.enum(["eq", "neq", "in", "isSet"]),
|
|
3697
|
+
op: z.enum(["eq", "neq", "in", "isSet", "notEmpty", "empty"]),
|
|
3645
3698
|
value: z.union([z.string(), z.array(z.string())]).optional()
|
|
3646
3699
|
})).min(1).max(20)
|
|
3647
|
-
}).optional().describe("show this prop only while SIBLING props (by key) match: {match:'all'|'any', rules:[{field, op:'eq'|'neq'|'in'|'isSet', value?}]}. 'isSet'
|
|
3700
|
+
}).optional().describe("show this prop only while SIBLING props (by key) match: {match:'all'|'any', rules:[{field, op:'eq'|'neq'|'in'|'isSet'|'notEmpty'|'empty', value?}]}. 'isSet', 'notEmpty' and 'empty' take no value. Editor-only."),
|
|
3701
|
+
// V2 editor-only keys, the model field's (playbook §14). Declared: this is a strip site.
|
|
3702
|
+
helpMore: z.string().max(2e3).optional().describe(PARAM_TEXT.helpMore),
|
|
3703
|
+
advanced: z.boolean().optional().describe(PARAM_TEXT.advanced),
|
|
3704
|
+
recommendedMax: z.number().int().positive().optional().describe(PARAM_TEXT.recommendedMax),
|
|
3705
|
+
openMode: z.enum(["inline", "drill", "popover", "dialog"], { error: OPEN_MODE_INVALID }).optional().describe("object/group/table props: how the value (or each row) opens"),
|
|
3648
3706
|
// ⚠ A STRIP SITE, exactly like OutField above: this is a `z.object`, so a key the model
|
|
3649
3707
|
// sends and this shape does not declare is dropped BEFORE the request leaves — a 200 with
|
|
3650
3708
|
// the declaration gone. `ui` rode through the backend's looseObject `config` and died here.
|
|
3651
3709
|
ui: propUiObject.optional().describe(
|
|
3652
|
-
"AUTHORING CHROME for this prop's control in the inspector \u2014 never a delivery effect, and the same object a FIELD's `ui` takes. `preview` and `layout` are valid on 'group', 'table' and 'slot'
|
|
3710
|
+
"AUTHORING CHROME for this prop's control in the inspector \u2014 never a delivery effect, and the same object a FIELD's `ui` takes. `preview` and `layout` are valid on 'group', 'table', 'object' and 'slot' (`layout` 'rows'|'chips'|'gallery' also on 'multiselect', 'tags'); `reorderable` on 'table' ALONE, the one prop type whose value is a LIST (a 'group' is one object, a 'slot' one component instance). preview keys name a SUB-FIELD of this prop's config.fields and are checked against them (on a 'slot' the shape is checked but the keys cannot be \u2014 they belong to whichever component fills it). An unknown key inside `ui` is a 400, not a silent strip. Read it back with get_component: a prop whose `ui` is absent there was not stored."
|
|
3653
3711
|
)
|
|
3654
3712
|
});
|
|
3655
3713
|
const getLayoutInput = z.object({
|
|
@@ -3728,7 +3786,7 @@ function buildToolDefs(deps) {
|
|
|
3728
3786
|
description: z.string().optional().describe(PARAM_TEXT.describes),
|
|
3729
3787
|
blockJson: z.array(blockObject).default([]).describe("the component's block tree"),
|
|
3730
3788
|
props: z.array(componentPropObject).default([]).describe(
|
|
3731
|
-
"overridable fields. DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch: only the leaves a prop targets are click-to-edit. Each one may carry `ui` to say how its control presents in the inspector \u2014 a collapsed group, what titles and thumbnails a table's rows, whether that table's rows can be dragged at all. 'button' is ONE unit {label, href, variant?, size?, arrow?, icon?, newTab?} on a button block's 'props', never a text prop plus a url prop. 'link' an href, 'date' YYYY-MM-DD, 'color' a hex, 'file' {url, name?}, 'icon' an icon name or an image, 'multiselect' a string[] from config.options (required). " + SLOT_MULTIPLE_NOTE
|
|
3789
|
+
"overridable fields. DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch: only the leaves a prop targets are click-to-edit. Each one may carry `ui` to say how its control presents in the inspector \u2014 a collapsed group, what titles and thumbnails a table's rows, whether that table's rows can be dragged at all. 'button' is ONE unit {label, href, variant?, size?, arrow?, icon?, newTab?} on a button block's 'props', never a text prop plus a url prop. 'link' an href, 'date' YYYY-MM-DD, 'color' a hex, 'file' {url, name?}, 'icon' an icon name or an image, 'multiselect' a string[] from config.options (required). " + SLOT_MULTIPLE_NOTE + " " + PROP_V2_NOTE
|
|
3732
3790
|
)
|
|
3733
3791
|
});
|
|
3734
3792
|
const updateComponentInput = z.object({
|
|
@@ -4273,7 +4331,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4273
4331
|
def(
|
|
4274
4332
|
"set_binding_mode",
|
|
4275
4333
|
"Set how the site's bindings are resolved",
|
|
4276
|
-
"Switch this project between the two binding resolvers, from the NEXT release on. `declaredBindings: true` makes the annotator trust the template's own data-bcms-field / data-bcms-props and never guess from rendered text \u2014 the durable state; `false` returns to text-matching, which
|
|
4334
|
+
"Switch this project between the two binding resolvers, from the NEXT release on. `declaredBindings: true` makes the annotator trust the template's own data-bcms-field / data-bcms-props and never guess from rendered text \u2014 the durable state; `false` returns to text-matching, which holds only while the CMS values equal the built copy. Call it ONLY after every page's copy is declared in the template: undeclared fields stop being editable. The order around the flip (push, release, clean get_binding_report, flip, release, confirm): skill bindings. Flipping back is the same call. REQUIRES artifact:write (the authority that deploys the site): this decides what every future release does to every page. On a workspace-wide connection pass `projectId`: the scope is there, but the human who authorized the connection must be able to publish THAT project or it is refused with 403 PROJECT_PUBLISH_DENIED. Playbook section 13.",
|
|
4277
4335
|
z.object({ declaredBindings: z.boolean().describe("true = trust the template's declared bindings; false = text-match (the default)") }).shape,
|
|
4278
4336
|
async (c, a) => ok("Recorded the binding mode.", await data(c, "PATCH", `/management/projects/current/binding-mode`, { declaredBindings: a.declaredBindings }))
|
|
4279
4337
|
),
|
|
@@ -4408,7 +4466,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4408
4466
|
def(
|
|
4409
4467
|
"update_page",
|
|
4410
4468
|
"Edit a page",
|
|
4411
|
-
"Edit a page: title, slug, SEO metaTitle/metaDescription, structured data (`schemaType`, `schema`), publish status (draft|published), and `blockJson` (its block composition \u2014 passing it REPLACES the whole array, so read get_page first and send its `optimisticVersion` as `ifMatch`). A `blockJson` that swaps a component block to another component comes back with `warnings` (COMPONENT_SWAP_DROPS_OVERRIDES)
|
|
4469
|
+
"Edit a page: title, slug, SEO metaTitle/metaDescription, structured data (`schemaType`, `schema`), publish status (draft|published), and `blockJson` (its block composition \u2014 passing it REPLACES the whole array, so read get_page first and send its `optimisticVersion` as `ifMatch`). A `blockJson` that swaps a component block to another component comes back with `warnings` (COMPONENT_SWAP_DROPS_OVERRIDES: skill components). A section change the live page cannot show comes back in `holds`: saved as a draft, not live until each `action` is taken (skill publishing). It does NOT change the field SCHEMA \u2014 use add_field (pageId) / set_page_content for that. Renaming the slug keeps content intact. Publishing copies the draft blocks live in the same call \u2014 and on a singleton it publishes the page's content entry too; if that is refused, the page still goes live and `warnings` carries ENTRY_STILL_DRAFT (skill publishing). STRUCTURED DATA is native SEO, never a field: Automatic JSON-LD from the content, unless `schemaType` picks a schema.org type or pasted `schema` (wins over both, null clears it) is set (skill seo). " + PAGE_HEAD_NOTE + " " + SECTION_DOCTRINE,
|
|
4412
4470
|
z.object({ ...headPatchShape, noindex: z.boolean().optional().describe("hide this page from search engines and site search"), pageId: z.string().min(1).describe(PARAM_TEXT.pageRef), 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(), schemaType: z.enum(["auto", ...SCHEMA_ORG_TYPES]).optional().describe("structured data type; 'auto' (the default) derives it from the content"), schema: z.union([z.record(z.string(), z.unknown()), z.array(z.record(z.string(), z.unknown()))]).nullable().optional().describe("custom JSON-LD; wins over schemaType; null clears it"), status: z.enum(["draft", "published"]).optional(), scheduledAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageScheduledAt), unpublishAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageUnpublishAt), locale: z.string().nullable().optional().describe(PARAM_TEXT.locale), translationOf: z.string().nullable().optional().describe(PARAM_TEXT.translationOf), ifMatch: z.number().int().min(0).optional().describe(UPDATE_PAGE_IF_MATCH_NOTE), dynamic: z.object({ previewPath: z.string().nullable().optional().describe(PARAM_TEXT.previewPath), modelId: z.string().min(1).optional().describe(PARAM_TEXT.pageModelId) }).optional().describe(PARAM_TEXT.pageDynamic) }).shape,
|
|
4413
4471
|
async (c, a) => {
|
|
4414
4472
|
const res = await c.fetchJSON(
|
|
@@ -4421,8 +4479,9 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4421
4479
|
);
|
|
4422
4480
|
const note = res.redirectNote;
|
|
4423
4481
|
const summary = note ? `Updated page. An existing redirect from ${note.source} already points to ${note.existingDestination}; it wasn't changed, so the old address does not redirect to the new one.` : "Updated page.";
|
|
4424
|
-
|
|
4425
|
-
|
|
4482
|
+
const held = (res.holds ?? []).map((h) => `HELD ${h.reason}: ${h.message} Action: ${h.action.label}.`);
|
|
4483
|
+
const lines = [summary, ...res.warnings ?? [], ...held].join("\n");
|
|
4484
|
+
return ok(lines, res.holds?.length ? { ...res.data, holds: res.holds } : res.data);
|
|
4426
4485
|
}
|
|
4427
4486
|
),
|
|
4428
4487
|
// ── Code + deploy (parity with remote /mcp; needs artifact:write) ──
|
|
@@ -4569,6 +4628,10 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4569
4628
|
placement: z.enum(["cover", "title", "excerpt", "body", "inline", "panel"]).nullable().optional(),
|
|
4570
4629
|
readOnly: z.union([z.literal(true), z.literal("developer")]).nullable().optional().describe(PARAM_TEXT.lockClears),
|
|
4571
4630
|
lockReason: z.string().max(200).nullable().optional().describe(PARAM_TEXT.lockClears),
|
|
4631
|
+
helpMore: z.string().max(2e3).nullable().optional().describe(PARAM_TEXT.helpMore),
|
|
4632
|
+
advanced: z.boolean().nullable().optional().describe(PARAM_TEXT.advanced),
|
|
4633
|
+
recommendedMax: z.number().int().positive().nullable().optional().describe(PARAM_TEXT.recommendedMax),
|
|
4634
|
+
openMode: z.enum(["inline", "drill", "popover", "dialog"], { error: OPEN_MODE_INVALID }).nullable().optional().describe(PARAM_TEXT.openMode),
|
|
4572
4635
|
type: fieldType.optional().describe("a new type; needs allowRetype"),
|
|
4573
4636
|
key: fieldKey.optional().describe("a new key (flat namespace, unique in the schema)"),
|
|
4574
4637
|
allowRetype: z.boolean().optional().describe("confirm a type change, once the user has agreed"),
|
|
@@ -5073,7 +5136,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
5073
5136
|
def(
|
|
5074
5137
|
"get_deploy_status",
|
|
5075
5138
|
"Get deploy/build status",
|
|
5076
|
-
"Get the connected project's deploy/build status: state (idle|queued|building|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `editorSetup`: whether editing is set up for the live build (never or stale = call editor_setup). Poll this after deploy_project until state is idle with your sha live. `release` and `
|
|
5139
|
+
"Get the connected project's deploy/build status: state (idle|queued|building|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `editorSetup`: whether editing is set up for the live build (never or stale = call editor_setup). Poll this after deploy_project until state is idle with your sha live. `release`, `held` and `publishes` (each `live` or `partial`): skill publishing. `runtime` ({state: ok|crash-loop, restarts, oomKilled}) appears for a server app only: crash-loop means its container keeps restarting (often out of memory) though the build went live fine. Also returns `canvas.lane` (skill bindings) and `editorUrl`, the visual editor to hand the user.",
|
|
5077
5140
|
z.object({}).shape,
|
|
5078
5141
|
// Flat body, no `data` envelope: `data()` read it as undefined.
|
|
5079
5142
|
async (c) => ok("Deploy status.", await c.fetchJSON(c.url(`/management/projects/deploy-status`), { method: "GET" }))
|
|
@@ -5084,10 +5147,12 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
5084
5147
|
NEW_TOOL_TEXT.editor_setup,
|
|
5085
5148
|
z.object({
|
|
5086
5149
|
force: z.boolean().optional().describe(PARAM_TEXT.editorSetupForce),
|
|
5087
|
-
check: z.boolean().optional().describe(PARAM_TEXT.editorSetupCheck)
|
|
5150
|
+
check: z.boolean().optional().describe(PARAM_TEXT.editorSetupCheck),
|
|
5151
|
+
exempt: z.array(z.record(z.string(), z.unknown())).optional().describe(PARAM_TEXT.editorSetupExempt)
|
|
5088
5152
|
}).shape,
|
|
5089
5153
|
// The route answers `{ state, … }` with no `data` envelope, so the whole body is the result.
|
|
5090
|
-
|
|
5154
|
+
// `exempt` records setup-gate exceptions instead of running anything (the gate's one write).
|
|
5155
|
+
async (c, a) => a.exempt ? ok("Setup gate exceptions.", await c.fetchJSON(c.url(`/management/projects/setup-gate/exceptions`), { method: "POST", body: JSON.stringify({ exempt: a.exempt }) })) : ok("Editor setup.", await c.fetchJSON(c.url(`/management/projects/editor-setup`), a.check === true ? { method: "GET" } : { method: "POST", body: JSON.stringify({ force: a.force }) }))
|
|
5091
5156
|
),
|
|
5092
5157
|
// ── Deploy to the user's own host (parity with remote /mcp) ──
|
|
5093
5158
|
// A person connects Vercel or Netlify in the dashboard; nothing here touches OAuth or a host
|
|
@@ -5240,7 +5305,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5240
5305
|
name: "add_field",
|
|
5241
5306
|
config: {
|
|
5242
5307
|
title: "Add a field to a content model or a page",
|
|
5243
|
-
description: ADD_FIELD_DESCRIPTION + " " + RICH_TEXT_NOTE,
|
|
5308
|
+
description: ADD_FIELD_DESCRIPTION + " " + V2_LEAF_TYPES_NOTE + " " + RICH_TEXT_NOTE,
|
|
5244
5309
|
inputSchema: addFieldInput.shape
|
|
5245
5310
|
},
|
|
5246
5311
|
handler: guard(
|
|
@@ -5771,7 +5836,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5771
5836
|
client.url(`/management/pages/${pageId}/meta`),
|
|
5772
5837
|
{ method: "PATCH", body: JSON.stringify({ blockJson: page.blockJson, ...args.dryRun ? { dryRun: true } : {} }) }
|
|
5773
5838
|
);
|
|
5774
|
-
results.push({ ok: true, ...args.dryRun ? { status: "would-update" } : {}, pageId, slug: written.data?.slug ?? page.slug, path: written.data?.path, blocks: page.blockJson.length });
|
|
5839
|
+
results.push({ ok: true, ...args.dryRun ? { status: "would-update" } : {}, pageId, slug: written.data?.slug ?? page.slug, path: written.data?.path, blocks: page.blockJson.length, ...written.holds?.length ? { holds: written.holds } : {} });
|
|
5775
5840
|
} catch (err) {
|
|
5776
5841
|
results.push({ ok: false, pageId, slug: page.slug, error: errorText(err) });
|
|
5777
5842
|
}
|
|
@@ -6089,6 +6154,9 @@ DERIVED by \`editor_setup\`, where the componentize lane in \xA710 reuses every
|
|
|
6089
6154
|
A call to action (words + link)? -> ONE \`button\` field {label, href, variant?, size?,
|
|
6090
6155
|
arrow?, icon?, newTab?} \u2014 never text + link fields
|
|
6091
6156
|
(in a component: a \`button\` block, \xA73)
|
|
6157
|
+
A link to a page or CMS item? -> a \`link\` value {type:'page'|'entry', href, ref:<id>}:
|
|
6158
|
+
the write checks ref, delivery resolves it;
|
|
6159
|
+
a plain href string stays valid
|
|
6092
6160
|
Site-wide nav/footer/modal? -> the LAYOUT (get_layout/update_layout), never a
|
|
6093
6161
|
page section and never one component per page
|
|
6094
6162
|
|
|
@@ -6239,7 +6307,7 @@ is interpolated per entry with \`{{fieldKey}}\` placeholders \u2014 \`{{title}}\
|
|
|
6239
6307
|
6. content create_content_entry / set_page_content
|
|
6240
6308
|
7. publish update_page status:'published'
|
|
6241
6309
|
7b. layout publish_layout <- if you authored the Global Layout (nav/footer); page overrides publish with the page
|
|
6242
|
-
8. check get_next_steps, and fix what it lists
|
|
6310
|
+
8. check get_next_steps, and fix what it lists; on an imported site, until receipt.complete is true
|
|
6243
6311
|
|
|
6244
6312
|
## 8. Two ways to ship a blank site
|
|
6245
6313
|
|
|
@@ -6415,7 +6483,8 @@ the release's characters. Ornaments, skip links and the platform badge are exclu
|
|
|
6415
6483
|
4. deploy again and re-read. Repeat until every route is under 2%.
|
|
6416
6484
|
|
|
6417
6485
|
\`get_next_steps\` runs the same test and hands you the worst route with its buckets
|
|
6418
|
-
(\`unaddressable-copy\`).
|
|
6486
|
+
(\`unaddressable-copy\`). Its \`receipt.gate\` holds EVERY built route to that bar, beside its page,
|
|
6487
|
+
its images and its links, and \`receipt.complete\` stays false until each route passes. A \`coverage\` carrying \`error: "nothing-bound"\` means NOT ONE element
|
|
6419
6488
|
of the build is bound \u2014 usually the singleton lane declined the project (\`skipReasons\`
|
|
6420
6489
|
\`authored-content\`: its content models were authored by the repo), so the percentage beside it
|
|
6421
6490
|
describes a site this build is not.
|
|
@@ -6550,9 +6619,12 @@ below is one heading of that ask.
|
|
|
6550
6619
|
the \`add-component\` command. CTA buttons are \`button\` BLOCKS inside the section that uses them,
|
|
6551
6620
|
exposed as \`text\` + \`url\` props that target that ONE block \u2014 never the words as a separate text
|
|
6552
6621
|
block beside it, or the Button panel edits nothing \u2014 variant, size and fullWidth are that block's own props and
|
|
6553
|
-
\`style\`. Do not hand-build a Button component
|
|
6554
|
-
family (Globals/Buttons) on its own, and a new look is \`create_component\` with
|
|
6555
|
-
Button. On
|
|
6622
|
+
\`style\`. Do not hand-build a Button component. On a site built here every button look you write joins
|
|
6623
|
+
the project's Button family (Globals/Buttons) on its own, and a new look is \`create_component\` with
|
|
6624
|
+
\`variantOf\` naming a Button. On an IMPORTED site the family is the looks measured in the site and
|
|
6625
|
+
nothing you write adds to it: read it with \`list_components\`, place a look as a \`component\` block,
|
|
6626
|
+
and never add CSS for a look the site does not have. On a page's FIELDS a CTA is one \`button\` field
|
|
6627
|
+
(\xA72), whose \`variant\` picks a look of that family. Rich text is a \`richtext\` prop over a
|
|
6556
6628
|
\`richtext\`/\`text\` block. Forms are \`create_form\` plus a \`form\` block, and form FIELDS are the
|
|
6557
6629
|
one place the platform models \`required\`, \`placeholder\`, \`helpText\` and real \`validation\`
|
|
6558
6630
|
(emailPolicy, min/max, phoneFormat, pattern) \u2014 put a site's validated inputs there. Cards,
|
|
@@ -6880,6 +6952,15 @@ conversion is yours to write.
|
|
|
6880
6952
|
fetch the live URL cache-busted (RECEIPTS in \xA711: publish and deploy are separate claims).
|
|
6881
6953
|
\`get_next_steps\` keeps reporting the gap until every one of these holds.
|
|
6882
6954
|
|
|
6955
|
+
**The finish line is \`receipt.complete\`.** \`get_next_steps\` answers it from the setup gate
|
|
6956
|
+
(\`receipt.gate\`): every route of the live build has a published page (or entry), one page only,
|
|
6957
|
+
at most 2% of its visible text unowned, every image a field's and 95% of its links. \`blocking\`
|
|
6958
|
+
lists what fails, worst route first, each with the \`fix\` call to run; a null \`fix\` is the
|
|
6959
|
+
user's to decide. A gap left on purpose is RECORDED, \`editor_setup { exempt: [{ route, check,
|
|
6960
|
+
reason, note }] }\`, with a reason \`allowedExceptions\` lists for that check. Running out of time,
|
|
6961
|
+
context or patience is in \`neverExceptions\`: it is refused, and telling the user why instead
|
|
6962
|
+
leaves the gate incomplete. \`bettercms init\` reads the same gate when you exit and prints it.
|
|
6963
|
+
|
|
6883
6964
|
**Done means.** Whichever recipe you ran: \`get_binding_report\` reads \`mode "declared"\`,
|
|
6884
6965
|
\`unmatched\` empty, \`bound\` above 0, \`coverage.pending\` empty, and \`canvas.lane\` is \`bridge\` or
|
|
6885
6966
|
\`draft-route\` when the site should show structural drafts.
|
|
@@ -6969,7 +7050,7 @@ FIELD never takes \`ui.group\`; it joins a fieldset with \`fieldsetId\`.
|
|
|
6969
7050
|
- **leaf** \u2014 one field, one control. Nothing to declare; \`ui.control\` only when the default is wrong.
|
|
6970
7051
|
- **group** \u2014 a \`kind: 'group'\` fieldset (a prop: \`ui.group\`): a disclosure that shows two values
|
|
6971
7052
|
when closed. \`collapsed: true\` starts it closed.
|
|
6972
|
-
- **row** \u2014 a \`kind: 'row'\` fieldset: at most 3 short scalars (number, color, boolean, date,
|
|
7053
|
+
- **row** \u2014 a \`kind: 'row'\` fieldset: at most 3 short scalars (number, rating, color, boolean, date,
|
|
6973
7054
|
datetime, select) on one line. No \`ui.control\` on a row member.
|
|
6974
7055
|
- **branch** \u2014 a nesting field drawn as a drill-in row with its \`ui.icon\` and a summary.
|
|
6975
7056
|
- **list** \u2014 repeatable items named by \`ui.itemLabel\` and \`ui.preview\`.
|
|
@@ -7002,6 +7083,32 @@ Importance orders a screen: required fields first, then array order.
|
|
|
7002
7083
|
| CTA | one \`kind:'group'\` fieldset holding the heading, text and ONE \`button\` field, \`icon:'launch'\` |
|
|
7003
7084
|
| Dimensions | one \`kind:'row'\` fieldset of up to 3 numbers (width, height, gap) |
|
|
7004
7085
|
|
|
7086
|
+
**The V2 schema language \u2014 to get X in the editor, write Y.** Every key below is editor-only and stored as sent, EXCEPT \`type: 'object'\`: it changes the stored and delivered value to \`{ [childKey]: value }\` (a \`group\` carries \`<key>.nonRepeatable.<sub>\` instead), and codegen types it as a nested object, so swapping group \u2194 object moves values and templates.
|
|
7087
|
+
|
|
7088
|
+
| To get this in the editor | Write this |
|
|
7089
|
+
|---|---|
|
|
7090
|
+
| A section that starts closed / a static heading | fieldset \`collapsed: true\` / \`collapsible: false\` |
|
|
7091
|
+
| Two short fields side by side | fieldset \`columns: 2\` |
|
|
7092
|
+
| One Desktop \xB7 Tablet \xB7 Phone switcher | three \`type: 'object'\` fields named \`desktop\`, \`tablet\`, \`phone\`, each with \`fieldsetId\` pointing at ONE fieldset with \`layout: 'breakpoints'\` |
|
|
7093
|
+
| A small inset of 2\u20133 fields, or a row that opens its own screen | \`type: 'object'\` with \`fields\`; \`openMode: 'inline', 'drill', 'popover', 'dialog'\` |
|
|
7094
|
+
| List rows that drill in instead of expanding | \`openMode: 'drill'\` on the array (or repeater) |
|
|
7095
|
+
| Tiles instead of rows | \`ui.layout: 'grid'\` on a list of objects (V2 \`arrayLayout: 'grid'\`); a primitive image array takes \`'gallery'\` |
|
|
7096
|
+
| BetterCMS-only list layouts | \`ui.layout: 'rows'\` (\u2261 'list'), \`'chips'\`, \`'gallery'\`, also on a primitive array. Never chips for a feature list: chips cut long lines |
|
|
7097
|
+
| A size capsule with numeric steps | \`type: 'number'\`, \`config.scale: { steps: {min, max, step} or [{value, label}], units?, presets? }\`. Token (string) steps and an \`allowCustom\` object are refused (\`scale_invalid\`): use numeric steps and \`allowCustom: true\` |
|
|
7098
|
+
| Help under the label / longer help behind \u24D8 | \`helpText\` / \`helpMore\` |
|
|
7099
|
+
| Calm defaults for rare settings | \`advanced: true\` |
|
|
7100
|
+
| A soft length warning (amber, never a refusal) | \`recommendedMax: 60\` |
|
|
7101
|
+
| A marketer cannot change it | \`readOnly: 'developer'\` plus \`lockReason\` |
|
|
7102
|
+
| A field that shows only sometimes | \`showIf\` (it SHOWS the field): ops \`eq\`, \`neq\`, \`in\`, \`isSet\`, \`notEmpty\`, \`empty\` |
|
|
7103
|
+
| A good row title and thumbnail | \`ui.preview: { title, subtitle, media, fallback }\` (\`fallback\` is a literal label) |
|
|
7104
|
+
| Labels that classify or filter (topics, keywords), never a feature list | \`type: 'tags'\`: a string[], trimmed, de-duplicated, each \u226464 characters; \`config.options\` are suggestions, \`config.max\`, \`config.allowCustom: false\` closes the list (\`tags_value_invalid\`) |
|
|
7105
|
+
| A star rating | \`type: 'rating'\`: a number from 0 to \`config.max\` (5), \`config.step\` 1 or 0.5 (\`rating_value_invalid\`) |
|
|
7106
|
+
| A code snippet | \`type: 'code'\`: \`{ language?, code }\` or a string; \`config.language\` default, \`config.languages\` allowlist (\`code_value_invalid\`) |
|
|
7107
|
+
| "Add question" instead of "Add item" | \`ui.itemLabel: 'Question'\` (V2 \`itemNoun\`; there is no itemNoun key) |
|
|
7108
|
+
| The same section as a component (the dock edits a page section through its props) | the same keys on its \`props\`: \`type: 'object'\` with \`config.fields\` (\`group\` and \`table\` too, 10 levels), \`datetime\`, \`slug\`, \`email\`, \`phone\`, \`json\`, \`location\`, \`tags\`, \`rating\`, \`code\`, plus the two CMS-item types (\`reference\`, \`multi-reference\`: \`config.contentModelId\` or \`polymorphic\` + \`allowedModelSlugs\`; page reads hydrate them, a text slot shows the title), and every key above except fieldsets (props group with \`ui.group\`) |
|
|
7109
|
+
|
|
7110
|
+
An unknown \`openMode\` is refused with \`openMode_invalid\`, one on a leaf \`openMode_leaf\`; a bad \`config.scale\` with \`scale_invalid\`; more than 10 nested field levels with \`field_depth_exceeded\`. update_field sets and clears (null) helpMore, advanced, recommendedMax and openMode on an existing field.
|
|
7111
|
+
|
|
7005
7112
|
**Tabs.** A \`kind: 'tab'\` fieldset is a tab:
|
|
7006
7113
|
- at most **3** tabs, names at most 20 characters and unique;
|
|
7007
7114
|
- top-level fields only: tabs never nest and props never take them;
|
|
@@ -7250,8 +7357,8 @@ error-prone, so go slow and confirm. Never guess the layout.
|
|
|
7250
7357
|
the same prop keys across a family or a swap drops content.
|
|
7251
7358
|
4. **Props** \u2014 declare only what should really be editable:
|
|
7252
7359
|
{ key, label, target: { blockId, path },
|
|
7253
|
-
type: text|richtext|image|url|boolean|number|select|group|table|slot|form|link|button|date|color|file|icon|multiselect }.
|
|
7254
|
-
'select'/'multiselect' need config.options; 'group'/'table' need config.fields; a 'button'
|
|
7360
|
+
type: text|richtext|image|url|boolean|number|select|group|table|slot|form|link|button|date|color|file|icon|multiselect|object|datetime|slug|email|phone|json|location|tags|rating|code|reference|multi-reference }.
|
|
7361
|
+
'select'/'multiselect' need config.options; 'group'/'table'/'object' need config.fields; a 'button'
|
|
7255
7362
|
is ONE prop on a button block's 'props', never a label prop plus a url prop; 'slot' holds ONE nested component (a list with config.multiple);
|
|
7256
7363
|
'form' holds ONE Form document \u2014 point target.path at 'props' on an empty form block and set
|
|
7257
7364
|
defaultValue to {formId}. Never re-declare a form's FIELDS as props: the Form document owns
|
|
@@ -7710,8 +7817,10 @@ Read bettercms://playbook/schema section 11 first, then work this order:
|
|
|
7710
7817
|
scale, nav position/background, footer surface as DTCG {"$type","$value"} entries) and
|
|
7711
7818
|
redeploy, or structural draft previews render in a generic theme, not this site's design.
|
|
7712
7819
|
5. VERIFY \u2014 call get_next_steps and fix what it lists (it knows about missing manifests,
|
|
7713
|
-
a setup that has not run, and placed-but-unpublished components)
|
|
7714
|
-
|
|
7820
|
+
a setup that has not run, and placed-but-unpublished components). You are finished only
|
|
7821
|
+
when its receipt.complete is true: work receipt.gate.blocking top down, and record an item
|
|
7822
|
+
you cannot close with editor_setup { exempt } under an allowed reason. Telling me why is
|
|
7823
|
+
not an exception.
|
|
7715
7824
|
|
|
7716
7825
|
On 401/403, the MCP key needs (re)authorizing.`
|
|
7717
7826
|
}
|