@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.
Files changed (40) hide show
  1. package/README.md +86 -86
  2. package/SKILL.md +160 -160
  3. package/dist/index.js +38 -9
  4. package/dist/index.js.map +1 -1
  5. package/dist/kit-catalog.json +1509 -1509
  6. package/dist/skills/bettercms/SKILL.md +206 -206
  7. package/dist/skills/bettercms/references/accessibility.md +171 -171
  8. package/dist/skills/bettercms/references/animation.md +296 -296
  9. package/dist/skills/bettercms/references/astro.md +148 -148
  10. package/dist/skills/bettercms/references/bindings.md +366 -366
  11. package/dist/skills/bettercms/references/componentize.md +185 -185
  12. package/dist/skills/bettercms/references/components.md +415 -415
  13. package/dist/skills/bettercms/references/content-experiments.md +62 -62
  14. package/dist/skills/bettercms/references/content-modeling.md +382 -382
  15. package/dist/skills/bettercms/references/detection.md +465 -465
  16. package/dist/skills/bettercms/references/dock-input.md +121 -121
  17. package/dist/skills/bettercms/references/editor-chrome.md +92 -92
  18. package/dist/skills/bettercms/references/external-content.md +78 -78
  19. package/dist/skills/bettercms/references/finish-site.md +377 -377
  20. package/dist/skills/bettercms/references/forms-and-interactions.md +245 -245
  21. package/dist/skills/bettercms/references/frameworks.md +159 -159
  22. package/dist/skills/bettercms/references/getting-started.md +156 -156
  23. package/dist/skills/bettercms/references/integrations.md +364 -364
  24. package/dist/skills/bettercms/references/kit-catalog.md +3259 -3259
  25. package/dist/skills/bettercms/references/kit-scenarios.md +712 -712
  26. package/dist/skills/bettercms/references/localization.md +85 -85
  27. package/dist/skills/bettercms/references/media-and-brand.md +181 -181
  28. package/dist/skills/bettercms/references/next.md +189 -189
  29. package/dist/skills/bettercms/references/other-frameworks.md +85 -85
  30. package/dist/skills/bettercms/references/portable-text.md +213 -213
  31. package/dist/skills/bettercms/references/programmatic-seo.md +98 -98
  32. package/dist/skills/bettercms/references/publishing.md +289 -289
  33. package/dist/skills/bettercms/references/seo.md +357 -357
  34. package/dist/skills/bettercms/references/shopify-headless.md +52 -52
  35. package/dist/skills/bettercms/references/site-blueprints.md +895 -895
  36. package/dist/skills/bettercms/references/site-review.md +74 -74
  37. package/dist/skills/bettercms/references/worked-examples-advanced.md +509 -509
  38. package/dist/skills/bettercms/references/worked-examples.md +885 -885
  39. package/dist/skills/bettercms/references/write-limits.md +217 -217
  40. package/package.json +1 -1
