@bettercms-ai/mcp 0.30.0 → 0.31.2

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 CHANGED
@@ -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(
@@ -2961,7 +2965,7 @@ function buildToolDefs(deps) {
2961
2965
  def(
2962
2966
  "deploy_project",
2963
2967
  "Deploy new source/build",
2964
- "Deploy new source/build for the connected project and make it live at its <handle>.bettercms.site. Pass a .tgz or .zip of the project as a base64 string in `data`: SOURCE (has package.json) is built server-side in an isolated sandbox; a prebuilt static site is served as-is. Returns the release id + sha \u2014 then poll get_deploy_status until it is live. IMPORTANT \u2014 you MUST exclude node_modules, .git, and build output/caches (dist, build, .next, .astro, .cache) BEFORE creating the archive: a source deploy is reinstalled and built server-side, so those are never needed, and the upload has a hard size ceiling (~100 MB) enforced before the request reaches the server \u2014 an archive that includes node_modules is rejected in transit (a 413/502 with no server-side detail). Keep the archive to your own source files. Server-side stripping exists as a safety net, but it runs AFTER the upload and cannot rescue an over-limit body. For a LARGE archive (or if this returns a 413/502), use create_deploy_upload + deploy_from_upload instead \u2014 that path uploads straight to storage with no size ceiling.",
2968
+ "Deploy new source/build for the connected project and make it live at its <handle>.bettercms.site. Pass a .tgz or .zip of the project as a base64 string in `data`: SOURCE (has package.json) is built server-side in an isolated sandbox; a prebuilt static site is served as-is. Returns the release id + sha \u2014 then poll get_deploy_status until it is live. IMPORTANT \u2014 you MUST exclude node_modules, .git, and build output/caches (dist, build, .next, .astro, .cache) BEFORE creating the archive: a source deploy is reinstalled and built server-side, so those are never needed, and the upload has a hard size ceiling (~100 MB) enforced before the request reaches the server \u2014 an archive that includes node_modules is rejected in transit (a 413/502 with no server-side detail). Keep the archive to your own source files. Server-side stripping exists as a safety net, but it runs AFTER the upload and cannot rescue an over-limit body. For a LARGE archive (or if this returns a 413/502), use create_deploy_upload + deploy_from_upload instead \u2014 that path uploads straight to storage with no size ceiling. Returns `canvas.lane` \u2014 which live-preview lane the last release gave the editor (`bridge` / `draft-route` / `none`; see the playbook's section 11) \u2014 and `editorUrl`, the visual editor to hand the user.",
2965
2969
  z.object({ data: z.string().min(1).describe("base64 .tgz/.zip of the project"), mimeType: z.string().optional() }).shape,
2966
2970
  async (c, a) => ok("Deploy queued.", await raw(c, `/management/projects/deploy`, s(a.data), a.mimeType))
2967
2971
  ),
@@ -3004,7 +3008,7 @@ function buildToolDefs(deps) {
3004
3008
  def(
3005
3009
  "get_binding_report",
3006
3010
  "Check what on the live site is editable",
3007
- "The receipt for 'is this site actually EDITABLE?'. Every release scans the built HTML for the element that renders each CMS field value; this returns what that scan found, per slot: `mode` ('text-match' = bindings guessed from rendered text, 'declared' = the template declares them), `pagesInspected`, `bound` (elements carrying a binding), and `unmatched` \u2014 per page, each path with its kind and the reason it failed (not-declared / ambiguous-text / no-element). DEPLOY FIRST: before any release there is no report and this answers pages 0, mode null, refreshRequired true. It certifies exactly one thing \u2014 that every non-empty field of every page has SOME element carrying its path. It cannot see copy that was never modelled, so diff each route's visible text against its entry values yourself before calling a page done. Pass `slot` ('current' or 'staging') to read the other tree; the default is the slot this project's releases land in.",
3011
+ "The receipt for 'is this site actually EDITABLE?'. Every release scans the built HTML for the element that renders each CMS field value; this returns what that scan found, per slot: `mode` ('text-match' = bindings guessed from rendered text, 'declared' = the template declares them), `pagesInspected`, `bound` (elements carrying a binding), and `unmatched` \u2014 per page, each path with its kind and the reason it failed (not-declared / ambiguous-text / no-element). DEPLOY FIRST: before any release there is no report and this answers pages 0, mode null, refreshRequired true. It certifies exactly one thing \u2014 that every non-empty field of every page has SOME element carrying its path. It cannot see copy that was never modelled, so diff each route's visible text against its entry values yourself before calling a page done. `builtRoutes` lists the routes the live build has HTML for (null when unknown: runtime release, >500 routes, or a report older than this field). `canvas.lane` names the live-preview lane this build gives the editor \u2014 `bridge` (the build ships BcmsDraftBridge), `draft-route` (a node runtime rendering drafts server-side) or `none`, in which case get_next_steps carries the recipe (playbook section 11); null means the report predates the field. `unaddressable` counts visible text on the live site that no field owns, as the editor's x-ray measured it against the sha now serving \u2014 the one thing `unmatched` structurally cannot see; absent means nobody has turned that control on for this build. `unaddressable` is measured PER SLOT by the editor, which frames `current`; on a promote-gated project pass slot:'current' to read it. Pass `slot` ('current' or 'staging') to read the other tree; the default is the slot this project's releases land in.",
3008
3012
  z.object({ slot: z.enum(["current", "staging"]).optional().describe("which release tree to read; defaults to the one this project deploys to") }).shape,
3009
3013
  async (c, a) => ok("Binding report.", await data(c, "GET", `/management/projects/current/binding-report${q({ slot: a.slot })}`))
3010
3014
  ),
@@ -3214,7 +3218,7 @@ function buildToolDefs(deps) {
3214
3218
  def(
3215
3219
  "get_deploy_status",
3216
3220
  "Get deploy/build status",
3217
- "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. Poll this after deploy_project until state is idle with your sha live.",
3221
+ "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. Poll this after deploy_project until state is idle with your sha live. Also returns `canvas.lane` (the editor's live-preview lane for this site: `bridge` / `draft-route` / `none`) and `editorUrl`, the visual editor to hand the user.",
3218
3222
  z.object({}).shape,
3219
3223
  async (c) => ok("Deploy status.", await data(c, "GET", `/management/projects/deploy-status`))
3220
3224
  )
@@ -3565,7 +3569,7 @@ function buildToolDefs(deps) {
3565
3569
  })
3566
3570
  )
3567
3571
  },
3568
- // ── Project/page Layout (draft-only; publish remains dashboard-gated) ──
3572
+ // ── Project/page Layout (draft commands + Global Layout publish; page overrides publish with the page) ──
3569
3573
  {
3570
3574
  name: "get_layout",
3571
3575
  config: {
@@ -3590,6 +3594,18 @@ function buildToolDefs(deps) {
3590
3594
  return ok(`Updated ${layout.scope === "global" ? "Global" : `Page ${layout.pageSlug}`} Layout draft to revision ${layout.revision}.`, layout);
3591
3595
  }))
3592
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
+ },
3593
3609
  // ── Components (discover + author reusable symbols) ──
