@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,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.
|