@@ -1,159 +1,159 @@
1
- ---
2
- title: "Framework wiring"
3
- description: "Hosting modes, the env map, TypeGen with bettercms-codegen, value shapes, revalidation tags, the media host, static snapshots and the verify steps shared by every framework."
4
- ---
5
-
6
- # Framework wiring, hosting modes and media
7
-
8
- Doctrine (what a headless or imported site needs first): §10 and §13 of
9
- `bettercms://playbook/schema`. This is the code side shared by both adapters. The repo root has
10
- `astro.config.*`: also read [astro.md](astro.md); `next.config.*`: [next.md](next.md); neither:
11
- [other-frameworks.md](other-frameworks.md) (`@bettercms-ai/sdk` `createClient`), and classify the
12
- codebase first ([bindings.md](bindings.md) §6). Read the adapter reference before writing code; env names differ between the two.
13
-
14
- ## Hosting modes
15
-
16
- A project's `hostingMode` is `managed`, `hybrid` or `headless`.
17
-
18
- | Mode | Who builds and serves production | Rule |
19
- |---|---|---|
20
- | managed | BetterCMS | `deploy_project` / `create_deploy_upload` are the deploy path |
21
- | hybrid | BetterCMS, and the client also reads content headlessly | Treat exactly like managed for builds and hosting |
22
- | headless | The client's own host (Vercel, Netlify, Cloudflare Pages) | Never call `deploy_project`, `deploy_to_host` or `create_deploy_upload`; never add `bcms-rebuild.yml`. Deploy = `npx @bettercms-ai/cli@latest deploy --to <host>`, or push to the repo the host builds. Done = `receipt.frontEnd` closed ([getting-started.md](getting-started.md)) |
23
-
24
- Headless refusals, by code:
25
-
26
- | Code | Cause | Fix |
27
- |---|---|---|
28
- | `HEADLESS_NO_BUILD` | A build, release or deploy was sent for a headless project | Stop; `bettercms deploy --to <host>` or push to the provider instead |
29
- | `HEADLESS_NEEDS_CUSTOM_HOST` | GitHub connect without a provider host | Add Vercel, Netlify or Cloudflare Pages under Custom hosting in the dashboard, then connect |
30
- | `HEADLESS_ARTIFACT_TARGET` | The custom host expects builds from BetterCMS | Switch the host to build from the repo |
31
-
32
- **Server rendering is one change across four systems, or none.** An SSR build (`output: 'server'`
33
- with `@astrojs/node`) emits `dist/client` + `dist/server` and no `index.html` at the root. A
34
- managed project serves that from its build directory only when the Hosting page is set to
35
- **Run as a server app** — a dashboard setting no tool can change. Until it is set, the server never
36
- runs: only prerendered pages and assets are served, and every other route answers 404 while the
37
- build, the release and `get_deploy_status` read green. The deploy workflow refuses an SSR build
38
- pointed to by `BCMS_BUILD_DIR` (`STATIC_HOST_GOT_SSR_ARTIFACT`) and warns when it packages one it
39
- found itself. So: switch Hosting first (ask the owner), then ship the build, then fetch the URL.
40
-
41
- ## Env map
42
-
43
- | Package | Variable | Purpose |
44
- |---|---|---|
45
- | `@bettercms-ai/astro` | `PUBLIC_BCMS_API_URL`, `PUBLIC_BCMS_WORKSPACE`, `PUBLIC_BCMS_PROJECT_ID` | Where reads go |
46
- | `@bettercms-ai/astro` | `PUBLIC_BCMS_MEDIA_URL`, `PUBLIC_BCMS_STUDIO_URL` | Media host, editor origin |
47
- | `@bettercms-ai/astro` | `BCMS_API_KEY`, `BCMS_DRAFT_SECRET` | Server-only key; draft cookie signing |
48
- | `@bettercms-ai/react` | `VITE_BCMS_API_URL`, `VITE_BCMS_WORKSPACE`, `VITE_BCMS_PROJECT_ID` | Where reads go (no key, no secret: a browser app) |
49
- | `@bettercms-ai/sveltekit` | `PUBLIC_BCMS_API_URL`, `PUBLIC_BCMS_WORKSPACE`, `PUBLIC_BCMS_PROJECT_ID`; `BCMS_DRAFT_SECRET` | Where reads go; draft cookie signing |
50
- | `@bettercms-ai/nuxt` | `NUXT_PUBLIC_BCMS_API_URL`, `NUXT_PUBLIC_BCMS_WORKSPACE`, `NUXT_PUBLIC_BCMS_PROJECT_ID`; `BCMS_DRAFT_SECRET` | Where reads go; draft cookie signing |
51
- | `@bettercms-ai/next` (snapshot bin) | `BETTERCMS_API_URL`, `BETTERCMS_API_KEY`, `BETTERCMS_WORKSPACE` | Pull `bcms-content.json` |
52
- | `@bettercms-ai/next` | `BETTERCMS_PREVIEW=1` | Read live instead of the snapshot in a preview build |
53
- | `@bettercms-ai/codegen` | `BETTERCMS_API_URL`, `BETTERCMS_API_KEY` | Fetch models for typegen |
54
- | `@bettercms-ai/mcp` | `BETTERCMS_API_URL` | A rung name `prod`, `dev`, `local`, or a full URL |
55
-
56
- Rule: Astro names start `PUBLIC_BCMS_`/`BCMS_`; Next and codegen names start `BETTERCMS_`. Never
57
- copy one adapter's `.env` into the other. Keys never go in a `PUBLIC_` variable.
58
-
59
- ## Types
60
-
61
- Run `bettercms-codegen` after every model or field change and commit the generated file. Why:
62
- templates typed against an old schema compile and then read `undefined` for a renamed field.
63
-
64
- 1. **Generate into the compiled tree:**
65
- `npx @bettercms-ai/codegen --out src/bettercms.generated.ts`. The default output is
66
- `bettercms.generated.ts` at the repo root, which a `tsconfig.json` whose `include` is `src/**`
67
- never type-checks. `--bindings-out <path>` also writes the Live Preview bindings module,
68
- `--components-out <path>` typed React `RichText`/`Image` components.
69
- 2. **Auth:** `BETTERCMS_API_KEY` (or `--key`) must be a management key; `BETTERCMS_API_URL` (or
70
- `--api-url`) picks the rung. The key is a secret: a CI secret or a local `.env` that git
71
- ignores, never a `PUBLIC_` variable.
72
- 3. **Commit, and let CI check.** Output is sorted and byte-stable, so a committed file diffs
73
- cleanly. In CI, run the same command, then `git diff --exit-code` on the file: a failure means
74
- the schema moved and nobody regenerated. Generating only in CI (not committed) also works, but
75
- then every local typecheck needs the key.
76
- 4. **Never edit the file by hand**; it says so in its header, and the next run overwrites it.
77
- 5. **Without a key** (an agent session, a quick check), `get_content_types` returns the same
78
- generated types over the MCP. Paste them only as a stopgap; the CLI is the repeatable path.
79
-
80
- ## Value shapes templates read
81
-
82
- - **Richtext**: `{ format, value, html }`. Render `value` (Portable Text) or `html`; write rules
83
- are in [portable-text.md](portable-text.md).
84
- - **Image**: an object `{ id, url, name, altText }`. Read `.url` and `.altText`, never treat the
85
- field as a string.
86
- - **Repeater / zoned array**: stored as `{ repeatable: [...] }`. Read `field.repeatable`, not
87
- `field` as an array. A bare array written to such a field is wrapped on write.
88
-
89
- ## Revalidation
90
-
91
- - On publish BetterCMS sends a webhook signed with `X-BetterCMS-Signature` (HMAC-SHA256 of the raw
92
- body, using the webhook secret configured on the project in the dashboard).
93
- - The backend invalidates tags `bcms:{workspaceSlug}:all` and `bcms:{workspaceSlug}:{type}:{slug}`.
94
- The SDK tags reads `bcms:{workspace}:{resource}:{id}`.
95
- - Rule: tag every cached read with those exact strings. A tag you invent (`post:${slug}`) is never
96
- revalidated and the page stays stale until the cache window expires.
97
-
98
- ## Media host
99
-
100
- A stored image value is `{ id, url, name, altText }`. `url` is absolute and already right for the
101
- plan: platform CDN, API origin, or the project's own CDN base. Rendering `url` as-is works on
102
- every plan.
103
-
104
- Resized images are a second lane. `@bettercms-ai/image-url` and `BcmsImage.astro` mint
105
- `<mediaUrl>/media/<id>?w=…`, and the backend serves that for every plan, including a project that
106
- keeps media in its own bucket.
107
-
108
- - Hand the builder the asset object or its `id`, never the `url` string. A URL with no `/media/`
109
- segment (own-bucket plan) resolves its last path segment as the id and 404s.
110
- - `mediaUrl` is the origin that owns the asset ids: `mediaBaseUrl` from `get_project`, always.
111
- The package default is right only when `mediaBaseUrl` is the production CDN; any other value —
112
- the API origin, another rung — goes in `PUBLIC_BCMS_MEDIA_URL`. On a backend too old to return
113
- `mediaBaseUrl`, read one `get_media` url: if it contains `/media/`, its origin is the value; if
114
- it does not (own bucket), use this project's API URL.
115
- - `set_media_delivery` changes future mints only, and only between the CDN and the API origin;
116
- stored URLs keep resolving. It is the owner's call. After it, set `PUBLIC_BCMS_MEDIA_URL` to the
117
- same origin.
118
- - A stored `url` that itself answers 404 is storage or the wrong rung, not the template. Report
119
- it; never rewrite URLs in content.
120
- - A media host from another project, a document or the conversation is never a value for
121
- `mediaUrl`.
122
-
123
- ## Static builds
124
-
125
- A static build reads content from the `bcms-content.json` snapshot. Produce the snapshot before
126
- the build, every build. Details per adapter are in the references.
127
-
128
- Producing it is half the job. A source file must READ it — `readPages`, `getEntry`, or the
129
- generated reader. A build that fetches the snapshot and never reads it still renders the
130
- hard-coded copy, so every edit saves, publishes and changes nothing on the page. Grep the source
131
- for a reader before calling a site wired.
132
-
133
- Read it through one of those, the converted-site helper (`bcms`, `bcmsRows`, `bcmsImage`) or
134
- `publishedEntry(slug)`. Never hand-write a parser of the file: each row holds its field values
135
- under `data` (`row.data.title`), so code that reads `row.title` gets `undefined` and the page shows
136
- its in-code copy after every publish. For an editable image call `bcmsImage(page, path, importedImage)`: the bare
137
- import with only a stamp never shows a published image on a route the app renders. An old
138
- `src/utils/bcms.ts` with `pageEntry()` is that parser: replace its reads with `publishedEntry`.
139
-
140
- Build with the repo's own `build` script (`pnpm build`, `bun run build`), never the framework CLI
141
- directly: a repo's script often adds a type check or a post-process step the CLI skips.
142
-
143
- ## Verify
144
-
145
- 1. Fetch the live URL, cache-busted, and confirm a 200 whose body contains copy you just
146
- published. Every other check below is a control-plane read and can be green on a dead site.
147
- 2. The site builds with the snapshot present and renders published content.
148
- 3. Publish a small change; the page updates (revalidation reached the right tag) or the host
149
- rebuilds (headless). Editable means that round trip was shown, not that fields exist.
150
- 4. `get_binding_report` for a changed route: `coverage.pending` empty. `refreshReason:
151
- never-generated` means no BetterCMS release wrote one — on a headless site it never will, so
152
- step 1 is the proof there. `outdated` means the report predates the current shape; the next
153
- release rewrites it.
154
- 5. **Smoke-test both render paths** on the dev server or a preview deploy: load a list page,
155
- click through to a detail page with the site's own link (never paste the URL), and read the
156
- browser console: clean, no `ReferenceError`, no hard reload to `/`. Then reload that detail
157
- page from the URL bar. Why: a client-side route transition loads lazy chunks that read env and
158
- the snapshot through a different code path than the server render, and that is where a
159
- missing public variable or a server-only import surfaces.
1
+ ---
2
+ title: "Framework wiring"
3
+ description: "Hosting modes, the env map, TypeGen with bettercms-codegen, value shapes, revalidation tags, the media host, static snapshots and the verify steps shared by every framework."
4
+ ---
5
+
6
+ # Framework wiring, hosting modes and media
7
+
8
+ Doctrine (what a headless or imported site needs first): §10 and §13 of
9
+ `bettercms://playbook/schema`. This is the code side shared by both adapters. The repo root has
10
+ `astro.config.*`: also read [astro.md](astro.md); `next.config.*`: [next.md](next.md); neither:
11
+ [other-frameworks.md](other-frameworks.md) (`@bettercms-ai/sdk` `createClient`), and classify the
12
+ codebase first ([bindings.md](bindings.md) §6). Read the adapter reference before writing code; env names differ between the two.
13
+
14
+ ## Hosting modes
15
+
16
+ A project's `hostingMode` is `managed`, `hybrid` or `headless`.
17
+
18
+ | Mode | Who builds and serves production | Rule |
19
+ |---|---|---|
20
+ | managed | BetterCMS | `deploy_project` / `create_deploy_upload` are the deploy path |
21
+ | hybrid | BetterCMS, and the client also reads content headlessly | Treat exactly like managed for builds and hosting |
22
+ | headless | The client's own host (Vercel, Netlify, Cloudflare Pages) | Never call `deploy_project`, `deploy_to_host` or `create_deploy_upload`; never add `bcms-rebuild.yml`. Deploy = `npx @bettercms-ai/cli@latest deploy --to <host>`, or push to the repo the host builds. Done = `receipt.frontEnd` closed ([getting-started.md](getting-started.md)) |
23
+
24
+ Headless refusals, by code:
25
+
26
+ | Code | Cause | Fix |
27
+ |---|---|---|
28
+ | `HEADLESS_NO_BUILD` | A build, release or deploy was sent for a headless project | Stop; `bettercms deploy --to <host>` or push to the provider instead |
29
+ | `HEADLESS_NEEDS_CUSTOM_HOST` | GitHub connect without a provider host | Add Vercel, Netlify or Cloudflare Pages under Custom hosting in the dashboard, then connect |
30
+ | `HEADLESS_ARTIFACT_TARGET` | The custom host expects builds from BetterCMS | Switch the host to build from the repo |
31
+
32
+ **Server rendering is one change across four systems, or none.** An SSR build (`output: 'server'`
33
+ with `@astrojs/node`) emits `dist/client` + `dist/server` and no `index.html` at the root. A
34
+ managed project serves that from its build directory only when the Hosting page is set to
35
+ **Run as a server app** — a dashboard setting no tool can change. Until it is set, the server never
36
+ runs: only prerendered pages and assets are served, and every other route answers 404 while the
37
+ build, the release and `get_deploy_status` read green. The deploy workflow refuses an SSR build
38
+ pointed to by `BCMS_BUILD_DIR` (`STATIC_HOST_GOT_SSR_ARTIFACT`) and warns when it packages one it
39
+ found itself. So: switch Hosting first (ask the owner), then ship the build, then fetch the URL.
40
+
41
+ ## Env map
42
+
43
+ | Package | Variable | Purpose |
44
+ |---|---|---|
45
+ | `@bettercms-ai/astro` | `PUBLIC_BCMS_API_URL`, `PUBLIC_BCMS_WORKSPACE`, `PUBLIC_BCMS_PROJECT_ID` | Where reads go |
46
+ | `@bettercms-ai/astro` | `PUBLIC_BCMS_MEDIA_URL`, `PUBLIC_BCMS_STUDIO_URL` | Media host, editor origin |
47
+ | `@bettercms-ai/astro` | `BCMS_API_KEY`, `BCMS_DRAFT_SECRET` | Server-only key; draft cookie signing |
48
+ | `@bettercms-ai/react` | `VITE_BCMS_API_URL`, `VITE_BCMS_WORKSPACE`, `VITE_BCMS_PROJECT_ID` | Where reads go (no key, no secret: a browser app) |
49
+ | `@bettercms-ai/sveltekit` | `PUBLIC_BCMS_API_URL`, `PUBLIC_BCMS_WORKSPACE`, `PUBLIC_BCMS_PROJECT_ID`; `BCMS_DRAFT_SECRET` | Where reads go; draft cookie signing |
50
+ | `@bettercms-ai/nuxt` | `NUXT_PUBLIC_BCMS_API_URL`, `NUXT_PUBLIC_BCMS_WORKSPACE`, `NUXT_PUBLIC_BCMS_PROJECT_ID`; `BCMS_DRAFT_SECRET` | Where reads go; draft cookie signing |
51
+ | `@bettercms-ai/next` (snapshot bin) | `BETTERCMS_API_URL`, `BETTERCMS_API_KEY`, `BETTERCMS_WORKSPACE` | Pull `bcms-content.json` |
52
+ | `@bettercms-ai/next` | `BETTERCMS_PREVIEW=1` | Read live instead of the snapshot in a preview build |
53
+ | `@bettercms-ai/codegen` | `BETTERCMS_API_URL`, `BETTERCMS_API_KEY` | Fetch models for typegen |
54
+ | `@bettercms-ai/mcp` | `BETTERCMS_API_URL` | A rung name `prod`, `dev`, `local`, or a full URL |
55
+
56
+ Rule: Astro names start `PUBLIC_BCMS_`/`BCMS_`; Next and codegen names start `BETTERCMS_`. Never
57
+ copy one adapter's `.env` into the other. Keys never go in a `PUBLIC_` variable.
58
+
59
+ ## Types
60
+
61
+ Run `bettercms-codegen` after every model or field change and commit the generated file. Why:
62
+ templates typed against an old schema compile and then read `undefined` for a renamed field.
63
+
64
+ 1. **Generate into the compiled tree:**
65
+ `npx @bettercms-ai/codegen --out src/bettercms.generated.ts`. The default output is
66
+ `bettercms.generated.ts` at the repo root, which a `tsconfig.json` whose `include` is `src/**`
67
+ never type-checks. `--bindings-out <path>` also writes the Live Preview bindings module,
68
+ `--components-out <path>` typed React `RichText`/`Image` components.
69
+ 2. **Auth:** `BETTERCMS_API_KEY` (or `--key`) must be a management key; `BETTERCMS_API_URL` (or
70
+ `--api-url`) picks the rung. The key is a secret: a CI secret or a local `.env` that git
71
+ ignores, never a `PUBLIC_` variable.
72
+ 3. **Commit, and let CI check.** Output is sorted and byte-stable, so a committed file diffs
73
+ cleanly. In CI, run the same command, then `git diff --exit-code` on the file: a failure means
74
+ the schema moved and nobody regenerated. Generating only in CI (not committed) also works, but
75
+ then every local typecheck needs the key.
76
+ 4. **Never edit the file by hand**; it says so in its header, and the next run overwrites it.
77
+ 5. **Without a key** (an agent session, a quick check), `get_content_types` returns the same
78
+ generated types over the MCP. Paste them only as a stopgap; the CLI is the repeatable path.
79
+
80
+ ## Value shapes templates read
81
+
82
+ - **Richtext**: `{ format, value, html }`. Render `value` (Portable Text) or `html`; write rules
83
+ are in [portable-text.md](portable-text.md).
84
+ - **Image**: an object `{ id, url, name, altText }`. Read `.url` and `.altText`, never treat the
85
+ field as a string.
86
+ - **Repeater / zoned array**: stored as `{ repeatable: [...] }`. Read `field.repeatable`, not
87
+ `field` as an array. A bare array written to such a field is wrapped on write.
88
+
89
+ ## Revalidation
90
+
91
+ - On publish BetterCMS sends a webhook signed with `X-BetterCMS-Signature` (HMAC-SHA256 of the raw
92
+ body, using the webhook secret configured on the project in the dashboard).
93
+ - The backend invalidates tags `bcms:{workspaceSlug}:all` and `bcms:{workspaceSlug}:{type}:{slug}`.
94
+ The SDK tags reads `bcms:{workspace}:{resource}:{id}`.
95
+ - Rule: tag every cached read with those exact strings. A tag you invent (`post:${slug}`) is never
96
+ revalidated and the page stays stale until the cache window expires.
97
+
98
+ ## Media host
99
+
100
+ A stored image value is `{ id, url, name, altText }`. `url` is absolute and already right for the
101
+ plan: platform CDN, API origin, or the project's own CDN base. Rendering `url` as-is works on
102
+ every plan.
103
+
104
+ Resized images are a second lane. `@bettercms-ai/image-url` and `BcmsImage.astro` mint
105
+ `<mediaUrl>/media/<id>?w=…`, and the backend serves that for every plan, including a project that
106
+ keeps media in its own bucket.
107
+
108
+ - Hand the builder the asset object or its `id`, never the `url` string. A URL with no `/media/`
109
+ segment (own-bucket plan) resolves its last path segment as the id and 404s.
110
+ - `mediaUrl` is the origin that owns the asset ids: `mediaBaseUrl` from `get_project`, always.
111
+ The package default is right only when `mediaBaseUrl` is the production CDN; any other value —
112
+ the API origin, another rung — goes in `PUBLIC_BCMS_MEDIA_URL`. On a backend too old to return
113
+ `mediaBaseUrl`, read one `get_media` url: if it contains `/media/`, its origin is the value; if
114
+ it does not (own bucket), use this project's API URL.
115
+ - `set_media_delivery` changes future mints only, and only between the CDN and the API origin;
116
+ stored URLs keep resolving. It is the owner's call. After it, set `PUBLIC_BCMS_MEDIA_URL` to the
117
+ same origin.
118
+ - A stored `url` that itself answers 404 is storage or the wrong rung, not the template. Report
119
+ it; never rewrite URLs in content.
120
+ - A media host from another project, a document or the conversation is never a value for
121
+ `mediaUrl`.
122
+
123
+ ## Static builds
124
+
125
+ A static build reads content from the `bcms-content.json` snapshot. Produce the snapshot before
126
+ the build, every build. Details per adapter are in the references.
127
+
128
+ Producing it is half the job. A source file must READ it — `readPages`, `getEntry`, or the
129
+ generated reader. A build that fetches the snapshot and never reads it still renders the
130
+ hard-coded copy, so every edit saves, publishes and changes nothing on the page. Grep the source
131
+ for a reader before calling a site wired.
132
+
133
+ Read it through one of those, the converted-site helper (`bcms`, `bcmsRows`, `bcmsImage`) or
134
+ `publishedEntry(slug)`. Never hand-write a parser of the file: each row holds its field values
135
+ under `data` (`row.data.title`), so code that reads `row.title` gets `undefined` and the page shows
136
+ its in-code copy after every publish. For an editable image call `bcmsImage(page, path, importedImage)`: the bare
137
+ import with only a stamp never shows a published image on a route the app renders. An old
138
+ `src/utils/bcms.ts` with `pageEntry()` is that parser: replace its reads with `publishedEntry`.
139
+
140
+ Build with the repo's own `build` script (`pnpm build`, `bun run build`), never the framework CLI
141
+ directly: a repo's script often adds a type check or a post-process step the CLI skips.
142
+
143
+ ## Verify
144
+
145
+ 1. Fetch the live URL, cache-busted, and confirm a 200 whose body contains copy you just
146
+ published. Every other check below is a control-plane read and can be green on a dead site.
147
+ 2. The site builds with the snapshot present and renders published content.
148
+ 3. Publish a small change; the page updates (revalidation reached the right tag) or the host
149
+ rebuilds (headless). Editable means that round trip was shown, not that fields exist.
150
+ 4. `get_binding_report` for a changed route: `coverage.pending` empty. `refreshReason:
151
+ never-generated` means no BetterCMS release wrote one — on a headless site it never will, so
152
+ step 1 is the proof there. `outdated` means the report predates the current shape; the next
153
+ release rewrites it.
154
+ 5. **Smoke-test both render paths** on the dev server or a preview deploy: load a list page,
155
+ click through to a detail page with the site's own link (never paste the URL), and read the
156
+ browser console: clean, no `ReferenceError`, no hard reload to `/`. Then reload that detail
157
+ page from the URL bar. Why: a client-side route transition loads lazy chunks that read env and
158
+ the snapshot through a different code path than the server render, and that is where a
159
+ missing public variable or a server-only import surfaces.