3594
3610
  {
3595
3611
  name: "list_components",
@@ -3788,9 +3804,11 @@ Decide before your first call; converting later means rewriting content.
3788
3804
 
3789
3805
  ## 3. Anatomy of a real section component
3790
3806
 
3791
- A component that is only headings and text renders as an unstyled stack. Every section the
3792
- product ships looks like this \u2014 a \`section\` block carrying \`style\`, with content nested in
3793
- \`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:
3794
3812
 
3795
3813
  {
3796
3814
  type: "section", id: "root",
@@ -3845,8 +3863,8 @@ words (the page-builder model): each placement takes that group's current values
3845
3863
  kept and marked \`origin: "componentized"\` so the editor stops offering a second place to type
3846
3864
  them. Either way, running it again converts nothing and duplicates nothing. Then run \`npx @bettercms-ai/convert --componentize\` in the repo
3847
3865
  so its templates render those sections from \`pages[].blocks\`, and finish with the publishes:
3848
- \`publish_component\` every new component and publish the pages, or the site renders the
3849
- 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.
3850
3868
 
3851
3869
  ## 5. Blocks and modular fields
3852
3870
 
@@ -3911,6 +3929,7 @@ is interpolated per entry with \`{{fieldKey}}\` placeholders \u2014 \`{{title}}\
3911
3929
  5. pages create_page with blockJson placing those components
3912
3930
  6. content create_content_entry / set_page_content
3913
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
3914
3933
  8. check get_next_steps, and fix what it lists
3915
3934
 
3916
3935
  ## 8. Two ways to ship a blank site
@@ -3930,8 +3949,11 @@ in preview and is blank in production. Always pass the project's id.
3930
3949
  accepts it and ignores it**, silently. No error, no warning, wrong project.
3931
3950
 
3932
3951
  So call \`get_project\` (no arguments) first \u2014 it reports the project you are actually
3933
- writing to. If that is not where the work belongs, stop and tell the user: only they can
3934
- 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.
3935
3957
 
3936
3958
  If you must probe, probe with a \`create_content_model\` \u2014 models are deletable
3937
3959
  (\`delete_content_model\`, soft-delete) and **there is no \`delete_component\`**. A component
@@ -3957,10 +3979,15 @@ in this order:
3957
3979
 
3958
3980
  1. create_component per section (they land as DRAFTS)
3959
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
3960
3983
  3. set_page_content place them as \`component\` blocks on the page
3961
3984
  4. list_extraction_candidates / extract_component
3962
3985
  now that blocks exist, fold any section repeated 3+ times
3963
3986
 
3987
+ Step 2 is not a formality: \`componentize_sections\` and \`create_component\` both land components
3988
+ as DRAFTS, so \`publish_component\` each one and publish the page \u2014 otherwise the canvas keeps
3989
+ painting the PUBLISHED copy and the editor reports N components unpublished.
3990
+
3964
3991
  **Answering \`fields\` is a real answer, not a deferral.** A blog, a catalogue or a directory
3965
3992
  is schema-first by design (\xA71) and should stay that way. Say so and move on.
3966
3993
 
@@ -3973,16 +4000,45 @@ the user rather than describing the choice in the abstract.
3973
4000
  A deploy makes a site LIVE. It does not make it editable \u2014 those are different states, and the
3974
4001
  gap between them is the single most common disappointment after an import.
3975
4002
 
3976
- **The canvas is the real site when it can be.** When a page's draft matches its published copy
3977
- structurally, the visual editor frames the project's OWN deployed build and paints unpublished
3978
- text over it. Structural drafts (new sections, unpublished pages, changed components) render on
3979
- the platform's own renderer instead \u2014 and that renderer previews in a GENERIC theme unless the
3980
- deploy artifact declares \`bcms-presentation.json\` at its root. Put the file in \`public/\`
4003
+ **The canvas is the real site.** A PUBLISHED page of a DEPLOYED site with DECLARED bindings
4004
+ frames the project's OWN build, with unpublished text painted over it, whether or not the draft
4005
+ has drifted from the published copy. STRUCTURAL drafts (new
4006
+ sections, reordering, a changed component definition) are the one thing that build cannot show,
4007
+ and there are four ways to see them: publish and redeploy; the editor's **Draft render** toggle,
4008
+ which swaps in the platform renderer on demand (approximate theme, one click back); live, through
4009
+ the draft bridge below (Next); or \u2014 for server-rendered sites \u2014 the node lane, coming.
4010
+
4011
+ Only FOUR states use the platform renderer by default: an UNPUBLISHED page, a page whose route
4012
+ the last build does not carry (\`get_binding_report.builtRoutes\`), a project with NO build at
4013
+ all, and a \`mode: "text-match"\` site whose draft has drifted (its elements are found by matching
4014
+ page text to stored values, so drifted text may be unclickable \u2014 declare the bindings, \xA713).
4015
+ Those are exactly where \`bcms-presentation.json\` matters \u2014 the renderer previews in a
4016
+ GENERIC theme unless the deploy artifact declares one at its root. Put the file in \`public/\`
3981
4017
  (the build lands it at the artifact root) declaring the site's presentation \u2014 container width,
3982
4018
  type scale, nav position and background, footer surface \u2014 as DTCG \`{"$type": ..., "$value": ...}\`
3983
4019
  entries. Redeclare it on every deploy; absent means "declared nothing" and previews fall back
3984
4020
  to platform defaults that will not look like this site.
3985
4021
 
4022
+ **Which live-preview lane THIS site has, and what it costs not to have one.**
4023
+ \`get_binding_report.canvas.lane\` names it: \`bridge\` (the build ships \`BcmsDraftBridge\` from
4024
+ \`@bettercms-ai/next/draft-bridge\`, so a structural draft re-renders inside the site's own
4025
+ build), \`draft-route\` (a node runtime whose app renders drafts server-side), or \`none\` \u2014 no
4026
+ lane, so every structural draft falls back to the platform renderer and its generic theme.
4027
+ \`null\` means the report predates the field; redeploy to learn. When it is \`none\`,
4028
+ \`get_next_steps\` names the recipe for this project's framework (\`canvas-lane-missing\`), and
4029
+ \xA713 step 5c has both. The same report carries \`unaddressable\`: how much VISIBLE text on the
4030
+ live site no field owns, counted against the sha now serving when an author turns the visual
4031
+ editor's **"Unbound text"** control on \u2014 \`unmatched\` cannot see it, because it only ever speaks
4032
+ about fields that already exist. Absent means nobody has turned that control on for this build,
4033
+ not that the site is clean.
4034
+
4035
+ **A site built in the CMS with NO deployed build has only that renderer** \u2014 live and in the
4036
+ editor \u2014 so the generic theme IS the site until you theme it. Theme it without a deploy:
4037
+ \`update_project { presentation: { "page.maxWidth": { "$type": "dimension", "$value": 1200 },
4038
+ "nav.background": { "$type": "color", "$value": "#0b0b0b" }, ... } }\` takes the same knobs as
4039
+ bcms-presentation.json, and set \`style\` on every Section's root (\xA73). A later deploy that
4040
+ ships bcms-presentation.json replaces it wholesale.
4041
+
3986
4042
  **Editing binds by VALUE.** The editor matches CMS field values against the text the site
3987
4043
  renders. Three consequences, each load-bearing:
3988
4044
 
@@ -3995,6 +4051,7 @@ renders. Three consequences, each load-bearing:
3995
4051
  c. set_page_content values EXACTLY equal to the text the site renders \u2014
3996
4052
  binding matches by value, so a paraphrase binds nothing
3997
4053
  d. update_page status 'published' \u2014 the canvas binds the PUBLISHED copy
4054
+ e. publish_layout if site chrome was authored \u2014 the canvas binds the PUBLISHED layout too
3998
4055
  2. A value that renders in more than one place is still ONE field: bind every element that
3999
4056
  renders it, and the editor keeps them in sync \u2014 an edit patches every copy at once.
4000
4057
  Binding only one copy leaves the others showing the old text until the next rebuild.
@@ -4028,13 +4085,14 @@ renders. Three consequences, each load-bearing:
4028
4085
  content. Div-built chrome outside \`<main>\` is still excluded; div-built chrome with no
4029
4086
  \`<main>\` anywhere loses that protection.
4030
4087
 
4031
- **Structural drafts can render on the real site too \u2014 the draft bridge.** Wrap your page's
4088
+ **Structural drafts can render on the real site too, LIVE \u2014 the draft bridge.** Wrap your page's
4032
4089
  blocks in \`BcmsDraftBridge\` (\`@bettercms-ai/next/draft-bridge\`) instead of calling
4033
4090
  \`BcmsBlocks\` directly: standalone it renders identically, and inside the visual editor it
4034
4091
  receives the DRAFT block tree over a same-origin postMessage handshake and re-renders it with
4035
4092
  the site's own components \u2014 so adding, removing or reordering sections previews in the site's
4036
- real design instead of the platform's approximate renderer. Sites without the bridge keep the
4037
- approximate fallback; unpublished ROUTES always fall back (a static build has no file to frame).
4093
+ real design, with no publish and no Draft render detour. A bridge that answers wins over the
4094
+ toggle. Sites without one keep Draft render's approximate fallback, and a route the build does
4095
+ not carry falls back always (a static build has no file to frame).
4038
4096
 
4039
4097
  **Hosting decides whether a canvas exists at all.** A site deployed here is framed through a
4040
4098
  same-origin proxy \u2014 that is what the canvas requires. A site hosted elsewhere (your own Vercel,
@@ -4050,7 +4108,8 @@ green on the wrong document. Three rules, none optional:
4050
4108
  the draft; a PUBLISH claim verifies ONLY via \`get_layout copy:'published'\` (or the
4051
4109
  entry/page's published copy) \u2014 and check the response's \`copy\` echo says
4052
4110
  'published'. A reader that ignores your copy selector hands you the draft and a
4053
- false green; the echo is how you catch it.
4111
+ false green; the echo is how you catch it. \`publish_layout\`'s own response echoes
4112
+ copy:'published' too \u2014 that is the WRITE's echo, not a receipt; still re-read.
4054
4113
  2. Publish and deploy are SEPARATE claims. "Published" means the published copy changed;
4055
4114
  the LIVE SITE changes only after its next deploy/rebuild. Never report "it's live"
4056
4115
  from a publish receipt \u2014 fetch the live URL (cache-busted) for that claim.
@@ -4194,6 +4253,17 @@ conversion is yours to write.
4194
4253
  and no network content step, by design. So the archive MUST SHIP its own
4195
4254
  \`bcms-content.json\` \u2014 generate it locally with a delivery key and commit it. Without
4196
4255
  one the site renders its fallbacks and the report says \`no-element\` for every path.
4256
+ 5c. **Live preview lane.** Static HTML makes a page click-to-edit; it cannot show a STRUCTURAL
4257
+ draft \u2014 a new section, a reorder, a changed component \u2014 so those fall back to the platform
4258
+ renderer's generic theme unless the build can render drafts itself. Give it a lane:
4259
+ - NEXT: wrap the page's \`BcmsBlocks\` in \`BcmsDraftBridge\` from
4260
+ \`@bettercms-ai/next/draft-bridge\` and redeploy. The bridge marks its output
4261
+ \`data-bcms-canvas="bridge"\`, which is what the release scan records.
4262
+ - ASTRO: switch the pages to \`output: 'server'\` with the node adapter, mount
4263
+ \`@bettercms-ai/astro\`'s draft routes under \`src/pages/api/bcms/draft/\`, set
4264
+ \`BCMS_DRAFT_SECRET\`, redeploy, then switch the project to "Run as a server app" on the
4265
+ Hosting page. Never migrate a static site to SSR without asking the human first.
4266
+ Then \`get_binding_report.canvas.lane\` reads \`bridge\` or \`draft-route\` instead of \`none\`.
4197
4267
  6. Push, or \`deploy_project\`; poll \`get_deploy_status\` until it is live. Then
4198
4268
  \`get_binding_report\` \u2014 still \`text-match\`, and \`unmatched\` should be EMPTY because the
4199
4269
  values are byte-equal to what the build renders. On a converted site read \`coverage\` too: it
@@ -4287,9 +4357,10 @@ error-prone, so go slow and confirm. Never guess the layout.
4287
4357
  6. **\`publish_component\`.** It lands as a DRAFT, and a draft component renders as NOTHING on
4288
4358
  the live site \u2014 no error, no placeholder. This step is not optional.
4289
4359
  `;
4290
- var LAYOUT_FLOW = `### Project/page Layout authoring \u2192 \`get_layout\` / \`update_layout\`
4291
- Edit the project's draft Global Layout or one page's draft override. Layout publishing stays
4292
- in the dashboard; these tools never change the live site.
4360
+ var LAYOUT_FLOW = `### Project/page Layout authoring \u2192 \`get_layout\` / \`update_layout\` / \`publish_layout\`
4361
+ Edit the project's draft Global Layout or one page's draft override. \`update_layout\` never
4362
+ changes the live site; \`publish_layout\` takes the Global Layout draft live (page overrides go
4363
+ live with their page). Until it is called, site chrome you authored is NOT on the live site.
4293
4364
  1. **Read first** \u2014 call \`get_layout\` with scope \`global\`, or scope \`page\` + pageId.
4294
4365
  Keep the returned \`revision\`; every write must pass it as \`ifMatch\`.
4295
4366
  2. **Choose one discriminated command type** \u2014 the server derives its authority family;