@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,189 +1,189 @@
1
- ---
2
- title: "Next.js adapter"
3
- description: "The @bettercms-ai/next client, draft and revalidation routes, rendering exports, pagination and 404 handling, and static export with the snapshot."
4
- ---
5
-
6
- # @bettercms-ai/next
7
-
8
- Read this when the repo has `next.config.*`. Shared rules (hosting modes, env map, tags, value
9
- shapes) are in [frameworks.md](frameworks.md).
10
-
11
- ## Client
12
-
13
- `createBetterCMS<Schema>(config)` from `@bettercms-ai/next`, typed by the file
14
- `bettercms-codegen` writes. Config: `workspace` (slug), `apiKey` (delivery key), `revalidate`
15
- (ISR seconds, or `false`). Keep the key server-side: `BETTERCMS_API_KEY`, never
16
- `NEXT_PUBLIC_*`.
17
-
18
- ## Draft preview (App Router, mounted by you)
19
-
20
- Nothing is mounted automatically. Add the routes:
21
-
22
- - `createDraftModeRoute({ apiUrl })` → export `GET` from the route your project's preview link
23
- points at. It reads `?token=&redirect=`, has the backend validate the token, enables Next draft
24
- mode, stores the token in the httpOnly `bcms-preview-token` cookie and redirects.
25
- - `createDisableDraftRoute()` → export `GET` from an exit route.
26
- - Read drafts with `getEntry(slug, { preview: true, previewToken })`. `preview: true` without a
27
- token throws.
28
-
29
- ## Revalidation route
30
-
31
- `createRevalidateRoute()` → export `POST`. It verifies `X-BetterCMS-Signature` against the raw
32
- body with `secret`, else `BCMS_REVALIDATE_SECRET` (the project's revalidation secret from the
33
- dashboard), then revalidates every tag (`revalidateTag(tag, "max")`) and every page path
34
- (`revalidatePath`) the publish changed. Tag your reads with the backend's tag strings
35
- ([frameworks.md](frameworks.md) Revalidation).
36
-
37
- ## Rendering
38
-
39
- | Export | Use |
40
- |---|---|
41
- | `BcmsBlocks` | Render a block tree; pass `components` from `readComponents()`. A block outside its `window` is not rendered, read against `now` (default: render time); `showScheduled` draws it marked `data-bcms-window`, for a draft preview |
42
- | `readComponents()` | Component definitions from `bcms-content.json` |
43
- | `BcmsField` | `path`, `kind` (`richtext`/`image`), `as`; server-safe; no wrapper around a single DOM element ([animation.md](animation.md) §10) |
44
- | `BcmsButton` | A `button` field: `path`, `value`, `className`; one `<a>` with `data-variant` / `data-size`. Style only the looks the site has ([components.md](components.md) §5); server-safe |
45
- | `BcmsSections`, `BcmsSection` (from `@bettercms-ai/next/sections`) | The page's own sections, drawn in the CMS page's order (below); a server component |
46
- | `sectionsFor(slug)`, `sectionsStaticProps(slug, own?)`, `sectionsServerSideProps(slug, own)` | The block list, components and forms `BcmsSections` takes, from the snapshot |
47
- | `sectionRootAttrs(block)` | Spread on a section root your own code renders: `data-bcms-block`, `data-bcms-style` and the block's style, plus `data-bcms-hide` when `style.hideOn` is set: ship `RESPONSIVE_CSS` (exported here) in your global CSS ([components.md](components.md) §5) |
48
- | `bind`, `bindProps` | Binding attributes; instance-aware inside components |
49
- | `BcmsForm` + `getForm(idOrName)` | Forms from the snapshot ([forms-and-interactions.md](forms-and-interactions.md)) |
50
- | `buildMetadata(page, siteDefaults, location?)` | Next `Metadata` from native SEO fields |
51
- | `llmsTxtRoute(config)` | An `llms.txt` route handler |
52
- | `imageUrlBuilder({ baseUrl })` | Resize URLs; same media-host rules as Astro |
53
- | `BcmsImage` | An image field as one `<img>`: `path`, `src` (the value), `sizes` or `width`; declares the field and keeps the value's alt, crop and focal point; server-safe |
54
-
55
- Rules:
56
-
57
- - Binding attributes are always rendered; they are inert for visitors. Do not gate them behind
58
- draft mode.
59
- - Pass `location` `{ siteUrl, path }` to `buildMetadata`. Without it the site URL comes from
60
- `BETTERCMS_SITE_URL` and the path from the page slug; an app that serves pages under a prefix
61
- (`/docs/[slug]`) must pass `location.path` or its canonical is wrong.
62
- - The package ships no sitemap or robots helper. Write `app/sitemap.ts` and `app/robots.ts`
63
- yourself from published content, and leave out pages marked noindex.
64
-
65
- ## Sections in the CMS order
66
-
67
- A page whose sections are written in its own file follows the CMS page's block list through
68
- `<BcmsSections>`. `bettercms init` writes this wrap (`npx @bettercms-ai/convert --sections`);
69
- write it by hand for a page the codemod refused ([componentize.md](componentize.md) §5).
70
-
71
- App Router, in a server component:
72
-
73
- ```tsx
74
- import { sectionsFor } from "@bettercms-ai/next";
75
- import { BcmsSections, BcmsSection } from "@bettercms-ai/next/sections";
76
-
77
- export default function Home() {
78
- return (
79
- <main>
80
- <BcmsSections {...sectionsFor("home")}>
81
- <BcmsSection bind="hero">{(root) => <Hero rootProps={root} />}</BcmsSection>
82
- <BcmsSection bind="code:sec1"><LogoCloud /></BcmsSection>
83
- <BcmsSection bind="features"><section className="features">…</section></BcmsSection>
84
- </BcmsSections>
85
- </main>
86
- );
87
- }
88
- ```
89
-
90
- Pages Router: the page also renders in the browser, so the list is a page prop.
91
-
92
- ```tsx
93
- import { sectionsStaticProps, type SectionsData } from "@bettercms-ai/next";
94
- import { BcmsSections, BcmsSection } from "@bettercms-ai/next/sections";
95
-
96
- export default function Home({ bcmsSections }: { bcmsSections: SectionsData }) {
97
- return <BcmsSections {...bcmsSections}>…</BcmsSections>;
98
- }
99
- export const getStaticProps = sectionsStaticProps("home");
100
- ```
101
-
102
- A page with its own data function passes it in, and the list joins the props it answers:
103
- `export const getStaticProps = sectionsStaticProps("home", pageStaticProps)`, or
104
- `sectionsServerSideProps` for `getServerSideProps`. `--sections` writes this for you.
105
-
106
- A page that only returns a view (`return <HomeView locale="en" />`) has its sections in the
107
- view's file, and `--sections` wraps them there: it follows the import (relative, a `paths` alias
108
- of the repository's tsconfig or jsconfig, an index file or a barrel's re-export) up to three
109
- components down. One view often serves several pages (`/` and `/fr`), so the page reads its own
110
- list and hands it over, and the view takes it as an optional prop:
111
-
112
- ```tsx
113
- // app/fr/page.tsx
114
- export default function Page() {
115
- return <HomeView locale="fr" bcmsSections={sectionsFor("fr")} />;
116
- }
117
-
118
- // views/home-view.tsx: a server or a "use client" component, it only names the type
119
- export default function HomeView({ bcmsSections, locale }: Props & { bcmsSections?: SectionsData }) {
120
- return (
121
- <main>
122
- <BcmsSections {...(bcmsSections ?? { page: "" })}>
123
- <BcmsSection bind="hero"><Hero locale={locale} /></BcmsSection>
124
- …
125
- </BcmsSections>
126
- </main>
127
- );
128
- }
129
- ```
130
-
131
- A render that was handed no list (a route the CMS has no page for) keeps the source order. In the
132
- Pages Router the page passes on the prop it got: `<HomeView bcmsSections={bcmsSections} />`.
133
-
134
- - A bind is the family of the section's first bound field (`features-title` → `features`): the
135
- `bind` on the page's `component` block. A block bound to a section renders it at the block's
136
- position; a section added in the editor (no bind) renders through `BcmsBlocks`.
137
- - The stamp goes on the section's own root. One DOM element (`<section>`) gets
138
- `data-bcms-block`, `data-bcms-style` and the block's style cloned onto it. A COMPONENT child
139
- cannot be handed unknown props, so write it as a function and spread `root` on the element the
140
- component renders (`<section {...rootProps}>`). Without the function it is stamped on a box:
141
- `display:contents` while its block has no style, one `display:flow-root` box carrying the
142
- style once it has ([animation.md](animation.md) §10).
143
- - A section no block names: `code:<bind>` always renders where it is written. A bare `<bind>`
144
- does too until the list names one of the page's sections; from then on it is a removed one.
145
- Write a bind bare only when the page already places it (`get_page`), otherwise `code:`.
146
- - `blocks` null (no snapshot) renders every child in source order, untouched.
147
- - Never inside a `"use client"` file: it reads its children's own elements, which a client
148
- component cannot do across the server boundary.
149
-
150
- ## Lists, pagination and missing content
151
-
152
- - `listEntries(model, { page, perPage })`: `page` is 1-based; `perPage` defaults to 20 and the
153
- API caps it at 100. The result carries `items`, `totalItems`, `totalPages`, `hasNextPage` and
154
- `hasPreviousPage`: drive the pager from those, never from `items.length`. Pass `select` on list
155
- pages so each card fetches only the keys it renders.
156
- - A page route (`/blog/page/[n]`) answers `notFound()` when `n` is past `totalPages`; page 1 of an
157
- empty collection is a 200 with an empty state, not a 404. Prerender pages 1…`totalPages` from
158
- `generateStaticParams`.
159
- - `getEntry(slug)` returns `null` on a 404: call `notFound()` on `null`, never render an empty
160
- shell with a 200. Why: an empty 200 is indexed as a thin page, and a typo'd slug never reads as
161
- missing.
162
- - `listEntries` THROWS `BetterCMSError` on a 404 (an unknown workspace or model), on purpose: an
163
- empty list there would hide a wrong model slug. Let it fail the build; do not catch it into `[]`.
164
-
165
- ## Static export (`output: "export"`)
166
-
167
- - Pull the content before `next build`, every build: `"build": "bettercms pull-content && next build"`
168
- (what `bettercms init` writes; keyless, it reads the project's published content).
169
- `bettercms-snapshot` (from `BETTERCMS_API_URL`, `BETTERCMS_API_KEY`, `BETTERCMS_WORKSPACE`) does the
170
- same with a key. Either writes `bcms-content.json`, and `createBetterCMS()` with no arguments reads
171
- its workspace and project from it (after `NEXT_PUBLIC_BCMS_*`).
172
- - Without the snapshot the reads throw `SNAPSHOT_MISSING`. Why it throws instead of fetching:
173
- a live fetch would turn pages into dynamic routes, and a static export silently leaves them out
174
- of `out/`.
175
- - A preview build that must read live sets `BETTERCMS_PREVIEW=1`.
176
- - On a BetterCMS build the platform's own `bcms-content.json` is already in the project root.
177
- `bettercms-snapshot` keeps it and `getContent` reads it: one file feeds `getContent`, `readPages`,
178
- `readForms` and `readComponents`.
179
- - Draft mode and the revalidation route need a server; a static export has neither. Content
180
- changes reach a static site only by rebuilding it.
181
-
182
- ## Traps
183
-
184
- | Symptom | Cause | Rule |
185
- |---|---|---|
186
- | Empty `out/` after `next build` | No snapshot | Run `bettercms pull-content` first |
187
- | Page never updates after publish | Read tags do not match backend tags | Use `bcms:{workspace}:…` tags |
188
- | Component instance renders nothing | `components` not passed | `readComponents()` into `BcmsBlocks` |
189
- | Canonical missing or wrong only on the self-hosted site | `buildMetadata` without `location`, and no `BETTERCMS_SITE_URL` or a prefixed route | Pass `{ siteUrl, path }` |
1
+ ---
2
+ title: "Next.js adapter"
3
+ description: "The @bettercms-ai/next client, draft and revalidation routes, rendering exports, pagination and 404 handling, and static export with the snapshot."
4
+ ---
5
+
6
+ # @bettercms-ai/next
7
+
8
+ Read this when the repo has `next.config.*`. Shared rules (hosting modes, env map, tags, value
9
+ shapes) are in [frameworks.md](frameworks.md).
10
+
11
+ ## Client
12
+
13
+ `createBetterCMS<Schema>(config)` from `@bettercms-ai/next`, typed by the file
14
+ `bettercms-codegen` writes. Config: `workspace` (slug), `apiKey` (delivery key), `revalidate`
15
+ (ISR seconds, or `false`). Keep the key server-side: `BETTERCMS_API_KEY`, never
16
+ `NEXT_PUBLIC_*`.
17
+
18
+ ## Draft preview (App Router, mounted by you)
19
+
20
+ Nothing is mounted automatically. Add the routes:
21
+
22
+ - `createDraftModeRoute({ apiUrl })` → export `GET` from the route your project's preview link
23
+ points at. It reads `?token=&redirect=`, has the backend validate the token, enables Next draft
24
+ mode, stores the token in the httpOnly `bcms-preview-token` cookie and redirects.
25
+ - `createDisableDraftRoute()` → export `GET` from an exit route.
26
+ - Read drafts with `getEntry(slug, { preview: true, previewToken })`. `preview: true` without a
27
+ token throws.
28
+
29
+ ## Revalidation route
30
+
31
+ `createRevalidateRoute()` → export `POST`. It verifies `X-BetterCMS-Signature` against the raw
32
+ body with `secret`, else `BCMS_REVALIDATE_SECRET` (the project's revalidation secret from the
33
+ dashboard), then revalidates every tag (`revalidateTag(tag, "max")`) and every page path
34
+ (`revalidatePath`) the publish changed. Tag your reads with the backend's tag strings
35
+ ([frameworks.md](frameworks.md) Revalidation).
36
+
37
+ ## Rendering
38
+
39
+ | Export | Use |
40
+ |---|---|
41
+ | `BcmsBlocks` | Render a block tree; pass `components` from `readComponents()`. A block outside its `window` is not rendered, read against `now` (default: render time); `showScheduled` draws it marked `data-bcms-window`, for a draft preview |
42
+ | `readComponents()` | Component definitions from `bcms-content.json` |
43
+ | `BcmsField` | `path`, `kind` (`richtext`/`image`), `as`; server-safe; no wrapper around a single DOM element ([animation.md](animation.md) §10) |
44
+ | `BcmsButton` | A `button` field: `path`, `value`, `className`; one `<a>` with `data-variant` / `data-size`. Style only the looks the site has ([components.md](components.md) §5); server-safe |
45
+ | `BcmsSections`, `BcmsSection` (from `@bettercms-ai/next/sections`) | The page's own sections, drawn in the CMS page's order (below); a server component |
46
+ | `sectionsFor(slug)`, `sectionsStaticProps(slug, own?)`, `sectionsServerSideProps(slug, own)` | The block list, components and forms `BcmsSections` takes, from the snapshot |
47
+ | `sectionRootAttrs(block)` | Spread on a section root your own code renders: `data-bcms-block`, `data-bcms-style` and the block's style, plus `data-bcms-hide` when `style.hideOn` is set: ship `RESPONSIVE_CSS` (exported here) in your global CSS ([components.md](components.md) §5) |
48
+ | `bind`, `bindProps` | Binding attributes; instance-aware inside components |
49
+ | `BcmsForm` + `getForm(idOrName)` | Forms from the snapshot ([forms-and-interactions.md](forms-and-interactions.md)) |
50
+ | `buildMetadata(page, siteDefaults, location?)` | Next `Metadata` from native SEO fields |
51
+ | `llmsTxtRoute(config)` | An `llms.txt` route handler |
52
+ | `imageUrlBuilder({ baseUrl })` | Resize URLs; same media-host rules as Astro |
53
+ | `BcmsImage` | An image field as one `<img>`: `path`, `src` (the value), `sizes` or `width`; declares the field and keeps the value's alt, crop and focal point; server-safe |
54
+
55
+ Rules:
56
+
57
+ - Binding attributes are always rendered; they are inert for visitors. Do not gate them behind
58
+ draft mode.
59
+ - Pass `location` `{ siteUrl, path }` to `buildMetadata`. Without it the site URL comes from
60
+ `BETTERCMS_SITE_URL` and the path from the page slug; an app that serves pages under a prefix
61
+ (`/docs/[slug]`) must pass `location.path` or its canonical is wrong.
62
+ - The package ships no sitemap or robots helper. Write `app/sitemap.ts` and `app/robots.ts`
63
+ yourself from published content, and leave out pages marked noindex.
64
+
65
+ ## Sections in the CMS order
66
+
67
+ A page whose sections are written in its own file follows the CMS page's block list through
68
+ `<BcmsSections>`. `bettercms init` writes this wrap (`npx @bettercms-ai/convert --sections`);
69
+ write it by hand for a page the codemod refused ([componentize.md](componentize.md) §5).
70
+
71
+ App Router, in a server component:
72
+
73
+ ```tsx
74
+ import { sectionsFor } from "@bettercms-ai/next";
75
+ import { BcmsSections, BcmsSection } from "@bettercms-ai/next/sections";
76
+
77
+ export default function Home() {
78
+ return (
79
+ <main>
80
+ <BcmsSections {...sectionsFor("home")}>
81
+ <BcmsSection bind="hero">{(root) => <Hero rootProps={root} />}</BcmsSection>
82
+ <BcmsSection bind="code:sec1"><LogoCloud /></BcmsSection>
83
+ <BcmsSection bind="features"><section className="features">…</section></BcmsSection>
84
+ </BcmsSections>
85
+ </main>
86
+ );
87
+ }
88
+ ```
89
+
90
+ Pages Router: the page also renders in the browser, so the list is a page prop.
91
+
92
+ ```tsx
93
+ import { sectionsStaticProps, type SectionsData } from "@bettercms-ai/next";
94
+ import { BcmsSections, BcmsSection } from "@bettercms-ai/next/sections";
95
+
96
+ export default function Home({ bcmsSections }: { bcmsSections: SectionsData }) {
97
+ return <BcmsSections {...bcmsSections}>…</BcmsSections>;
98
+ }
99
+ export const getStaticProps = sectionsStaticProps("home");
100
+ ```
101
+
102
+ A page with its own data function passes it in, and the list joins the props it answers:
103
+ `export const getStaticProps = sectionsStaticProps("home", pageStaticProps)`, or
104
+ `sectionsServerSideProps` for `getServerSideProps`. `--sections` writes this for you.
105
+
106
+ A page that only returns a view (`return <HomeView locale="en" />`) has its sections in the
107
+ view's file, and `--sections` wraps them there: it follows the import (relative, a `paths` alias
108
+ of the repository's tsconfig or jsconfig, an index file or a barrel's re-export) up to three
109
+ components down. One view often serves several pages (`/` and `/fr`), so the page reads its own
110
+ list and hands it over, and the view takes it as an optional prop:
111
+
112
+ ```tsx
113
+ // app/fr/page.tsx
114
+ export default function Page() {
115
+ return <HomeView locale="fr" bcmsSections={sectionsFor("fr")} />;
116
+ }
117
+
118
+ // views/home-view.tsx: a server or a "use client" component, it only names the type
119
+ export default function HomeView({ bcmsSections, locale }: Props & { bcmsSections?: SectionsData }) {
120
+ return (
121
+ <main>
122
+ <BcmsSections {...(bcmsSections ?? { page: "" })}>
123
+ <BcmsSection bind="hero"><Hero locale={locale} /></BcmsSection>
124
+ …
125
+ </BcmsSections>
126
+ </main>
127
+ );
128
+ }
129
+ ```
130
+
131
+ A render that was handed no list (a route the CMS has no page for) keeps the source order. In the
132
+ Pages Router the page passes on the prop it got: `<HomeView bcmsSections={bcmsSections} />`.
133
+
134
+ - A bind is the family of the section's first bound field (`features-title` → `features`): the
135
+ `bind` on the page's `component` block. A block bound to a section renders it at the block's
136
+ position; a section added in the editor (no bind) renders through `BcmsBlocks`.
137
+ - The stamp goes on the section's own root. One DOM element (`<section>`) gets
138
+ `data-bcms-block`, `data-bcms-style` and the block's style cloned onto it. A COMPONENT child
139
+ cannot be handed unknown props, so write it as a function and spread `root` on the element the
140
+ component renders (`<section {...rootProps}>`). Without the function it is stamped on a box:
141
+ `display:contents` while its block has no style, one `display:flow-root` box carrying the
142
+ style once it has ([animation.md](animation.md) §10).
143
+ - A section no block names: `code:<bind>` always renders where it is written. A bare `<bind>`
144
+ does too until the list names one of the page's sections; from then on it is a removed one.
145
+ Write a bind bare only when the page already places it (`get_page`), otherwise `code:`.
146
+ - `blocks` null (no snapshot) renders every child in source order, untouched.
147
+ - Never inside a `"use client"` file: it reads its children's own elements, which a client
148
+ component cannot do across the server boundary.
149
+
150
+ ## Lists, pagination and missing content
151
+
152
+ - `listEntries(model, { page, perPage })`: `page` is 1-based; `perPage` defaults to 20 and the
153
+ API caps it at 100. The result carries `items`, `totalItems`, `totalPages`, `hasNextPage` and
154
+ `hasPreviousPage`: drive the pager from those, never from `items.length`. Pass `select` on list
155
+ pages so each card fetches only the keys it renders.
156
+ - A page route (`/blog/page/[n]`) answers `notFound()` when `n` is past `totalPages`; page 1 of an
157
+ empty collection is a 200 with an empty state, not a 404. Prerender pages 1…`totalPages` from
158
+ `generateStaticParams`.
159
+ - `getEntry(slug)` returns `null` on a 404: call `notFound()` on `null`, never render an empty
160
+ shell with a 200. Why: an empty 200 is indexed as a thin page, and a typo'd slug never reads as
161
+ missing.
162
+ - `listEntries` THROWS `BetterCMSError` on a 404 (an unknown workspace or model), on purpose: an
163
+ empty list there would hide a wrong model slug. Let it fail the build; do not catch it into `[]`.
164
+
165
+ ## Static export (`output: "export"`)
166
+
167
+ - Pull the content before `next build`, every build: `"build": "bettercms pull-content && next build"`
168
+ (what `bettercms init` writes; keyless, it reads the project's published content).
169
+ `bettercms-snapshot` (from `BETTERCMS_API_URL`, `BETTERCMS_API_KEY`, `BETTERCMS_WORKSPACE`) does the
170
+ same with a key. Either writes `bcms-content.json`, and `createBetterCMS()` with no arguments reads
171
+ its workspace and project from it (after `NEXT_PUBLIC_BCMS_*`).
172
+ - Without the snapshot the reads throw `SNAPSHOT_MISSING`. Why it throws instead of fetching:
173
+ a live fetch would turn pages into dynamic routes, and a static export silently leaves them out
174
+ of `out/`.
175
+ - A preview build that must read live sets `BETTERCMS_PREVIEW=1`.
176
+ - On a BetterCMS build the platform's own `bcms-content.json` is already in the project root.
177
+ `bettercms-snapshot` keeps it and `getContent` reads it: one file feeds `getContent`, `readPages`,
178
+ `readForms` and `readComponents`.
179
+ - Draft mode and the revalidation route need a server; a static export has neither. Content
180
+ changes reach a static site only by rebuilding it.
181
+
182
+ ## Traps
183
+
184
+ | Symptom | Cause | Rule |
185
+ |---|---|---|
186
+ | Empty `out/` after `next build` | No snapshot | Run `bettercms pull-content` first |
187
+ | Page never updates after publish | Read tags do not match backend tags | Use `bcms:{workspace}:…` tags |
188
+ | Component instance renders nothing | `components` not passed | `readComponents()` into `BcmsBlocks` |
189
+ | Canonical missing or wrong only on the self-hosted site | `buildMetadata` without `location`, and no `BETTERCMS_SITE_URL` or a prefixed route | Pass `{ siteUrl, path }` |
@@ -1,85 +1,85 @@
1
- ---
2
- title: "Other frameworks"
3
- description: "Reading BetterCMS content from Nuxt, SvelteKit, React Router or Vite through the SDK createClient read core: options, 404 behaviour, env names, and what only the Astro and Next adapters provide."
4
- ---
5
-
6
- # Other frameworks
7
-
8
- Read this when the repo has neither `astro.config.*` nor `next.config.*`. React (Vite), SvelteKit
9
- and Nuxt have adapters (§0); anything else reads through the framework-agnostic core in
10
- `@bettercms-ai/sdk`, the same one every adapter wraps. Shared rules (hosting modes, value shapes,
11
- media host, revalidation tags) are in [frameworks.md](frameworks.md).
12
-
13
- ## 0. The adapters
14
-
15
- | Repo has | Install | Settings | Sections | Drafts |
16
- |---|---|---|---|---|
17
- | `vite.config.*` + React | `@bettercms-ai/react` | `<BcmsProvider env={import.meta.env}>`, `VITE_BCMS_*` | `<BcmsBlocks blocks={page.blocks}>` (Next's markup) | the editor's `/api/draft` link, no server; the host rewrites it to the app |
18
- | `svelte.config.*` (SvelteKit) | `@bettercms-ai/sveltekit` | `createBcms({ env: { ...publicEnv, ...privateEnv } })`, `PUBLIC_BCMS_*` | `bcms.sections(slug, fetch)` in `load`, then `<div use:bcmsSections={data.sections}>{@html sectionsMarkup(data.sections)}</div>` | `src/routes/api/draft/+server.ts`: `GET = ({ request }) => bcms.draftEnable(request)`; `await bcms.client(request)` reads drafts |
19
- | `nuxt.config.*` | `@bettercms-ai/nuxt` | `createBcms({ env: process.env })` in `server/utils`, `NUXT_PUBLIC_BCMS_*` | a server route returning `bcms.sections(slug)`, then `<div v-bcms-sections v-html="sectionsMarkup(data)">` | `server/routes/api/draft.get.ts`: `bcms.draftEnable(toWebRequest(e))` |
20
-
21
- - SvelteKit and Nuxt sections are server-rendered by BetterCMS (the hosted renderer's markup, a
22
- stylesheet scoped to `[data-bcms-rendered="platform"]`, and the form, slider and tab scripts):
23
- never rebuild them as components, and never paste their HTML into the repo. Field copy outside
24
- the sections is still read with the client and bound with `data-bcms-field`
25
- ([bindings.md](bindings.md)).
26
- - The sections are the PUBLISHED page; draft mode switches the client, not the fragment.
27
- - Set `BCMS_PROJECT_ID` under the framework's prefix: sections and keyless reads are addressed to it.
28
- - `BCMS_DRAFT_SECRET` (server-only) signs the draft cookie on SvelteKit and Nuxt; a React SPA has none.
29
-
30
- ## 1. The client
31
-
32
- ```ts
33
- import { createClient } from "@bettercms-ai/sdk";
34
-
35
- export const bcms = createClient({
36
- apiUrl: process.env.BCMS_API_URL!, // the backend origin; the SDK adds /api/v1/delivery/<workspace>
37
- workspace: process.env.BCMS_WORKSPACE!,
38
- projectId: process.env.BCMS_PROJECT_ID, // scopes listForms
39
- });
40
- ```
41
-
42
- Reading published content needs no key. The platform sets `BCMS_API_URL`, `BCMS_WORKSPACE` and
43
- `BCMS_PROJECT_ID` on every build and running server app, and again under each framework's public
44
- prefix (`NUXT_PUBLIC_`, `PUBLIC_`, `VITE_`…); the table is in
45
- [external-content.md](external-content.md). On a site BetterCMS builds, never add them to `.env`.
46
- A headless front end on its own host sets them itself: in `.env` locally, and on the host by
47
- `npx @bettercms-ai/cli@latest deploy --to <host>`.
48
-
49
- | Option | Use |
50
- |---|---|
51
- | `perspective: "drafts"` | Draft reads; needs a `content:read:draft` key or a `previewToken`. Stega is on by default there ([bindings.md](bindings.md) §5) |
52
- | `cache: true` | Collapse identical GETs for the client's life. Only for a static build (one point-in-time dataset); never in dev, SSR or drafts |
53
- | `timeout` | Per-attempt ms, default 10000 |
54
-
55
- ## 2. Methods and their 404s
56
-
57
- | Method | Returns | On a missing slug |
58
- |---|---|---|
59
- | `getPage(slug)` | the page (`blocks`, meta columns) | `null`, after scanning the page list |
60
- | `getEntry(slug, { depth, select })` | one entry | throws `BetterCMSError` with `status` 404 |
61
- | `listEntries({ model, page, perPage, depth, select })` | `{ items, page, perPage, totalItems, totalPages, hasNextPage, hasPreviousPage }` | a list 404 (unknown model or workspace) throws |
62
- | `listPages`, `listComponents`, `getLayout`, `listForms` | pages, component definitions, the Global Layout (`null` if none), forms (render only `status: "published"`) | |
63
-
64
- Catch the 404 on `getEntry` and answer the framework's not-found (a 404 status, never an empty
65
- 200). Let a list 404 fail the build. Drive pagers from `totalPages`/`hasNextPage`.
66
-
67
- ## 3. Where to call it
68
-
69
- Call the client only in code that runs on the server or at build time, so a key (drafts) never
70
- reaches the bundle: a Nuxt server route or `useAsyncData` on the server, a SvelteKit
71
- `+page.server.ts` `load`, a React Router `loader`. A browser-only Vite SPA reads published content
72
- with the public-prefixed variables and no key.
73
-
74
- ## 4. What only the adapters give you
75
-
76
- No `BcmsBlocks`/`BcmsField` components, no `bind`/`bindProps` helpers, no draft routes and no
77
- snapshot bin exist for these frameworks. So:
78
-
79
- - Render component blocks yourself from `listComponents()` definitions, and write binding
80
- attributes by hand per [bindings.md](bindings.md) (`data-bcms-field`, `data-bcms-block`).
81
- - Rich text: read it through the SDK's `plain`, `rich` or `portableText` helpers, which accept a
82
- bare string or the `{ format, value, html }` envelope.
83
- - Images: `imageUrlBuilder` is re-exported from the SDK ([media-and-brand.md](media-and-brand.md)).
84
- - Meta: `resolveSeo` merges page over site defaults; write the result into `<head>` yourself.
85
- - Verify with the two-path smoke test in [frameworks.md](frameworks.md) Verify.
1
+ ---
2
+ title: "Other frameworks"
3
+ description: "Reading BetterCMS content from Nuxt, SvelteKit, React Router or Vite through the SDK createClient read core: options, 404 behaviour, env names, and what only the Astro and Next adapters provide."
4
+ ---
5
+
6
+ # Other frameworks
7
+
8
+ Read this when the repo has neither `astro.config.*` nor `next.config.*`. React (Vite), SvelteKit
9
+ and Nuxt have adapters (§0); anything else reads through the framework-agnostic core in
10
+ `@bettercms-ai/sdk`, the same one every adapter wraps. Shared rules (hosting modes, value shapes,
11
+ media host, revalidation tags) are in [frameworks.md](frameworks.md).
12
+
13
+ ## 0. The adapters
14
+
15
+ | Repo has | Install | Settings | Sections | Drafts |
16
+ |---|---|---|---|---|
17
+ | `vite.config.*` + React | `@bettercms-ai/react` | `<BcmsProvider env={import.meta.env}>`, `VITE_BCMS_*` | `<BcmsBlocks blocks={page.blocks}>` (Next's markup) | the editor's `/api/draft` link, no server; the host rewrites it to the app |
18
+ | `svelte.config.*` (SvelteKit) | `@bettercms-ai/sveltekit` | `createBcms({ env: { ...publicEnv, ...privateEnv } })`, `PUBLIC_BCMS_*` | `bcms.sections(slug, fetch)` in `load`, then `<div use:bcmsSections={data.sections}>{@html sectionsMarkup(data.sections)}</div>` | `src/routes/api/draft/+server.ts`: `GET = ({ request }) => bcms.draftEnable(request)`; `await bcms.client(request)` reads drafts |
19
+ | `nuxt.config.*` | `@bettercms-ai/nuxt` | `createBcms({ env: process.env })` in `server/utils`, `NUXT_PUBLIC_BCMS_*` | a server route returning `bcms.sections(slug)`, then `<div v-bcms-sections v-html="sectionsMarkup(data)">` | `server/routes/api/draft.get.ts`: `bcms.draftEnable(toWebRequest(e))` |
20
+
21
+ - SvelteKit and Nuxt sections are server-rendered by BetterCMS (the hosted renderer's markup, a
22
+ stylesheet scoped to `[data-bcms-rendered="platform"]`, and the form, slider and tab scripts):
23
+ never rebuild them as components, and never paste their HTML into the repo. Field copy outside
24
+ the sections is still read with the client and bound with `data-bcms-field`
25
+ ([bindings.md](bindings.md)).
26
+ - The sections are the PUBLISHED page; draft mode switches the client, not the fragment.
27
+ - Set `BCMS_PROJECT_ID` under the framework's prefix: sections and keyless reads are addressed to it.
28
+ - `BCMS_DRAFT_SECRET` (server-only) signs the draft cookie on SvelteKit and Nuxt; a React SPA has none.
29
+
30
+ ## 1. The client
31
+
32
+ ```ts
33
+ import { createClient } from "@bettercms-ai/sdk";
34
+
35
+ export const bcms = createClient({
36
+ apiUrl: process.env.BCMS_API_URL!, // the backend origin; the SDK adds /api/v1/delivery/<workspace>
37
+ workspace: process.env.BCMS_WORKSPACE!,
38
+ projectId: process.env.BCMS_PROJECT_ID, // scopes listForms
39
+ });
40
+ ```
41
+
42
+ Reading published content needs no key. The platform sets `BCMS_API_URL`, `BCMS_WORKSPACE` and
43
+ `BCMS_PROJECT_ID` on every build and running server app, and again under each framework's public
44
+ prefix (`NUXT_PUBLIC_`, `PUBLIC_`, `VITE_`…); the table is in
45
+ [external-content.md](external-content.md). On a site BetterCMS builds, never add them to `.env`.
46
+ A headless front end on its own host sets them itself: in `.env` locally, and on the host by
47
+ `npx @bettercms-ai/cli@latest deploy --to <host>`.
48
+
49
+ | Option | Use |
50
+ |---|---|
51
+ | `perspective: "drafts"` | Draft reads; needs a `content:read:draft` key or a `previewToken`. Stega is on by default there ([bindings.md](bindings.md) §5) |
52
+ | `cache: true` | Collapse identical GETs for the client's life. Only for a static build (one point-in-time dataset); never in dev, SSR or drafts |
53
+ | `timeout` | Per-attempt ms, default 10000 |
54
+
55
+ ## 2. Methods and their 404s
56
+
57
+ | Method | Returns | On a missing slug |
58
+ |---|---|---|
59
+ | `getPage(slug)` | the page (`blocks`, meta columns) | `null`, after scanning the page list |
60
+ | `getEntry(slug, { depth, select })` | one entry | throws `BetterCMSError` with `status` 404 |
61
+ | `listEntries({ model, page, perPage, depth, select })` | `{ items, page, perPage, totalItems, totalPages, hasNextPage, hasPreviousPage }` | a list 404 (unknown model or workspace) throws |
62
+ | `listPages`, `listComponents`, `getLayout`, `listForms` | pages, component definitions, the Global Layout (`null` if none), forms (render only `status: "published"`) | |
63
+
64
+ Catch the 404 on `getEntry` and answer the framework's not-found (a 404 status, never an empty
65
+ 200). Let a list 404 fail the build. Drive pagers from `totalPages`/`hasNextPage`.
66
+
67
+ ## 3. Where to call it
68
+
69
+ Call the client only in code that runs on the server or at build time, so a key (drafts) never
70
+ reaches the bundle: a Nuxt server route or `useAsyncData` on the server, a SvelteKit
71
+ `+page.server.ts` `load`, a React Router `loader`. A browser-only Vite SPA reads published content
72
+ with the public-prefixed variables and no key.
73
+
74
+ ## 4. What only the adapters give you
75
+
76
+ No `BcmsBlocks`/`BcmsField` components, no `bind`/`bindProps` helpers, no draft routes and no
77
+ snapshot bin exist for these frameworks. So:
78
+
79
+ - Render component blocks yourself from `listComponents()` definitions, and write binding
80
+ attributes by hand per [bindings.md](bindings.md) (`data-bcms-field`, `data-bcms-block`).
81
+ - Rich text: read it through the SDK's `plain`, `rich` or `portableText` helpers, which accept a
82
+ bare string or the `{ format, value, html }` envelope.
83
+ - Images: `imageUrlBuilder` is re-exported from the SDK ([media-and-brand.md](media-and-brand.md)).
84
+ - Meta: `resolveSeo` merges page over site defaults; write the result into `<head>` yourself.
85
+ - Verify with the two-path smoke test in [frameworks.md](frameworks.md) Verify.