@bettercms-ai/mcp 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
File without changes
package/SKILL.md CHANGED
@@ -25,7 +25,7 @@ Decide the architecture before the first call. A marketing site is **components-
25
25
  a library of `create_component` sections, each with a `sectionType` and a library
26
26
  category, placed on pages via `create_page`'s `blockJson` as `component` blocks. A blog or
27
27
  catalogue is **schema-first**: a `create_content_model` collection whose body field is
28
- `document` (the article canvas — collections only, one per model, top level).
28
+ `document` (the article canvas — pages or collections, one per schema, top level; cannot coexist with a Section zone).
29
29
 
30
30
  Within a page's own schema, destructure into a tree: plain fields, **group** (one nested
31
31
  object), **repeater** (a repeatable array). Never a 1-item repeater, never a repeater of a
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { BetterCMS } from "@bettercms-ai/sdk";
14
14
  import { z } from "zod";
15
15
 
16
16
  // ../types/src/component.ts
17
- 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 blueprint (list_components returns locked `builtin:*` blueprints 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; for a recurring band, materialize the blueprint with create_component so it becomes project-scoped before implementation validation, Output or publication. Direct `builtin:*` component references exist only for legacy delivery compatibility. 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.";
17
+ 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: inside a component, ONLY the leaves a declared prop TARGETS are click-to-edit on the canvas \u2014 each declared prop's `target` is re-keyed to that PLACEMENT's own override address, so editing it changes this page and not the shared definition. A prop with no `target` still SHOWS as a dock control, but its override reaches no rendered slot, so editing it changes nothing on the page. Copy that no prop points at is worse still: no binding and no control, unreachable from click-to-edit AND from the dock, changeable only by editing the component definition, which rewrites every page that places it. A `section` block's children stay click-to-edit unconditionally. So when you choose a component, DECLARE A PROP \u2014 WITH A `target` \u2014 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 blueprint (list_components returns locked `builtin:*` blueprints 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; for a recurring band, materialize the blueprint with create_component so it becomes project-scoped before implementation validation, Output or publication. Direct `builtin:*` component references exist only for legacy delivery compatibility. 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.";
18
18
 
19
19
  // ../types/src/layout-lucide-icons.ts
20
20
  var LAYOUT_SECTION_ICONS = Object.freeze([
@@ -2089,7 +2089,7 @@ var fieldType = z.enum([
2089
2089
  "modular",
2090
2090
  "location",
2091
2091
  // All LEAF types, so the pass-through branch above covers them. `document` is THE article
2092
- // body — the rich document canvas, collections only, at most one per model, top level
2092
+ // body — the rich document canvas, pages or collections, at most one per schema, top level
2093
2093
  // only. It was the conspicuous omission: the dashboard shipped an editor for it while no
2094
2094
  // agent could create the field. `sections` is a page-section zone composed in the Visual
2095
2095
  // Editor, so it has no inline control.
@@ -2485,6 +2485,10 @@ function buildToolDefs(deps) {
2485
2485
  command: layoutCommand.describe("one canonical discriminated Layout command; authority is derived from type"),
2486
2486
  ifMatch: z.number().int().nonnegative().describe("revision returned by get_layout")
2487
2487
  });
2488
+ const publishLayoutInput = z.object({
2489
+ projectId: z.string().min(1).optional().describe("required only for a workspace-scoped grant"),
2490
+ ifMatch: z.number().int().nonnegative().describe("revision returned by get_layout (scope global)")
2491
+ });
2488
2492
  const componentCategory = z.enum([
2489
2493
  "navbar",
2490
2494
  "footer",
@@ -2833,8 +2837,8 @@ function buildToolDefs(deps) {
2833
2837
  def(
2834
2838
  "update_project",
2835
2839
  "Update the connected project",
2836
- "Update the connected project's settings \u2014 rename it, change its slug/description, SEO defaults, or visibility. Only the provided fields change.",
2837
- z.object({ name: z.string().optional(), slug: z.string().optional(), description: z.string().optional(), visibility: z.string().optional(), seoDefaults: z.record(z.string(), z.unknown()).optional() }).shape,
2840
+ "Update the connected project's settings \u2014 rename it, change its slug/description, SEO defaults, visibility, or `presentation`. Only the provided fields change. `presentation` themes the platform's OWN renderer (the one serving a site built in the CMS with no deployed build, and previewing structural drafts everywhere) with the same closed knobs as bcms-presentation.json \u2014 DTCG { $type, $value } tokens keyed like 'page.maxWidth', 'nav.background'. Without it that renderer is a generic 768px document theme. An invalid manifest is 422 PRESENTATION_INVALID.",
2841
+ z.object({ name: z.string().optional(), slug: z.string().optional(), description: z.string().optional(), visibility: z.string().optional(), seoDefaults: z.record(z.string(), z.unknown()).optional(), presentation: z.record(z.string(), z.unknown()).optional().describe("theme knobs for the platform renderer, keyed like bcms-presentation.json") }).shape,
2838
2842
  async (c, a) => ok("Updated project.", await data(c, "PATCH", `/management/projects/current`, a))
2839
2843
  ),
2840
2844
  def(
@@ -3028,20 +3032,23 @@ function buildToolDefs(deps) {
3028
3032
  def(
3029
3033
  "get_componentize_plan",
3030
3034
  "Get the plan for turning this site's sections into components",
3031
- "What this site's SECTIONS would become as components \u2014 a proposal that creates nothing, changes nothing and is computed live on every call. For a site whose pages were DERIVED at import (the site get_conversion_brief describes), each top-level field GROUP is one section: `hero-*` and `faq-*` keys, and the repeaters the import already folded (`group-*`). Per page it returns each section's `groupKey`, its `sectionType` family (Hero, FAQ, CTA, Features, Social proof\u2026), its leaf `fields` (key, path, type, the value the CMS holds), a `shapeHash`, and either the component that already renders it (`reuse.componentId`) or the one this plan proposes (`reuse.proposedSlug`) \u2014 and the components themselves under `components`: a NEW one carries the exact `props` and `blockJson` create_component would take, while a row for a component that ALREADY EXISTS carries its `componentId`, `slug` and `name` and no definition, because nothing will be written for it. Groups with the SAME shape across pages collapse into ONE component with several placements. `pending` says why a group is not offered: `NO_GROUP_ROOT` (the page's field keys are still the derive lane's own \u2014 `h1-welcome`, `p-we-build-things` \u2014 so there is no family to group by; rename them into families first), `NOT_A_SECTION` (a lone scalar with no family, or the page's own metadata \u2014 a section is a group field, a repeater, or a family two or more leaves share, so a legal page of `title`/`metaDescription`/`intro` proposes nothing), `ALREADY_COMPONENTIZED`, `EMPTY_GROUP`, `NESTED_REPEATER` (a repeater inside a repeater \u2014 one prop cannot describe two levels of rows), `PAGE_NOT_EMPTY` (the page holds blocks this lane does not own and will not overwrite). Chrome is NEVER a section: `nav-`/`footer-` keys and everything promoted into the project Layout are edited through the Layout. Keep the `digest` \u2014 componentize_sections refuses any other.",
3035
+ "What this site's SECTIONS would become as components \u2014 a proposal that creates nothing, changes nothing and is computed live on every call. For a site whose pages were DERIVED at import (the site get_conversion_brief describes), each top-level field GROUP is one section: `hero-*` and `faq-*` keys, and the repeaters the import already folded (`group-*`). Per page it returns each section's `groupKey`, its `sectionType` family (Hero, FAQ, CTA, Features, Social proof\u2026), its leaf `fields` (key, path, type, the value the CMS holds), a `shapeHash`, and either the component that already renders it (`reuse.componentId`) or the one this plan proposes (`reuse.proposedSlug`) \u2014 and the components themselves under `components`: a NEW one carries the exact `props` and `blockJson` create_component would take, while a row for a component that ALREADY EXISTS carries its `componentId`, `slug` and `name` and no definition, because nothing will be written for it. Groups with the SAME shape across pages collapse into ONE component with several placements. `pending` says why a group is not offered: `NO_GROUP_ROOT` (the page's field keys are still the derive lane's own \u2014 `h1-welcome`, `p-we-build-things` \u2014 so there is no family to group by; rename them into families first), `NOT_A_SECTION` (a lone scalar with no family, or the page's own metadata \u2014 a section is a group field, a repeater, or a family two or more leaves share, so a legal page of `title`/`metaDescription`/`intro` proposes nothing), `ALREADY_COMPONENTIZED`, `EMPTY_GROUP`, `NESTED_REPEATER` (a repeater THREE deep; TWO levels are expressed exactly \u2014 the group's `table` prop gains a nested `table` sub-field, and each row's nested column is an array of row objects), `PAGE_NOT_EMPTY` (the page holds blocks this lane does not own and will not overwrite). Chrome is NEVER a section: `nav-`/`footer-` keys and everything promoted into the project Layout are edited through the Layout. Keep the `digest` \u2014 componentize_sections refuses any other.",
3032
3036
  z.object({}).shape,
3033
3037
  async (c) => ok("Componentize plan.", await data(c, "GET", `/management/projects/current/componentize-plan`))
3034
3038
  ),
3035
3039
  def(
3036
3040
  "componentize_sections",
3037
3041
  "Turn this site's derived sections into components",
3038
- "Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections and say how many components it will create and which pages it will rewrite. It creates each proposed component as a DRAFT (its `sectionType` family, category 'section', placeable on any page) and replaces each page's DRAFT blocks with an ordered list of `component` instances \u2014 one per group, each carrying `props.bind: \"<groupKey>\"`, which points at the page field group that already holds the copy. So nothing is copied and nothing moves: the page keeps its `fields`, its values and its bindings, click-to-edit keeps working and the coverage meter does not change. Pass the plan's `digest`; a 409 `stale-plan` means the site changed since you read that plan, so read it again, show the user what changed and confirm again. Call it with `dryRun: true` first \u2014 same receipt, nothing written. Running it twice is safe: a group that already has a placement comes back in `sections.pending` as ALREADY_COMPONENTIZED and no second component is created. DRAFTS ONLY \u2014 an unpublished component renders as an EMPTY STRING on the live site, so publish_component each one and publish the pages before this reaches a visitor. Then run `npx @bettercms-ai/convert --componentize` in the repo so its templates render these sections from `pages[].blocks`.",
3042
+ "Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections and say how many components it will create and which pages it will rewrite. It creates each proposed component as a DRAFT (its `sectionType` family, category 'section', placeable on any page) and replaces each page's DRAFT blocks with an ordered list of `component` instances \u2014 one per group, each carrying `props.bind: \"<groupKey>\"`, which points at the page field group that already holds the copy. So nothing is copied and nothing moves: the page keeps its `fields`, its values and its bindings, click-to-edit keeps working and the coverage meter does not change. Pass the plan's `digest`; a 409 `stale-plan` means the site changed since you read that plan, so read it again, show the user what changed and confirm again. Call it with `dryRun: true` first \u2014 same receipt, nothing written. Running it twice is safe: a group that already has a placement comes back in `sections.pending` as ALREADY_COMPONENTIZED and no second component is created. DRAFTS ONLY \u2014 an unpublished component renders as an EMPTY STRING on the live site, so publish_component each one and publish the pages before this reaches a visitor. Then run `npx @bettercms-ai/convert --componentize` in the repo so its templates render these sections from `pages[].blocks`. `copy` says WHO OWNS THE COPY. The default `bind` is the above: the words stay in the page's field group. `copy: \"instance\"` is the page-builder model \u2014 each placement takes that group's CURRENT draft values into its own `props.overrides` (a repeater becomes one table prop holding every row), records which page path each came from in `props.source`, and drops `props.bind`; the page's fields are KEPT and marked `origin: \"componentized\"`, so nothing is lost and the editor stops showing a second place to type the same words. Run it again and it converts nothing and duplicates nothing. `sections` narrows a run to named groupKeys (`hero`, `group-fast`); a key the plan does not list on the pages you named is refused as `unknown-section` rather than quietly doing nothing.",
3039
3043
  z.object({
3040
3044
  digest: z.string().min(1).describe("The `digest` get_componentize_plan returned. A different one is refused with 409 stale-plan."),
3041
3045
  pageIds: z.array(z.string().min(1)).optional().describe("Componentize only these pages (ids from the plan). Omit for every page the plan lists."),
3046
+ pages: z.array(z.string().min(1)).optional().describe("The same page filter under the name the dashboard uses. Unioned with pageIds; an EMPTY list is refused rather than treated as every page."),
3047
+ sections: z.array(z.string().min(1)).optional().describe("Act only on these section groupKeys (from the plan). Every other section on the page keeps the placement it already has."),
3048
+ copy: z.enum(["bind", "instance"]).optional().describe("Who owns each section's copy: 'bind' (default) leaves it in the page fields; 'instance' moves it onto the placement and marks those fields origin: componentized."),
3042
3049
  dryRun: z.boolean().optional().describe("true = return the receipt without writing anything. Do this first.")
3043
3050
  }).shape,
3044
- async (c, a) => ok("Componentized the sections.", await data(c, "POST", `/management/projects/current/componentize`, { digest: a.digest, pageIds: a.pageIds, dryRun: a.dryRun }))
3051
+ async (c, a) => ok("Componentized the sections.", await data(c, "POST", `/management/projects/current/componentize`, { digest: a.digest, pageIds: a.pageIds, pages: a.pages, sections: a.sections, copy: a.copy, dryRun: a.dryRun }))
3045
3052
  ),
3046
3053
  def(
3047
3054
  "get_analytics_overview",
@@ -3562,7 +3569,7 @@ function buildToolDefs(deps) {
3562
3569
  })
3563
3570
  )
3564
3571
  },
3565
- // ── Project/page Layout (draft-only; publish remains dashboard-gated) ──
3572
+ // ── Project/page Layout (draft commands + Global Layout publish; page overrides publish with the page) ──
3566
3573
  {
3567
3574
  name: "get_layout",
3568
3575
  config: {
@@ -3587,6 +3594,18 @@ function buildToolDefs(deps) {
3587
3594
  return ok(`Updated ${layout.scope === "global" ? "Global" : `Page ${layout.pageSlug}`} Layout draft to revision ${layout.revision}.`, layout);
3588
3595
  }))
3589
3596
  },
3597
+ {
3598
+ name: "publish_layout",
3599
+ config: {
3600
+ title: "Publish the Global Layout draft",
3601
+ description: "Publish the connected project's GLOBAL Layout draft (navigation, footer, every reserved section) so the live site builds from it. Until this is called the layout stays draft and get_layout copy:'published' answers PUBLISHED_LAYOUT_UNAVAILABLE \u2014 site chrome authored with update_layout is NOT live. Read get_layout first and pass its revision as ifMatch; a stale revision returns 409 \u2014 re-read, never retry blindly. A 422 lists validation issues to fix with update_layout first. Verify with get_layout copy:'published' and check the copy echo \u2014 this tool's own response is the write's echo, not a receipt. Page overrides go live with the page (update_page status:'published'). A 403 PUBLISH_NOT_GRANTED means this connection can author drafts but cannot publish \u2014 say so and let the user allow publishing or publish from the dashboard.",
3602
+ inputSchema: publishLayoutInput.shape
3603
+ },
3604
+ handler: guard(async (args) => withClient(async (client) => {
3605
+ const layout = await client.publishManagedLayout(args);
3606
+ return ok(`Published the Global Layout at revision ${layout.revision} (copy: published). Verify with get_layout copy:'published'.`, layout);
3607
+ }))
3608
+ },
3590
3609
  // ── Components (discover + author reusable symbols) ──
3591
3610
  {
3592
3611
  name: "list_components",
@@ -3785,9 +3804,11 @@ Decide before your first call; converting later means rewriting content.
3785
3804
 
3786
3805
  ## 3. Anatomy of a real section component
3787
3806
 
3788
- A component that is only headings and text renders as an unstyled stack. Every section the
3789
- product ships looks like this \u2014 a \`section\` block carrying \`style\`, with content nested in
3790
- \`props.children\`:
3807
+ A component that is only headings and text renders as an unstyled stack \u2014 plain text on white
3808
+ in a 768px column, on the live site AND in the visual editor. Every section the product ships
3809
+ looks like this \u2014 a \`section\` block carrying \`style\`, with content nested in
3810
+ \`props.children\`. \`create_component\` and \`update_component\` return a \`warnings\` entry when a
3811
+ Section's root section has no \`style\`; treat it as a defect, not a note:
3791
3812
 
3792
3813
  {
3793
3814
  type: "section", id: "root",
@@ -3836,10 +3857,14 @@ with every string, link and image already declared as a prop. Show the user the
3836
3857
  **confirm before writing**, then \`componentize_sections { digest, dryRun: true }\` and, once the
3837
3858
  receipt reads right, without \`dryRun\`. Each page's blocks become ordered \`component\`
3838
3859
  instances carrying \`props.bind\` \u2014 the field group keeps the copy, so nothing moves and
3839
- click-to-edit keeps working. Then run \`npx @bettercms-ai/convert --componentize\` in the repo
3860
+ click-to-edit keeps working. Pass \`copy: "instance"\` instead when the INSTANCE should own its
3861
+ words (the page-builder model): each placement takes that group's current values into
3862
+ \`props.overrides\`, records where each came from in \`props.source\`, and the page's fields are
3863
+ kept and marked \`origin: "componentized"\` so the editor stops offering a second place to type
3864
+ them. Either way, running it again converts nothing and duplicates nothing. Then run \`npx @bettercms-ai/convert --componentize\` in the repo
3840
3865
  so its templates render those sections from \`pages[].blocks\`, and finish with the publishes:
3841
- \`publish_component\` every new component and publish the pages, or the site renders the
3842
- sections as empty strings.
3866
+ \`publish_component\` every new component, \`publish_layout\` if you touched site chrome (nav,
3867
+ footer), and publish the pages, or the site renders the sections as empty strings.
3843
3868
 
3844
3869
  ## 5. Blocks and modular fields
3845
3870
 
@@ -3848,15 +3873,39 @@ model's \`modular\` field. Create the blocks FIRST, then name their slugs (not i
3848
3873
  \`config.blockSlugs\`. This is the page-builder shape: one \`sections\` field holding an
3849
3874
  ordered list of typed blocks.
3850
3875
 
3876
+ ## 5b. NEVER MODEL SEO. It is native.
3877
+
3878
+ Do not create \`seoTitle\`, \`metaTitle\`, \`metaDescription\`, \`ogImage\`, \`canonicalUrl\`,
3879
+ \`noindex\` or an \`seo\` group on ANY model, page or component. Every one of those already
3880
+ exists on the platform:
3881
+
3882
+ per page / entry metaTitle, metaDescription \u2014 set with update_page / update_content_entry
3883
+ site-wide defaults get_seo / update_seo \u2014 { metaTitle, metaDescription, ogImage, twitterHandle }
3884
+ generated for you generate_seo_meta
3885
+ audited for you list_seo_issues
3886
+
3887
+ A modelled copy does not add a capability, it adds a SECOND place the same fact lives \u2014 and the
3888
+ two immediately disagree. The native fields are what the renderer emits into \`<head>\`, what
3889
+ sitemap and llms.txt read, and what \`list_seo_issues\` scans; a field called \`seoTitle\` on a
3890
+ model is a string an author edits that changes nothing on the page. It is worse than useless:
3891
+ it looks like the control and is not.
3892
+
3893
+ If a page needs its own meta, set it on the page. If the whole site needs a default, set it with
3894
+ update_seo. If you want it written for you, call generate_seo_meta and apply the result.
3895
+
3896
+ The one legitimate exception is a field that is genuinely CONTENT and happens to be used in meta
3897
+ too \u2014 a blog post's \`excerpt\`, an article's \`coverImage\`. Model those as content, name them
3898
+ for what they are, and let \`generate_seo_meta\` read them.
3899
+
3851
3900
  ## 6. Blogs
3852
3901
 
3853
3902
  author collection: name, avatar, bio
3854
3903
  blog-post collection: title, slug, excerpt, cover (image),
3855
3904
  body (document), author (reference -> author)
3856
3905
 
3857
- \`document\` is THE article body: a rich document canvas, **collections only**, at most one
3858
- per model, top level only (never inside a group or repeater). A page's body is its block
3859
- content, so \`document\` on a page is rejected. Use \`richtext\` for a short formatted field,
3906
+ \`document\` is THE article body: a rich document canvas on pages or collections, at most one
3907
+ per schema, top level only (never inside a group or repeater). It cannot coexist with a
3908
+ Section zone, which also owns the document canvas. Use \`richtext\` for a short formatted field,
3860
3909
  \`longtext\` for multi-line plain text.
3861
3910
 
3862
3911
  Then the pages:
@@ -3880,6 +3929,7 @@ is interpolated per entry with \`{{fieldKey}}\` placeholders \u2014 \`{{title}}\
3880
3929
  5. pages create_page with blockJson placing those components
3881
3930
  6. content create_content_entry / set_page_content
3882
3931
  7. publish update_page status:'published'
3932
+ 7b. layout publish_layout <- if you authored the Global Layout (nav/footer); page overrides publish with the page
3883
3933
  8. check get_next_steps, and fix what it lists
3884
3934
 
3885
3935
  ## 8. Two ways to ship a blank site
@@ -3899,8 +3949,11 @@ in preview and is blank in production. Always pass the project's id.
3899
3949
  accepts it and ignores it**, silently. No error, no warning, wrong project.
3900
3950
 
3901
3951
  So call \`get_project\` (no arguments) first \u2014 it reports the project you are actually
3902
- writing to. If that is not where the work belongs, stop and tell the user: only they can
3903
- approve a grant for the other project, no tool can switch it.
3952
+ writing to. If that is not where the work belongs \u2014 or it answers that the grant covers the
3953
+ whole workspace \u2014 stop and tell the user the one-step fix: in the BetterCMS dashboard open
3954
+ **Settings \u2192 Connected AI clients** and use **Scope to project** on this connection. No tool
3955
+ can switch it, but the user does NOT need to re-authorize or re-add the server: the next call
3956
+ after that runs with the new scope. Then retry.
3904
3957
 
3905
3958
  If you must probe, probe with a \`create_content_model\` \u2014 models are deletable
3906
3959
  (\`delete_content_model\`, soft-delete) and **there is no \`delete_component\`**. A component
@@ -3926,6 +3979,7 @@ in this order:
3926
3979
 
3927
3980
  1. create_component per section (they land as DRAFTS)
3928
3981
  2. publish_component each one \u2014 unpublished renders as NOTHING, on a page that 200s
3982
+ 2b. publish_layout if you authored site chrome \u2014 the layout is a separate publish
3929
3983
  3. set_page_content place them as \`component\` blocks on the page
3930
3984
  4. list_extraction_candidates / extract_component
3931
3985
  now that blocks exist, fold any section repeated 3+ times
@@ -3952,6 +4006,13 @@ type scale, nav position and background, footer surface \u2014 as DTCG \`{"$type
3952
4006
  entries. Redeclare it on every deploy; absent means "declared nothing" and previews fall back
3953
4007
  to platform defaults that will not look like this site.
3954
4008
 
4009
+ **A site built in the CMS with NO deployed build has only that renderer** \u2014 live and in the
4010
+ editor \u2014 so the generic theme IS the site until you theme it. Theme it without a deploy:
4011
+ \`update_project { presentation: { "page.maxWidth": { "$type": "dimension", "$value": 1200 },
4012
+ "nav.background": { "$type": "color", "$value": "#0b0b0b" }, ... } }\` takes the same knobs as
4013
+ bcms-presentation.json, and set \`style\` on every Section's root (\xA73). A later deploy that
4014
+ ships bcms-presentation.json replaces it wholesale.
4015
+
3955
4016
  **Editing binds by VALUE.** The editor matches CMS field values against the text the site
3956
4017
  renders. Three consequences, each load-bearing:
3957
4018
 
@@ -3964,6 +4025,7 @@ renders. Three consequences, each load-bearing:
3964
4025
  c. set_page_content values EXACTLY equal to the text the site renders \u2014
3965
4026
  binding matches by value, so a paraphrase binds nothing
3966
4027
  d. update_page status 'published' \u2014 the canvas binds the PUBLISHED copy
4028
+ e. publish_layout if site chrome was authored \u2014 the canvas binds the PUBLISHED layout too
3967
4029
  2. A value that renders in more than one place is still ONE field: bind every element that
3968
4030
  renders it, and the editor keeps them in sync \u2014 an edit patches every copy at once.
3969
4031
  Binding only one copy leaves the others showing the old text until the next rebuild.
@@ -4019,7 +4081,8 @@ green on the wrong document. Three rules, none optional:
4019
4081
  the draft; a PUBLISH claim verifies ONLY via \`get_layout copy:'published'\` (or the
4020
4082
  entry/page's published copy) \u2014 and check the response's \`copy\` echo says
4021
4083
  'published'. A reader that ignores your copy selector hands you the draft and a
4022
- false green; the echo is how you catch it.
4084
+ false green; the echo is how you catch it. \`publish_layout\`'s own response echoes
4085
+ copy:'published' too \u2014 that is the WRITE's echo, not a receipt; still re-read.
4023
4086
  2. Publish and deploy are SEPARATE claims. "Published" means the published copy changed;
4024
4087
  the LIVE SITE changes only after its next deploy/rebuild. Never report "it's live"
4025
4088
  from a publish receipt \u2014 fetch the live URL (cache-busted) for that claim.
@@ -4256,9 +4319,10 @@ error-prone, so go slow and confirm. Never guess the layout.
4256
4319
  6. **\`publish_component\`.** It lands as a DRAFT, and a draft component renders as NOTHING on
4257
4320
  the live site \u2014 no error, no placeholder. This step is not optional.
4258
4321
  `;
4259
- var LAYOUT_FLOW = `### Project/page Layout authoring \u2192 \`get_layout\` / \`update_layout\`
4260
- Edit the project's draft Global Layout or one page's draft override. Layout publishing stays
4261
- in the dashboard; these tools never change the live site.
4322
+ var LAYOUT_FLOW = `### Project/page Layout authoring \u2192 \`get_layout\` / \`update_layout\` / \`publish_layout\`
4323
+ Edit the project's draft Global Layout or one page's draft override. \`update_layout\` never
4324
+ changes the live site; \`publish_layout\` takes the Global Layout draft live (page overrides go
4325
+ live with their page). Until it is called, site chrome you authored is NOT on the live site.
4262
4326
  1. **Read first** \u2014 call \`get_layout\` with scope \`global\`, or scope \`page\` + pageId.
4263
4327
  Keep the returned \`revision\`; every write must pass it as \`ifMatch\`.
4264
4328
  2. **Choose one discriminated command type** \u2014 the server derives its authority family;