@bettercms-ai/mcp 0.83.0 → 0.83.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/README.md +86 -86
- package/SKILL.md +160 -160
- package/dist/index.js +38 -9
- package/dist/index.js.map +1 -1
- package/dist/kit-catalog.json +1509 -1509
- package/dist/skills/bettercms/SKILL.md +206 -206
- package/dist/skills/bettercms/references/accessibility.md +171 -171
- package/dist/skills/bettercms/references/animation.md +296 -296
- package/dist/skills/bettercms/references/astro.md +148 -148
- package/dist/skills/bettercms/references/bindings.md +366 -366
- package/dist/skills/bettercms/references/componentize.md +185 -185
- package/dist/skills/bettercms/references/components.md +415 -415
- package/dist/skills/bettercms/references/content-experiments.md +62 -62
- package/dist/skills/bettercms/references/content-modeling.md +382 -382
- package/dist/skills/bettercms/references/detection.md +465 -465
- package/dist/skills/bettercms/references/dock-input.md +121 -121
- package/dist/skills/bettercms/references/editor-chrome.md +92 -92
- package/dist/skills/bettercms/references/external-content.md +78 -78
- package/dist/skills/bettercms/references/finish-site.md +377 -377
- package/dist/skills/bettercms/references/forms-and-interactions.md +245 -245
- package/dist/skills/bettercms/references/frameworks.md +159 -159
- package/dist/skills/bettercms/references/getting-started.md +156 -156
- package/dist/skills/bettercms/references/integrations.md +364 -364
- package/dist/skills/bettercms/references/kit-catalog.md +3259 -3259
- package/dist/skills/bettercms/references/kit-scenarios.md +712 -712
- package/dist/skills/bettercms/references/localization.md +85 -85
- package/dist/skills/bettercms/references/media-and-brand.md +181 -181
- package/dist/skills/bettercms/references/next.md +189 -189
- package/dist/skills/bettercms/references/other-frameworks.md +85 -85
- package/dist/skills/bettercms/references/portable-text.md +213 -213
- package/dist/skills/bettercms/references/programmatic-seo.md +98 -98
- package/dist/skills/bettercms/references/publishing.md +289 -289
- package/dist/skills/bettercms/references/seo.md +357 -357
- package/dist/skills/bettercms/references/shopify-headless.md +52 -52
- package/dist/skills/bettercms/references/site-blueprints.md +895 -895
- package/dist/skills/bettercms/references/site-review.md +74 -74
- package/dist/skills/bettercms/references/worked-examples-advanced.md +509 -509
- package/dist/skills/bettercms/references/worked-examples.md +885 -885
- package/dist/skills/bettercms/references/write-limits.md +217 -217
- package/package.json +1 -1
|
@@ -1,289 +1,289 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "Publishing and sync"
|
|
3
|
-
description: "Publish order, delivery lanes, execute and verify, rollback, reconciling a re-import or continuous sync, and the automation surfaces that exist."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Publishing, release, rollback and sync reconciliation
|
|
7
|
-
|
|
8
|
-
**Outcome.** The intended revision reaches the right public site with its dependencies, routes,
|
|
9
|
-
forms and cache state checked, and a new source revision lands without overwriting editorial
|
|
10
|
-
work. A dashboard success is not evidence that a public route works.
|
|
11
|
-
|
|
12
|
-
Order of operations and why a draft renders nothing: §7 of `bettercms://playbook/schema`.
|
|
13
|
-
Component refusal codes: [components.md](components.md) §4. Hosting modes, the SSR switch, env
|
|
14
|
-
names and revalidation: [frameworks.md](frameworks.md).
|
|
15
|
-
|
|
16
|
-
## 1. Plan the dependency graph
|
|
17
|
-
|
|
18
|
-
Confirm the project and its hosting mode from `get_project`, never from the repo name (`hybrid`
|
|
19
|
-
is handled exactly like `managed`). Record the source commit, content revision, live baseline,
|
|
20
|
-
release target and rollback point.
|
|
21
|
-
|
|
22
|
-
Publish in dependency order: component definitions (and nested components), Form documents, the
|
|
23
|
-
global navigation and footer, the Global Layout, pages and entries, SEO, then the frontend code
|
|
24
|
-
that reads them.
|
|
25
|
-
|
|
26
|
-
- **Chrome**: components first, then `publish_layout`; the order and the logo refusals are in
|
|
27
|
-
[components.md](components.md) §4.
|
|
28
|
-
- **Forms**: test the destination and consent behaviour, then `publish_form`; a draft form answers
|
|
29
|
-
403 to every public submission.
|
|
30
|
-
- **Pages and entries are two rows.** Publishing a singleton page publishes its entry too, as the
|
|
31
|
-
dashboard's publish button does, unless the entry's own gate refuses it (its workflow stage, or
|
|
32
|
-
the publish permission). `ENTRY_STILL_DRAFT` means the page went live over content that did
|
|
33
|
-
not, and names the entry and why; `ENTRY_SEED_FAILED` on `create_page` means none of its seed
|
|
34
|
-
data was saved.
|
|
35
|
-
- Publishing copies the stored draft to live, so what you checked in the draft is what ships.
|
|
36
|
-
- **Many at once**: `publish_entries` { action: 'publish' | 'unpublish', entryIds, pageIds } takes
|
|
37
|
-
up to 100 of each in one call (pages publish only). Each item still passes its own gates, and
|
|
38
|
-
nothing stops the run: read `failed` for every refusal and its reason, then `outcome` for what
|
|
39
|
-
the served pages show (the same shape `update_page` returns on a publish, below).
|
|
40
|
-
- **Throw away a draft**: `discard_draft` with `entryId` or `pageId` returns it to exactly what is
|
|
41
|
-
live; a page also drops its unpublished Layout overrides (`no_published_version` if it never
|
|
42
|
-
went live: delete it instead).
|
|
43
|
-
- **Edit a page safely**: read `get_page` and send its `optimisticVersion` as `ifMatch` on
|
|
44
|
-
`update_page`; a 409 means someone changed it since, so read again. `blockJson` replaces the
|
|
45
|
-
whole composition, so an unguarded write can erase a concurrent edit.
|
|
46
|
-
|
|
47
|
-
## 2. Choose the delivery lane
|
|
48
|
-
|
|
49
|
-
| Hosting mode | Release path | Public proof |
|
|
50
|
-
|---|---|---|
|
|
51
|
-
| `managed` or `hybrid` | `deploy_project` or `create_deploy_upload`, then `get_deploy_status` | Cache-busted BetterCMS URL answers 200 with the intended revision |
|
|
52
|
-
| `headless` | Publish content in BetterCMS; the frontend deploys through its own host (`npx @bettercms-ai/cli@latest deploy --to <host>` the first time, which registers the rebuild hook) and rebuild or revalidation | The external host's URL answers 200 with the intended content and metadata |
|
|
53
|
-
|
|
54
|
-
A managed site renders static unless the owner switches Hosting to **Run as a server app**, which
|
|
55
|
-
must happen before an SSR build ships ([frameworks.md](frameworks.md)).
|
|
56
|
-
|
|
57
|
-
### What a publish builds
|
|
58
|
-
|
|
59
|
-
On a site BetterCMS hosts, a publish applies in place with no build: values, Layout values, SEO of a
|
|
60
|
-
page with HTML on disk, and a reorder or removal of sections the build draws from the CMS (every band
|
|
61
|
-
stamped `data-bcms-block`, measured in CMS order). Only what needs new markup builds, by itself: a
|
|
62
|
-
new or removed page, a new section or one whose component changed, a Layout structure change, SEO
|
|
63
|
-
of a page with no HTML on disk, and a list whose published row count the page cannot take (it
|
|
64
|
-
renders rows no address names, or was built empty). That publish waits for its build ("Rebuilding so
|
|
65
|
-
the list shows N rows") and fails with the reason if the build still shows the page's own rows: the
|
|
66
|
-
site's code renders the list itself. `get_next_steps` names what is waiting.
|
|
67
|
-
|
|
68
|
-
A static page whose own code fixes its sections takes all of it in place too, when the project's
|
|
69
|
-
in-place structure switch is on: the publish writes the page's block list into the built HTML.
|
|
70
|
-
The code's bands stand in the published order, a removed one is cut, a section added in
|
|
71
|
-
BetterCMS is drawn between them, and every root takes its block's style. No repository change
|
|
72
|
-
and no build; each rebuild is put right the same way. The gate's `sections` row says which
|
|
73
|
-
routes take it (`accepts: "in-place"`).
|
|
74
|
-
|
|
75
|
-
A collection's item pages take it per TEMPLATE: the collection page's sections are the
|
|
76
|
-
template's, held once, so a section added to that page (`update_page` on the collection page)
|
|
77
|
-
stands on every entry's route, and an entry has no sections of its own. Its publish reads one
|
|
78
|
-
row for all of them (`route` `blog/*`). The entry's body is a section too: add sections around
|
|
79
|
-
it or move it, and space it through its field's style, since its element is the field's.
|
|
80
|
-
|
|
81
|
-
The SEO a page or entry stores is what its `<head>` says, whenever it was set: the title, the
|
|
82
|
-
description, the canonical, the card fields, a `noindex` that is on. A field left empty keeps what
|
|
83
|
-
the build rendered, and a site default only fills a tag the build has none of. Only a real choice
|
|
84
|
-
replaces what the build renders: a meta title that merely repeats the page's name (whatever its
|
|
85
|
-
case), a description that repeats the title or is the entry's auto-filled first paragraph, and
|
|
86
|
-
anything on a row no person or agent has ever written (`origin` `system:*`) are read as not set. To
|
|
87
|
-
change such a head, set the title or description yourself. On a route the site's app renders, no write
|
|
88
|
-
reaches the head: a publish whose SEO the page does not show fails with "Published, but this page's
|
|
89
|
-
code doesn't read this field yet: SEO title", and the fix is to render the head from the published
|
|
90
|
-
values.
|
|
91
|
-
|
|
92
|
-
Builds run `build:bcms` instead of `build` when `package.json` declares it. Keep CI-only checks out
|
|
93
|
-
of it and every step the build needs in: `"build": "astro check && astro build"` beside
|
|
94
|
-
`"build:bcms": "astro build"`. The convert codemod adds one to a repo whose build chains a check.
|
|
95
|
-
|
|
96
|
-
### Add a section, or set its spacing
|
|
97
|
-
|
|
98
|
-
One call each, on any page (a page BetterCMS draws, a code-built one, a collection template):
|
|
99
|
-
|
|
100
|
-
- **Add a section here**: `update_page { pageId, blockJson: { insertSection: { componentId, after: "<block id>" } } }`.
|
|
101
|
-
`componentId` is a component from `list_components`; a `builtin:*` id is copied into the project
|
|
102
|
-
once and the copy is placed. Place it with `at` (an index into the page's sections), `before`
|
|
103
|
-
or `after` (a section's block id from `get_page`); none of the three adds it last. `style` and
|
|
104
|
-
`overrides` set its block style and its copy in the same call.
|
|
105
|
-
- **Set spacing on this section**: `update_page { pageId, blockJson: { sectionStyle: { blockId, style } } }`.
|
|
106
|
-
Each key of `style` replaces the stored one and `null` unsets it: `paddingTop`, `paddingBottom`,
|
|
107
|
-
`paddingSides` (px, 0 to 240), `spaceTop`, `spaceBottom`, `spaceSides` (the spacing tokens),
|
|
108
|
-
`bg`, `bgCustom`, `contentWidth`, `contentAlign`, `theme`, `corner`, `shadow`, `border`,
|
|
109
|
-
`opacity`, `hideOn`. A key the block style does not have is refused (`INVALID_STYLE`).
|
|
110
|
-
|
|
111
|
-
The server edits the page as stored, so nothing else on it is resent or overwritten; send
|
|
112
|
-
`ifMatch` to refuse a page that changed since you read it. Both save a DRAFT: add
|
|
113
|
-
`status: "published"` to the same call, or publish after. Then read the answer, top down:
|
|
114
|
-
|
|
115
|
-
1. `held`: the draft is saved and the live page will NOT show it. It names the reason and the one
|
|
116
|
-
action (below). Do the action; do not report the change as made.
|
|
117
|
-
2. `outcome` (on a publish): `status` `live` is every value found on the served page. `partial`
|
|
118
|
-
lists what is not in `notLive`, each with its `reason` and `action` (the table under Reading
|
|
119
|
-
`get_deploy_status`). `next: "get_deploy_status"` means it is not judged whole: nothing has
|
|
120
|
-
looked at it yet, or a section change is still being written (`NOT_CHECKED`). It resolves by
|
|
121
|
-
itself to live or held: poll `publishes` there before you say anything is live.
|
|
122
|
-
3. `section`: the block's `id` and `index`, to place the next one beside it.
|
|
123
|
-
4. `warnings`: `COMPONENT_DRAFT` is a section that renders nothing until `publish_component`.
|
|
124
|
-
|
|
125
|
-
Refusals: `SECTION_NOT_FOUND` (the answer lists the page's section ids), `POSITION_OUT_OF_RANGE`,
|
|
126
|
-
`COMPONENT_NOT_FOUND`, `BUILTIN_NOT_FOUND`. A whole `blockJson` array still replaces the page's
|
|
127
|
-
sections; use it to move or remove one, with `ifMatch`.
|
|
128
|
-
|
|
129
|
-
### A section change that is held at the write
|
|
130
|
-
|
|
131
|
-
`update_page` and `compose_pages` save the draft and answer `held` first (with `holds`) when a
|
|
132
|
-
write adds, moves or removes a section the live page cannot show yet. Each hold has a `reason`,
|
|
133
|
-
a `message`, the `sections` it is about and the one `action`. Never report such a write as done:
|
|
134
|
-
|
|
135
|
-
- `CODE_FIXED_SECTIONS`: the site's code fixes this page's sections, so no publish ships the
|
|
136
|
-
change. Read the route's `sections` row in `get_next_steps` (`receipt.gate`): its `fixCode`
|
|
137
|
-
names the lane ([finish-site.md](finish-site.md)). `enable-in-place-structure` is the user's
|
|
138
|
-
switch and needs no code. `run-sections-conversion` is the wrap: `componentize_sections` with
|
|
139
|
-
`codemod: true` and the `digest` of `get_componentize_plan`, or, when that answers `skipped`
|
|
140
|
-
(no repository the platform may commit to), `npx @bettercms-ai/convert@latest --sections --root .`
|
|
141
|
-
in a checkout ([componentize.md](componentize.md) §5). After its build, publish.
|
|
142
|
-
- `SECTION_NEEDS_BUILD`: the build draws the page's sections from BetterCMS. Publish: the build
|
|
143
|
-
starts by itself, and a move or a removal needs none.
|
|
144
|
-
- `STRUCTURE_UNMEASURED`: nothing has measured the page. Publish, then read the publish's outcome
|
|
145
|
-
in `get_deploy_status`.
|
|
146
|
-
|
|
147
|
-
### Reading `get_deploy_status`
|
|
148
|
-
|
|
149
|
-
- `release` is the newest release: `id`, `sha`, `status`, `phaseMarks`, `error`, `createdAt`.
|
|
150
|
-
While an uploaded build is ingested or released the state is `building`, `release.sha` is the
|
|
151
|
-
new sha and `lastLiveSha` the old one. Sending the live sha again does nothing. A failed deploy
|
|
152
|
-
has `release.status` `failed` and its `error`.
|
|
153
|
-
- `held` means the release waits until the hold is cleared; `held.by` is `staff`, `archived` or
|
|
154
|
-
`no-subdomain`.
|
|
155
|
-
- `slot` is where the bytes are: `current` is production, `staging` the staging handle.
|
|
156
|
-
`productionSha` is what production serves. `awaitingPromote: true` is a build that is verified
|
|
157
|
-
and ready on staging while production keeps the older sha, until `promote_project` runs.
|
|
158
|
-
- The poll is finished only when `slot` is `current`, or when `awaitingPromote` is false and
|
|
159
|
-
`productionSha` equals the sha you deployed.
|
|
160
|
-
- `publishes` is the newest publishes, value by value. `status` `live` means every published value
|
|
161
|
-
was found in the file the site serves that page from. `partial` means some were not: `summary`
|
|
162
|
-
counts them ("3 of 4 live · 1 held") and `notLive` lists each with its `route`, `address`,
|
|
163
|
-
`state`, `reason` and the one `action` that fixes it. Never tell the user a publish is live
|
|
164
|
-
while it reads `partial`: relay the reason and do the action, then poll again.
|
|
165
|
-
|
|
166
|
-
| `reason` | `state` | The one action |
|
|
167
|
-
|---|---|---|
|
|
168
|
-
| `NOT_STAMPED` | held | Rebuild: the built page has no element for the field and does not show it |
|
|
169
|
-
| `VALUE_REFUSED` | held | Bind the field to an element that can hold its value |
|
|
170
|
-
| `APP_DOES_NOT_READ` | held | Convert in code: make the page's code read the published field |
|
|
171
|
-
| `ROWS_NEED_BUILD`, `SECTION_NEEDS_BUILD`, `NEEDS_BUILD` | held | Wait for the build the publish started |
|
|
172
|
-
| `STRUCTURE_UNBOUND` | refused | The fix the route's `sections` gate row names: the in-place switch, or the sections conversion |
|
|
173
|
-
| `SECTION_MISSING` | held | The same: the served page does not render the section |
|
|
174
|
-
| `SECTION_NOT_ON_PAGE` | held | In place: a section of the site's own code has no band on this page. The sections conversion, or remove the placement |
|
|
175
|
-
| `SECTION_NOT_RENDERABLE` | held | In place: the added section's component is not published, or it needs a form. `publish_component`, then publish the page again |
|
|
176
|
-
| `STYLE_NOT_PAINTABLE` | held | Rebuild once the section root carries the style marker (`sectionRootAttrs`, or the codemod's `--upgrade-sections`: [components.md](components.md)) |
|
|
177
|
-
| `WINDOW_NOT_OPEN` | held | Nothing: it goes live when its window opens |
|
|
178
|
-
| `ROUTE_NOT_OWNED` | held | Change one of the two page paths that share the URL |
|
|
179
|
-
| `APP_UNREACHABLE`, `NOT_CHECKED`, `EDGE_PENDING`, `IMAGE_UNAVAILABLE`, `STRUCTURE_UNMEASURED` | unverified | Poll again; publish again if it stays |
|
|
180
|
-
|
|
181
|
-
Each row's `action.code` is one of these, and `get_next_steps` lists every held or refused value
|
|
182
|
-
as `publish-held` until it is gone:
|
|
183
|
-
|
|
184
|
-
| `action.code` | What you do |
|
|
185
|
-
|---|---|
|
|
186
|
-
| `rebuild` | No tool rebuilds: change the site's code if the reason names it, then ask the user to Rebuild from Publishing (or `deploy_project` from a checkout) |
|
|
187
|
-
| `wait_for_build` | Poll `get_deploy_status` until the build the publish started is live |
|
|
188
|
-
| `convert_in_code` | Make the page's code read the field ([bindings.md](bindings.md)), then release |
|
|
189
|
-
| `control_section_order` | The route's `sections` gate row names it: the in-place switch (the user's), or `componentize_sections` with `codemod: true` (else `convert --sections` in a checkout), then release |
|
|
190
|
-
| `editor_setup` | `editor_setup` |
|
|
191
|
-
| `rebind` | Bind the field to an element that can hold it, in the site's code |
|
|
192
|
-
| `resolve_route` | Change one of the two page paths |
|
|
193
|
-
| `wait`, `retry` | Poll again; publish again if it stays |
|
|
194
|
-
|
|
195
|
-
### Deploy to Vercel or Netlify
|
|
196
|
-
|
|
197
|
-
To serve a `managed` or `hybrid` site from the owner's own Vercel or Netlify account, call
|
|
198
|
-
`list_hosting_connections` first; with no working connection for that host, hand the user
|
|
199
|
-
`connectUrl` to connect it in the browser and wait. Then `deploy_to_host` with the `provider`:
|
|
200
|
-
BetterCMS builds, the host serves, and calling it again redeploys the same site. Relay any
|
|
201
|
-
refusal's `fixUrl` to the user. Poll `get_host_deploy_status` until the newest deployment is live
|
|
202
|
-
and verify its `providerUrl`. Forms post to the BetterCMS API, so they need no rewrite, and no host
|
|
203
|
-
CLI or pasted host token is ever needed while a connection exists.
|
|
204
|
-
|
|
205
|
-
## 3. Execute and verify
|
|
206
|
-
|
|
207
|
-
1. After an authorized release, record the operation ids and revisions from each call.
|
|
208
|
-
2. On a refusal, stop: resolve the cause its code names, then retry only with changed input.
|
|
209
|
-
3. Fetch each touched route directly, cache-busted: status, the copy you published, component
|
|
210
|
-
overrides, media URLs, metadata and forms. Include one collection detail route, the global
|
|
211
|
-
navigation and footer, and mobile interaction when affected.
|
|
212
|
-
4. Run the verify loop in SKILL.md, noting each report's freshness.
|
|
213
|
-
5. On a cached site, confirm the publish webhook or rebuild actually refreshed the public page.
|
|
214
|
-
|
|
215
|
-
## 4. Roll back
|
|
216
|
-
|
|
217
|
-
Use the known prior content revision and source commit. For an entry, `list_entry_versions` then
|
|
218
|
-
`restore_entry_version`: it writes that version back as the current draft and adds a new version,
|
|
219
|
-
so publish it afterwards. Unpublished edits you do not want are `discard_draft`, not a restore.
|
|
220
|
-
Restore only the affected lane, recheck the live URLs, and never overwrite newer unrelated edits.
|
|
221
|
-
|
|
222
|
-
## 4b. Stage a change on a branch
|
|
223
|
-
|
|
224
|
-
A content branch is a copy of the project's content to edit without touching the live site.
|
|
225
|
-
`create_branch` { name } returns it; pass its id as `branchId` on every call made inside it
|
|
226
|
-
(`list_branches` names the one you are on). To merge, `apply_branch` { branchId, dryRun: true }
|
|
227
|
-
returns the diff and its conflicts: show it to the user, then apply, with `include` to take only
|
|
228
|
-
some changes and `fields` to settle each conflicting field (`keep_branch` / `keep_parent`).
|
|
229
|
-
|
|
230
|
-
## 5. Reconcile a re-import, a push or a continuous sync
|
|
231
|
-
|
|
232
|
-
Identify the previous source snapshot or commit, the new one, the import manifest, the
|
|
233
|
-
source-to-target id map and the current BetterCMS state. `list_activity` with `since` reads the
|
|
234
|
-
project's durable change feed from an ISO timestamp (up to 100 events per call): call it with the
|
|
235
|
-
time of the previous sync to see every dashboard or agent edit made since. Compare by stable identity, never
|
|
236
|
-
by display name or row order, and confirm it is the same project and source branch. Entries
|
|
237
|
-
written with `externalKey` (the source id) are their own id map: `list_content_entries` with
|
|
238
|
-
`externalKey` finds one, and re-sending the row answers `created`, `updated` or `unchanged`
|
|
239
|
-
([write-limits.md](write-limits.md)). Media, and references to items not yet created, still
|
|
240
|
-
need the map. Without a manifest, keys or an id map this is no longer an automatic sync; treat
|
|
241
|
-
it as a reviewed migration (the `bettercms-migrate` skill).
|
|
242
|
-
|
|
243
|
-
Classify each difference and keep a three-way view where you can (the value last imported, the
|
|
244
|
-
current source value, the current CMS value). Never flatten a person's CMS edit back to imported
|
|
245
|
-
code because the file was touched.
|
|
246
|
-
|
|
247
|
-
| Difference | Resolution |
|
|
248
|
-
|---|---|
|
|
249
|
-
| New in source | Import with the normal model and component rules; file into the existing Content sidebar folders |
|
|
250
|
-
| Changed only in source | Update fields that are still code-owned or explicitly synced; CMS-owned values stay |
|
|
251
|
-
| Changed only in CMS | Keep |
|
|
252
|
-
| Changed on both sides | Report both values with provenance; apply a winner only after the user chooses |
|
|
253
|
-
| Missing in source | An orphan, never an automatic delete. Tell removed from moved or renamed; offer archive / keep / relink with the inbound references, routes and redirects each affects |
|
|
254
|
-
| Route or slug change | A URL and redirect plan (`create_redirect`); check collisions, canonicals and internal links first |
|
|
255
|
-
| Assets | Match by source id or path plus checksum; a changed binary is a new revision; a missing source asset never breaks a published reference until the user approves its replacement or removal |
|
|
256
|
-
|
|
257
|
-
Dry-run the proposed operations, then run the same snapshot again to prove it is idempotent (no
|
|
258
|
-
new changes the second time). Apply only approved, supported changes, then compare counts,
|
|
259
|
-
references, component usage, field bindings, preview and the affected public routes. The rollback
|
|
260
|
-
for a content value the sync got wrong is §4.
|
|
261
|
-
|
|
262
|
-
## 6. Automation surfaces
|
|
263
|
-
|
|
264
|
-
"Run X when content changes" maps onto what exists; there is no user-deployed function runtime.
|
|
265
|
-
|
|
266
|
-
| Need | Surface | Owner |
|
|
267
|
-
|---|---|---|
|
|
268
|
-
| Rebuild or revalidate a site on publish | The publish webhook, signed `X-BetterCMS-Signature` (HMAC-SHA256 of the raw body) with the project's webhook secret ([frameworks.md](frameworks.md) Revalidation) | Secret set in the dashboard; your route verifies it |
|
|
269
|
-
| Notify another system of an event | Event webhooks, `list_webhooks` / `create_webhook`: `content.publish`, `content.unpublish`, `content.stage_changed` (workflow stage), `content.created`, `content.updated`, `content.deleted` and `deployment.triggered`; HMAC-SHA256 signed in `X-BetterCMS-Signature`; payloads carry ids, never content: an entry's `content.publish`/`content.unpublish` carries `data.entryId`, a page's carries `data.pageId` and `kind: "page"` (so `entryId` is not always present). The signing `secret` is returned ONCE, on create. A receiver that writes back on `content.updated` loops unless it skips its own writes. Per-subscription payload filters: dashboard | You |
|
|
270
|
-
| Send form submissions somewhere | Per-form integrations: Google Sheets, Slack, a webhook, email | Dashboard only |
|
|
271
|
-
| Go live or retire at a time | `scheduledAt` (publish a draft at a time; needs publish permission) and `unpublishAt` (return a published row to draft) on `update_content_entry` and `update_page`; `null` cancels either. Campaigns: `publish_campaign` takes both | You. An edit that omits `scheduledAt` CANCELS the pending publish, so resend it with every edit before it fires; omitting `unpublishAt` never cancels it |
|
|
272
|
-
| Show one section only for a period | `window` `{ from?, until? }` on a page block (`update_page` `blockJson`) or on a Layout Section (`update_layout` `add-section`, `update-section`). Shown from `from` until `until`; each is an ISO 8601 datetime with an offset (`2026-11-27T00:00:00+01:00`), stored as UTC, and either may be left out. `null` clears it. Refusals: `invalid_window` (no offset, or not a date), `window_inverted` (`from` not before `until`), `window_in_component` (inside a component's `blockJson`: put it on the placement), `window_in_entry` (on a block inside an entry's sections value: put it on the page block or Layout Section that shows the entry). A Section a page adds or customizes takes a window like a Global one | You. Outside its window the block is in no published output: the page, its Markdown, the delivery API and search. A draft preview draws it, marked. A page BetterCMS renders per request changes at the boundary; a static site catches up within about a minute, in place when a section goes and with a build when one appears. Site code that renders its own header ignores a Section's window |
|
|
273
|
-
| Load many entries | `create_content_entries`: up to 200 drafts of one collection per call, each row `created` or `failed` (with `externalKey` also `updated` or `unchanged`, so a re-run is safe); then `publish_entries` | You |
|
|
274
|
-
| React to edits made elsewhere | Poll `list_activity` with `since` set to the time of your last read (1–100 events per call) | You |
|
|
275
|
-
| Generate or rewrite in bulk with a human gate | AI jobs: `list_ai_jobs`, `get_ai_job`, then `approve_ai_job` or `reject_ai_job` on the user's word | User approves |
|
|
276
|
-
|
|
277
|
-
Not available, so say so instead of building around it: code that runs inside BetterCMS on a
|
|
278
|
-
content event, and a change subscription over the MCP (poll `list_activity` `since`). Anything
|
|
279
|
-
beyond the table runs in the user's own infrastructure, triggered by a webhook. Why: a timer on the
|
|
280
|
-
delivery API looks like a trigger, but it serves published content only and never sees an edit
|
|
281
|
-
before it ships.
|
|
282
|
-
|
|
283
|
-
## Report
|
|
284
|
-
|
|
285
|
-
State what is live, what remains draft, whether a rollback ran, and any owner action still
|
|
286
|
-
required (Visual Approval, a render-mode change, a secret). For a sync, list exact created,
|
|
287
|
-
updated, unchanged, orphaned and conflicted ids, the choices still open, and the rollback point;
|
|
288
|
-
an open conflict is never a successful sync. Do not say "launched" until the public response is
|
|
289
|
-
verified.
|
|
1
|
+
---
|
|
2
|
+
title: "Publishing and sync"
|
|
3
|
+
description: "Publish order, delivery lanes, execute and verify, rollback, reconciling a re-import or continuous sync, and the automation surfaces that exist."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Publishing, release, rollback and sync reconciliation
|
|
7
|
+
|
|
8
|
+
**Outcome.** The intended revision reaches the right public site with its dependencies, routes,
|
|
9
|
+
forms and cache state checked, and a new source revision lands without overwriting editorial
|
|
10
|
+
work. A dashboard success is not evidence that a public route works.
|
|
11
|
+
|
|
12
|
+
Order of operations and why a draft renders nothing: §7 of `bettercms://playbook/schema`.
|
|
13
|
+
Component refusal codes: [components.md](components.md) §4. Hosting modes, the SSR switch, env
|
|
14
|
+
names and revalidation: [frameworks.md](frameworks.md).
|
|
15
|
+
|
|
16
|
+
## 1. Plan the dependency graph
|
|
17
|
+
|
|
18
|
+
Confirm the project and its hosting mode from `get_project`, never from the repo name (`hybrid`
|
|
19
|
+
is handled exactly like `managed`). Record the source commit, content revision, live baseline,
|
|
20
|
+
release target and rollback point.
|
|
21
|
+
|
|
22
|
+
Publish in dependency order: component definitions (and nested components), Form documents, the
|
|
23
|
+
global navigation and footer, the Global Layout, pages and entries, SEO, then the frontend code
|
|
24
|
+
that reads them.
|
|
25
|
+
|
|
26
|
+
- **Chrome**: components first, then `publish_layout`; the order and the logo refusals are in
|
|
27
|
+
[components.md](components.md) §4.
|
|
28
|
+
- **Forms**: test the destination and consent behaviour, then `publish_form`; a draft form answers
|
|
29
|
+
403 to every public submission.
|
|
30
|
+
- **Pages and entries are two rows.** Publishing a singleton page publishes its entry too, as the
|
|
31
|
+
dashboard's publish button does, unless the entry's own gate refuses it (its workflow stage, or
|
|
32
|
+
the publish permission). `ENTRY_STILL_DRAFT` means the page went live over content that did
|
|
33
|
+
not, and names the entry and why; `ENTRY_SEED_FAILED` on `create_page` means none of its seed
|
|
34
|
+
data was saved.
|
|
35
|
+
- Publishing copies the stored draft to live, so what you checked in the draft is what ships.
|
|
36
|
+
- **Many at once**: `publish_entries` { action: 'publish' | 'unpublish', entryIds, pageIds } takes
|
|
37
|
+
up to 100 of each in one call (pages publish only). Each item still passes its own gates, and
|
|
38
|
+
nothing stops the run: read `failed` for every refusal and its reason, then `outcome` for what
|
|
39
|
+
the served pages show (the same shape `update_page` returns on a publish, below).
|
|
40
|
+
- **Throw away a draft**: `discard_draft` with `entryId` or `pageId` returns it to exactly what is
|
|
41
|
+
live; a page also drops its unpublished Layout overrides (`no_published_version` if it never
|
|
42
|
+
went live: delete it instead).
|
|
43
|
+
- **Edit a page safely**: read `get_page` and send its `optimisticVersion` as `ifMatch` on
|
|
44
|
+
`update_page`; a 409 means someone changed it since, so read again. `blockJson` replaces the
|
|
45
|
+
whole composition, so an unguarded write can erase a concurrent edit.
|
|
46
|
+
|
|
47
|
+
## 2. Choose the delivery lane
|
|
48
|
+
|
|
49
|
+
| Hosting mode | Release path | Public proof |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `managed` or `hybrid` | `deploy_project` or `create_deploy_upload`, then `get_deploy_status` | Cache-busted BetterCMS URL answers 200 with the intended revision |
|
|
52
|
+
| `headless` | Publish content in BetterCMS; the frontend deploys through its own host (`npx @bettercms-ai/cli@latest deploy --to <host>` the first time, which registers the rebuild hook) and rebuild or revalidation | The external host's URL answers 200 with the intended content and metadata |
|
|
53
|
+
|
|
54
|
+
A managed site renders static unless the owner switches Hosting to **Run as a server app**, which
|
|
55
|
+
must happen before an SSR build ships ([frameworks.md](frameworks.md)).
|
|
56
|
+
|
|
57
|
+
### What a publish builds
|
|
58
|
+
|
|
59
|
+
On a site BetterCMS hosts, a publish applies in place with no build: values, Layout values, SEO of a
|
|
60
|
+
page with HTML on disk, and a reorder or removal of sections the build draws from the CMS (every band
|
|
61
|
+
stamped `data-bcms-block`, measured in CMS order). Only what needs new markup builds, by itself: a
|
|
62
|
+
new or removed page, a new section or one whose component changed, a Layout structure change, SEO
|
|
63
|
+
of a page with no HTML on disk, and a list whose published row count the page cannot take (it
|
|
64
|
+
renders rows no address names, or was built empty). That publish waits for its build ("Rebuilding so
|
|
65
|
+
the list shows N rows") and fails with the reason if the build still shows the page's own rows: the
|
|
66
|
+
site's code renders the list itself. `get_next_steps` names what is waiting.
|
|
67
|
+
|
|
68
|
+
A static page whose own code fixes its sections takes all of it in place too, when the project's
|
|
69
|
+
in-place structure switch is on: the publish writes the page's block list into the built HTML.
|
|
70
|
+
The code's bands stand in the published order, a removed one is cut, a section added in
|
|
71
|
+
BetterCMS is drawn between them, and every root takes its block's style. No repository change
|
|
72
|
+
and no build; each rebuild is put right the same way. The gate's `sections` row says which
|
|
73
|
+
routes take it (`accepts: "in-place"`).
|
|
74
|
+
|
|
75
|
+
A collection's item pages take it per TEMPLATE: the collection page's sections are the
|
|
76
|
+
template's, held once, so a section added to that page (`update_page` on the collection page)
|
|
77
|
+
stands on every entry's route, and an entry has no sections of its own. Its publish reads one
|
|
78
|
+
row for all of them (`route` `blog/*`). The entry's body is a section too: add sections around
|
|
79
|
+
it or move it, and space it through its field's style, since its element is the field's.
|
|
80
|
+
|
|
81
|
+
The SEO a page or entry stores is what its `<head>` says, whenever it was set: the title, the
|
|
82
|
+
description, the canonical, the card fields, a `noindex` that is on. A field left empty keeps what
|
|
83
|
+
the build rendered, and a site default only fills a tag the build has none of. Only a real choice
|
|
84
|
+
replaces what the build renders: a meta title that merely repeats the page's name (whatever its
|
|
85
|
+
case), a description that repeats the title or is the entry's auto-filled first paragraph, and
|
|
86
|
+
anything on a row no person or agent has ever written (`origin` `system:*`) are read as not set. To
|
|
87
|
+
change such a head, set the title or description yourself. On a route the site's app renders, no write
|
|
88
|
+
reaches the head: a publish whose SEO the page does not show fails with "Published, but this page's
|
|
89
|
+
code doesn't read this field yet: SEO title", and the fix is to render the head from the published
|
|
90
|
+
values.
|
|
91
|
+
|
|
92
|
+
Builds run `build:bcms` instead of `build` when `package.json` declares it. Keep CI-only checks out
|
|
93
|
+
of it and every step the build needs in: `"build": "astro check && astro build"` beside
|
|
94
|
+
`"build:bcms": "astro build"`. The convert codemod adds one to a repo whose build chains a check.
|
|
95
|
+
|
|
96
|
+
### Add a section, or set its spacing
|
|
97
|
+
|
|
98
|
+
One call each, on any page (a page BetterCMS draws, a code-built one, a collection template):
|
|
99
|
+
|
|
100
|
+
- **Add a section here**: `update_page { pageId, blockJson: { insertSection: { componentId, after: "<block id>" } } }`.
|
|
101
|
+
`componentId` is a component from `list_components`; a `builtin:*` id is copied into the project
|
|
102
|
+
once and the copy is placed. Place it with `at` (an index into the page's sections), `before`
|
|
103
|
+
or `after` (a section's block id from `get_page`); none of the three adds it last. `style` and
|
|
104
|
+
`overrides` set its block style and its copy in the same call.
|
|
105
|
+
- **Set spacing on this section**: `update_page { pageId, blockJson: { sectionStyle: { blockId, style } } }`.
|
|
106
|
+
Each key of `style` replaces the stored one and `null` unsets it: `paddingTop`, `paddingBottom`,
|
|
107
|
+
`paddingSides` (px, 0 to 240), `spaceTop`, `spaceBottom`, `spaceSides` (the spacing tokens),
|
|
108
|
+
`bg`, `bgCustom`, `contentWidth`, `contentAlign`, `theme`, `corner`, `shadow`, `border`,
|
|
109
|
+
`opacity`, `hideOn`. A key the block style does not have is refused (`INVALID_STYLE`).
|
|
110
|
+
|
|
111
|
+
The server edits the page as stored, so nothing else on it is resent or overwritten; send
|
|
112
|
+
`ifMatch` to refuse a page that changed since you read it. Both save a DRAFT: add
|
|
113
|
+
`status: "published"` to the same call, or publish after. Then read the answer, top down:
|
|
114
|
+
|
|
115
|
+
1. `held`: the draft is saved and the live page will NOT show it. It names the reason and the one
|
|
116
|
+
action (below). Do the action; do not report the change as made.
|
|
117
|
+
2. `outcome` (on a publish): `status` `live` is every value found on the served page. `partial`
|
|
118
|
+
lists what is not in `notLive`, each with its `reason` and `action` (the table under Reading
|
|
119
|
+
`get_deploy_status`). `next: "get_deploy_status"` means it is not judged whole: nothing has
|
|
120
|
+
looked at it yet, or a section change is still being written (`NOT_CHECKED`). It resolves by
|
|
121
|
+
itself to live or held: poll `publishes` there before you say anything is live.
|
|
122
|
+
3. `section`: the block's `id` and `index`, to place the next one beside it.
|
|
123
|
+
4. `warnings`: `COMPONENT_DRAFT` is a section that renders nothing until `publish_component`.
|
|
124
|
+
|
|
125
|
+
Refusals: `SECTION_NOT_FOUND` (the answer lists the page's section ids), `POSITION_OUT_OF_RANGE`,
|
|
126
|
+
`COMPONENT_NOT_FOUND`, `BUILTIN_NOT_FOUND`. A whole `blockJson` array still replaces the page's
|
|
127
|
+
sections; use it to move or remove one, with `ifMatch`.
|
|
128
|
+
|
|
129
|
+
### A section change that is held at the write
|
|
130
|
+
|
|
131
|
+
`update_page` and `compose_pages` save the draft and answer `held` first (with `holds`) when a
|
|
132
|
+
write adds, moves or removes a section the live page cannot show yet. Each hold has a `reason`,
|
|
133
|
+
a `message`, the `sections` it is about and the one `action`. Never report such a write as done:
|
|
134
|
+
|
|
135
|
+
- `CODE_FIXED_SECTIONS`: the site's code fixes this page's sections, so no publish ships the
|
|
136
|
+
change. Read the route's `sections` row in `get_next_steps` (`receipt.gate`): its `fixCode`
|
|
137
|
+
names the lane ([finish-site.md](finish-site.md)). `enable-in-place-structure` is the user's
|
|
138
|
+
switch and needs no code. `run-sections-conversion` is the wrap: `componentize_sections` with
|
|
139
|
+
`codemod: true` and the `digest` of `get_componentize_plan`, or, when that answers `skipped`
|
|
140
|
+
(no repository the platform may commit to), `npx @bettercms-ai/convert@latest --sections --root .`
|
|
141
|
+
in a checkout ([componentize.md](componentize.md) §5). After its build, publish.
|
|
142
|
+
- `SECTION_NEEDS_BUILD`: the build draws the page's sections from BetterCMS. Publish: the build
|
|
143
|
+
starts by itself, and a move or a removal needs none.
|
|
144
|
+
- `STRUCTURE_UNMEASURED`: nothing has measured the page. Publish, then read the publish's outcome
|
|
145
|
+
in `get_deploy_status`.
|
|
146
|
+
|
|
147
|
+
### Reading `get_deploy_status`
|
|
148
|
+
|
|
149
|
+
- `release` is the newest release: `id`, `sha`, `status`, `phaseMarks`, `error`, `createdAt`.
|
|
150
|
+
While an uploaded build is ingested or released the state is `building`, `release.sha` is the
|
|
151
|
+
new sha and `lastLiveSha` the old one. Sending the live sha again does nothing. A failed deploy
|
|
152
|
+
has `release.status` `failed` and its `error`.
|
|
153
|
+
- `held` means the release waits until the hold is cleared; `held.by` is `staff`, `archived` or
|
|
154
|
+
`no-subdomain`.
|
|
155
|
+
- `slot` is where the bytes are: `current` is production, `staging` the staging handle.
|
|
156
|
+
`productionSha` is what production serves. `awaitingPromote: true` is a build that is verified
|
|
157
|
+
and ready on staging while production keeps the older sha, until `promote_project` runs.
|
|
158
|
+
- The poll is finished only when `slot` is `current`, or when `awaitingPromote` is false and
|
|
159
|
+
`productionSha` equals the sha you deployed.
|
|
160
|
+
- `publishes` is the newest publishes, value by value. `status` `live` means every published value
|
|
161
|
+
was found in the file the site serves that page from. `partial` means some were not: `summary`
|
|
162
|
+
counts them ("3 of 4 live · 1 held") and `notLive` lists each with its `route`, `address`,
|
|
163
|
+
`state`, `reason` and the one `action` that fixes it. Never tell the user a publish is live
|
|
164
|
+
while it reads `partial`: relay the reason and do the action, then poll again.
|
|
165
|
+
|
|
166
|
+
| `reason` | `state` | The one action |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `NOT_STAMPED` | held | Rebuild: the built page has no element for the field and does not show it |
|
|
169
|
+
| `VALUE_REFUSED` | held | Bind the field to an element that can hold its value |
|
|
170
|
+
| `APP_DOES_NOT_READ` | held | Convert in code: make the page's code read the published field |
|
|
171
|
+
| `ROWS_NEED_BUILD`, `SECTION_NEEDS_BUILD`, `NEEDS_BUILD` | held | Wait for the build the publish started |
|
|
172
|
+
| `STRUCTURE_UNBOUND` | refused | The fix the route's `sections` gate row names: the in-place switch, or the sections conversion |
|
|
173
|
+
| `SECTION_MISSING` | held | The same: the served page does not render the section |
|
|
174
|
+
| `SECTION_NOT_ON_PAGE` | held | In place: a section of the site's own code has no band on this page. The sections conversion, or remove the placement |
|
|
175
|
+
| `SECTION_NOT_RENDERABLE` | held | In place: the added section's component is not published, or it needs a form. `publish_component`, then publish the page again |
|
|
176
|
+
| `STYLE_NOT_PAINTABLE` | held | Rebuild once the section root carries the style marker (`sectionRootAttrs`, or the codemod's `--upgrade-sections`: [components.md](components.md)) |
|
|
177
|
+
| `WINDOW_NOT_OPEN` | held | Nothing: it goes live when its window opens |
|
|
178
|
+
| `ROUTE_NOT_OWNED` | held | Change one of the two page paths that share the URL |
|
|
179
|
+
| `APP_UNREACHABLE`, `NOT_CHECKED`, `EDGE_PENDING`, `IMAGE_UNAVAILABLE`, `STRUCTURE_UNMEASURED` | unverified | Poll again; publish again if it stays |
|
|
180
|
+
|
|
181
|
+
Each row's `action.code` is one of these, and `get_next_steps` lists every held or refused value
|
|
182
|
+
as `publish-held` until it is gone:
|
|
183
|
+
|
|
184
|
+
| `action.code` | What you do |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `rebuild` | No tool rebuilds: change the site's code if the reason names it, then ask the user to Rebuild from Publishing (or `deploy_project` from a checkout) |
|
|
187
|
+
| `wait_for_build` | Poll `get_deploy_status` until the build the publish started is live |
|
|
188
|
+
| `convert_in_code` | Make the page's code read the field ([bindings.md](bindings.md)), then release |
|
|
189
|
+
| `control_section_order` | The route's `sections` gate row names it: the in-place switch (the user's), or `componentize_sections` with `codemod: true` (else `convert --sections` in a checkout), then release |
|
|
190
|
+
| `editor_setup` | `editor_setup` |
|
|
191
|
+
| `rebind` | Bind the field to an element that can hold it, in the site's code |
|
|
192
|
+
| `resolve_route` | Change one of the two page paths |
|
|
193
|
+
| `wait`, `retry` | Poll again; publish again if it stays |
|
|
194
|
+
|
|
195
|
+
### Deploy to Vercel or Netlify
|
|
196
|
+
|
|
197
|
+
To serve a `managed` or `hybrid` site from the owner's own Vercel or Netlify account, call
|
|
198
|
+
`list_hosting_connections` first; with no working connection for that host, hand the user
|
|
199
|
+
`connectUrl` to connect it in the browser and wait. Then `deploy_to_host` with the `provider`:
|
|
200
|
+
BetterCMS builds, the host serves, and calling it again redeploys the same site. Relay any
|
|
201
|
+
refusal's `fixUrl` to the user. Poll `get_host_deploy_status` until the newest deployment is live
|
|
202
|
+
and verify its `providerUrl`. Forms post to the BetterCMS API, so they need no rewrite, and no host
|
|
203
|
+
CLI or pasted host token is ever needed while a connection exists.
|
|
204
|
+
|
|
205
|
+
## 3. Execute and verify
|
|
206
|
+
|
|
207
|
+
1. After an authorized release, record the operation ids and revisions from each call.
|
|
208
|
+
2. On a refusal, stop: resolve the cause its code names, then retry only with changed input.
|
|
209
|
+
3. Fetch each touched route directly, cache-busted: status, the copy you published, component
|
|
210
|
+
overrides, media URLs, metadata and forms. Include one collection detail route, the global
|
|
211
|
+
navigation and footer, and mobile interaction when affected.
|
|
212
|
+
4. Run the verify loop in SKILL.md, noting each report's freshness.
|
|
213
|
+
5. On a cached site, confirm the publish webhook or rebuild actually refreshed the public page.
|
|
214
|
+
|
|
215
|
+
## 4. Roll back
|
|
216
|
+
|
|
217
|
+
Use the known prior content revision and source commit. For an entry, `list_entry_versions` then
|
|
218
|
+
`restore_entry_version`: it writes that version back as the current draft and adds a new version,
|
|
219
|
+
so publish it afterwards. Unpublished edits you do not want are `discard_draft`, not a restore.
|
|
220
|
+
Restore only the affected lane, recheck the live URLs, and never overwrite newer unrelated edits.
|
|
221
|
+
|
|
222
|
+
## 4b. Stage a change on a branch
|
|
223
|
+
|
|
224
|
+
A content branch is a copy of the project's content to edit without touching the live site.
|
|
225
|
+
`create_branch` { name } returns it; pass its id as `branchId` on every call made inside it
|
|
226
|
+
(`list_branches` names the one you are on). To merge, `apply_branch` { branchId, dryRun: true }
|
|
227
|
+
returns the diff and its conflicts: show it to the user, then apply, with `include` to take only
|
|
228
|
+
some changes and `fields` to settle each conflicting field (`keep_branch` / `keep_parent`).
|
|
229
|
+
|
|
230
|
+
## 5. Reconcile a re-import, a push or a continuous sync
|
|
231
|
+
|
|
232
|
+
Identify the previous source snapshot or commit, the new one, the import manifest, the
|
|
233
|
+
source-to-target id map and the current BetterCMS state. `list_activity` with `since` reads the
|
|
234
|
+
project's durable change feed from an ISO timestamp (up to 100 events per call): call it with the
|
|
235
|
+
time of the previous sync to see every dashboard or agent edit made since. Compare by stable identity, never
|
|
236
|
+
by display name or row order, and confirm it is the same project and source branch. Entries
|
|
237
|
+
written with `externalKey` (the source id) are their own id map: `list_content_entries` with
|
|
238
|
+
`externalKey` finds one, and re-sending the row answers `created`, `updated` or `unchanged`
|
|
239
|
+
([write-limits.md](write-limits.md)). Media, and references to items not yet created, still
|
|
240
|
+
need the map. Without a manifest, keys or an id map this is no longer an automatic sync; treat
|
|
241
|
+
it as a reviewed migration (the `bettercms-migrate` skill).
|
|
242
|
+
|
|
243
|
+
Classify each difference and keep a three-way view where you can (the value last imported, the
|
|
244
|
+
current source value, the current CMS value). Never flatten a person's CMS edit back to imported
|
|
245
|
+
code because the file was touched.
|
|
246
|
+
|
|
247
|
+
| Difference | Resolution |
|
|
248
|
+
|---|---|
|
|
249
|
+
| New in source | Import with the normal model and component rules; file into the existing Content sidebar folders |
|
|
250
|
+
| Changed only in source | Update fields that are still code-owned or explicitly synced; CMS-owned values stay |
|
|
251
|
+
| Changed only in CMS | Keep |
|
|
252
|
+
| Changed on both sides | Report both values with provenance; apply a winner only after the user chooses |
|
|
253
|
+
| Missing in source | An orphan, never an automatic delete. Tell removed from moved or renamed; offer archive / keep / relink with the inbound references, routes and redirects each affects |
|
|
254
|
+
| Route or slug change | A URL and redirect plan (`create_redirect`); check collisions, canonicals and internal links first |
|
|
255
|
+
| Assets | Match by source id or path plus checksum; a changed binary is a new revision; a missing source asset never breaks a published reference until the user approves its replacement or removal |
|
|
256
|
+
|
|
257
|
+
Dry-run the proposed operations, then run the same snapshot again to prove it is idempotent (no
|
|
258
|
+
new changes the second time). Apply only approved, supported changes, then compare counts,
|
|
259
|
+
references, component usage, field bindings, preview and the affected public routes. The rollback
|
|
260
|
+
for a content value the sync got wrong is §4.
|
|
261
|
+
|
|
262
|
+
## 6. Automation surfaces
|
|
263
|
+
|
|
264
|
+
"Run X when content changes" maps onto what exists; there is no user-deployed function runtime.
|
|
265
|
+
|
|
266
|
+
| Need | Surface | Owner |
|
|
267
|
+
|---|---|---|
|
|
268
|
+
| Rebuild or revalidate a site on publish | The publish webhook, signed `X-BetterCMS-Signature` (HMAC-SHA256 of the raw body) with the project's webhook secret ([frameworks.md](frameworks.md) Revalidation) | Secret set in the dashboard; your route verifies it |
|
|
269
|
+
| Notify another system of an event | Event webhooks, `list_webhooks` / `create_webhook`: `content.publish`, `content.unpublish`, `content.stage_changed` (workflow stage), `content.created`, `content.updated`, `content.deleted` and `deployment.triggered`; HMAC-SHA256 signed in `X-BetterCMS-Signature`; payloads carry ids, never content: an entry's `content.publish`/`content.unpublish` carries `data.entryId`, a page's carries `data.pageId` and `kind: "page"` (so `entryId` is not always present). The signing `secret` is returned ONCE, on create. A receiver that writes back on `content.updated` loops unless it skips its own writes. Per-subscription payload filters: dashboard | You |
|
|
270
|
+
| Send form submissions somewhere | Per-form integrations: Google Sheets, Slack, a webhook, email | Dashboard only |
|
|
271
|
+
| Go live or retire at a time | `scheduledAt` (publish a draft at a time; needs publish permission) and `unpublishAt` (return a published row to draft) on `update_content_entry` and `update_page`; `null` cancels either. Campaigns: `publish_campaign` takes both | You. An edit that omits `scheduledAt` CANCELS the pending publish, so resend it with every edit before it fires; omitting `unpublishAt` never cancels it |
|
|
272
|
+
| Show one section only for a period | `window` `{ from?, until? }` on a page block (`update_page` `blockJson`) or on a Layout Section (`update_layout` `add-section`, `update-section`). Shown from `from` until `until`; each is an ISO 8601 datetime with an offset (`2026-11-27T00:00:00+01:00`), stored as UTC, and either may be left out. `null` clears it. Refusals: `invalid_window` (no offset, or not a date), `window_inverted` (`from` not before `until`), `window_in_component` (inside a component's `blockJson`: put it on the placement), `window_in_entry` (on a block inside an entry's sections value: put it on the page block or Layout Section that shows the entry). A Section a page adds or customizes takes a window like a Global one | You. Outside its window the block is in no published output: the page, its Markdown, the delivery API and search. A draft preview draws it, marked. A page BetterCMS renders per request changes at the boundary; a static site catches up within about a minute, in place when a section goes and with a build when one appears. Site code that renders its own header ignores a Section's window |
|
|
273
|
+
| Load many entries | `create_content_entries`: up to 200 drafts of one collection per call, each row `created` or `failed` (with `externalKey` also `updated` or `unchanged`, so a re-run is safe); then `publish_entries` | You |
|
|
274
|
+
| React to edits made elsewhere | Poll `list_activity` with `since` set to the time of your last read (1–100 events per call) | You |
|
|
275
|
+
| Generate or rewrite in bulk with a human gate | AI jobs: `list_ai_jobs`, `get_ai_job`, then `approve_ai_job` or `reject_ai_job` on the user's word | User approves |
|
|
276
|
+
|
|
277
|
+
Not available, so say so instead of building around it: code that runs inside BetterCMS on a
|
|
278
|
+
content event, and a change subscription over the MCP (poll `list_activity` `since`). Anything
|
|
279
|
+
beyond the table runs in the user's own infrastructure, triggered by a webhook. Why: a timer on the
|
|
280
|
+
delivery API looks like a trigger, but it serves published content only and never sees an edit
|
|
281
|
+
before it ships.
|
|
282
|
+
|
|
283
|
+
## Report
|
|
284
|
+
|
|
285
|
+
State what is live, what remains draft, whether a rollback ran, and any owner action still
|
|
286
|
+
required (Visual Approval, a render-mode change, a secret). For a sync, list exact created,
|
|
287
|
+
updated, unchanged, orphaned and conflicted ids, the choices still open, and the rollback point;
|
|
288
|
+
an open conflict is never a successful sync. Do not say "launched" until the public response is
|
|
289
|
+
verified.
|