@bettercms-ai/mcp 0.69.0 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +195 -75
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +10 -4
- package/dist/skills/bettercms/references/animation.md +235 -0
- package/dist/skills/bettercms/references/astro.md +7 -3
- package/dist/skills/bettercms/references/bindings.md +80 -3
- package/dist/skills/bettercms/references/componentize.md +6 -4
- package/dist/skills/bettercms/references/components.md +58 -11
- package/dist/skills/bettercms/references/content-modeling.md +17 -9
- package/dist/skills/bettercms/references/detection.md +4 -4
- package/dist/skills/bettercms/references/editor-chrome.md +4 -0
- package/dist/skills/bettercms/references/finish-site.md +80 -2
- package/dist/skills/bettercms/references/forms-and-interactions.md +16 -4
- package/dist/skills/bettercms/references/frameworks.md +2 -2
- package/dist/skills/bettercms/references/media-and-brand.md +22 -1
- package/dist/skills/bettercms/references/next.md +6 -5
- package/dist/skills/bettercms/references/publishing.md +46 -0
- package/dist/skills/bettercms/references/site-blueprints.md +18 -4
- package/dist/skills/bettercms/references/worked-examples-advanced.md +17 -3
- package/dist/skills/bettercms/references/worked-examples.md +26 -11
- package/package.json +2 -2
|
@@ -128,6 +128,7 @@ resources: `bettercms://skill/bettercms` for this file, and each reference under
|
|
|
128
128
|
| The same for an article entry, navigation in the Global Layout, tabs holding columns | [worked-examples-advanced.md](references/worked-examples-advanced.md) | a collection entry with a body, site chrome, or nested blocks | example, blog post, document body, mega menu, tabs, columns |
|
|
129
129
|
| Componentize a derived site | [componentize.md](references/componentize.md) | `get_site_composition` shows pages made of fields only | componentize, sections, fields-only, get_componentize_plan |
|
|
130
130
|
| Click-to-edit attributes, addresses, declared mode | [bindings.md](references/bindings.md) | `grep -rl "data-bcms-" src` finds files; a value the editor cannot select | data-bcms-field, click to edit, binding report, stega, declared |
|
|
131
|
+
| Animated sites: GSAP, ScrollTrigger, split text, Framer Motion, Lenis, marquees, carousels, Lottie, hydrated islands | [animation.md](references/animation.md) | `package.json` lists `gsap`, `lenis`, `framer-motion`, `swiper` or a text splitter; a publish refused with `SPLIT_PATTERN_UNKNOWN` or `ISLAND_PROPS_UNMATCHED`; a `client-only-island` gate exception | animation, GSAP, ScrollTrigger, SplitText, Lenis, Framer Motion, marquee, Swiper, Lottie, island, client:only, data-bcms-split |
|
|
131
132
|
| Wiring, env, hosting modes, media host, snapshots | [frameworks.md](references/frameworks.md) | `package.json` lists an adapter; `bcms-content.json` exists | hosting, headless, env, codegen, types, revalidate, snapshot |
|
|
132
133
|
| Astro adapter | [astro.md](references/astro.md) | the repo has `astro.config.*` | astro.config, Astro, BcmsBlocks, getStaticPaths |
|
|
133
134
|
| Next.js adapter | [next.md](references/next.md) | the repo has `next.config.*` | next.config, Next.js, App Router, ISR, notFound, static export |
|
|
@@ -181,7 +182,12 @@ Stop at the first step that is not clean, fix, then restart. A report that does
|
|
|
181
182
|
3. `get_site_composition`: every page you touched is made of sections, with the intended
|
|
182
183
|
components.
|
|
183
184
|
4. `list_seo_issues`: no issue on a page you created or edited.
|
|
184
|
-
5. `get_next_steps`:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
5. `get_next_steps`: `receipt.complete` is `true`. While it is `false`, take the first row of
|
|
186
|
+
`receipt.gate.blocking`, run its `fix`, and read again; nothing else ends an imported site's
|
|
187
|
+
setup. The gate, its bars and the exceptions it accepts are in
|
|
188
|
+
[finish-site.md](references/finish-site.md). Suggestions still in `missing` after that, and
|
|
189
|
+
owner actions, go in your final message by name.
|
|
190
|
+
|
|
191
|
+
"Done" means the loop is clean and the gate reads complete, not that a tool call returned 200.
|
|
192
|
+
`bettercms init` reads the gate again when you exit and prints its numbers per page, so the
|
|
193
|
+
user sees them whatever your last message said.
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Animated sites"
|
|
3
|
+
description: "Keeping GSAP, ScrollTrigger, SplitText, Framer Motion, Lenis, marquees, carousels, Lottie and hydrated islands working while their copy stays editable and publishable."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Animated sites: editable copy that keeps moving
|
|
7
|
+
|
|
8
|
+
Read this when `package.json` lists `gsap`, `lenis`, `framer-motion` or `motion`, `swiper`,
|
|
9
|
+
`split-type`, `splitting`, `lottie-web` or `three`, or when the built HTML carries
|
|
10
|
+
`astro-island`, split-text spans or a duplicated marquee track. Attribute grammar:
|
|
11
|
+
[bindings.md](bindings.md). Adapter wiring: [frameworks.md](frameworks.md).
|
|
12
|
+
|
|
13
|
+
## 1. What a publish does to animated markup
|
|
14
|
+
|
|
15
|
+
A text publish on a project in the text-node mode (off by default, for new projects too: the
|
|
16
|
+
operator switches it on per project, or for the fleet) writes the change into the element's
|
|
17
|
+
text nodes and keeps every element inside it. That is what the recipes below rely on:
|
|
18
|
+
|
|
19
|
+
| The page has | A publish does |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Inline markup inside the bound element (`<em>`, a highlight `<span>`, `<br>`) | changes the words, keeps the elements and their attributes; a bare `<em>` or `<span>` the new words leave empty is removed |
|
|
22
|
+
| Text cut into `word` / `char` / `line` spans, same word count | writes each span in place |
|
|
23
|
+
| The same, word count changed, spans made by a library at runtime | puts the plain text back; the library splits it on its next run |
|
|
24
|
+
| The same, spans written by your own template | rebuilds them from their own pattern, a new one copied from the last |
|
|
25
|
+
| A copy of the value under `aria-hidden="true"` or `data-bcms-clone` | writes the copy too (coverage kind `duplicate-track`) |
|
|
26
|
+
| The value inside `astro-island` props, `__NEXT_DATA__` or the app router's flight scripts | rewrites the serialized string (coverage kind `island-props`) |
|
|
27
|
+
|
|
28
|
+
It refuses, and says so on the publish, when the change would have to remove something:
|
|
29
|
+
`TEXT_IN_CHILD_FIELD` (the words belong to a field declared inside this one),
|
|
30
|
+
`TEXT_EMPTIES_ELEMENT` (a link, a scripted element or a wrapper with attributes would be left empty),
|
|
31
|
+
`SPLIT_PATTERN_UNKNOWN` (split markup that is not one row of like tokens) and
|
|
32
|
+
`ISLAND_PROPS_UNMATCHED` (a hydrated island draws the text from props that do not hold it).
|
|
33
|
+
Each is fixed in the template with the recipe for its library.
|
|
34
|
+
|
|
35
|
+
On a project in the older mode, the default, a text publish replaces the element's content with
|
|
36
|
+
plain text. There, bind only elements that hold text and nothing else.
|
|
37
|
+
|
|
38
|
+
## 2. Two rules for every library
|
|
39
|
+
|
|
40
|
+
1. **Bind the element that holds the words, never the element the animation owns.** A pinned
|
|
41
|
+
section, a scroll wrapper, a slider track and a motion container are layout. Their text
|
|
42
|
+
leaves are the fields.
|
|
43
|
+
2. **Refresh measurements after an edit.** The editor posts `{ type: "bcms:content-applied" }`
|
|
44
|
+
to the page's window after it applies an edit. Longer copy changes heights, so anything that
|
|
45
|
+
measured the page must measure again:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
window.addEventListener("message", (event) => {
|
|
49
|
+
if (event.data?.type !== "bcms:content-applied") return;
|
|
50
|
+
ScrollTrigger.refresh(); // or lenis.resize(), swiper.update(), split.revert() then split again
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`onContentApplied(handler)` from `@bettercms-ai/sdk` is the same listener with the check
|
|
55
|
+
written for you. On a published page nothing posts the message and the listener never runs.
|
|
56
|
+
|
|
57
|
+
## 3. GSAP and ScrollTrigger
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<section class="pin-wrap"> <!-- trigger and pin: no field -->
|
|
61
|
+
<div class="pin">
|
|
62
|
+
<h2 data-bcms-field="story.title">Built to last</h2>
|
|
63
|
+
<p data-bcms-field="story.lead">Forged, not cast.</p>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
gsap.to(".pin h2", { yPercent: -20, scrollTrigger: { trigger: ".pin-wrap", pin: ".pin", scrub: 1 } });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Never put `data-bcms-field` on `.pin-wrap` or `.pin`: ScrollTrigger wraps a pinned element in
|
|
73
|
+
a spacer and writes inline styles on it every frame. Use `<BcmsField>` only around a single
|
|
74
|
+
element, where it adds no wrapper (section 10); a wrapper span between `.pin` and its `h2`
|
|
75
|
+
would break `.pin > h2`.
|
|
76
|
+
|
|
77
|
+
Check: with the page open, `ScrollTrigger.getAll().length` is the same before and after a
|
|
78
|
+
publish of every field on the route.
|
|
79
|
+
|
|
80
|
+
## 4. SplitText, SplitType, Splitting.js
|
|
81
|
+
|
|
82
|
+
Bind the parent and say that it is split. Never bind a word or a character.
|
|
83
|
+
|
|
84
|
+
```html
|
|
85
|
+
<h1 class="js-split" data-bcms-field="hero.title" data-bcms-split="words">Fast screws, shipped</h1>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
let split = new SplitText(".js-split", { type: "words" });
|
|
90
|
+
window.addEventListener("message", (event) => {
|
|
91
|
+
if (event.data?.type !== "bcms:content-applied") return;
|
|
92
|
+
split.revert();
|
|
93
|
+
split = new SplitText(".js-split", { type: "words" });
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`data-bcms-split` takes `chars`, `words` or `lines`. It tells the editor that the spans under
|
|
98
|
+
this element are one value, and it tells a publish that the children are tokens even when their
|
|
99
|
+
class is not `word`, `char` or `line`.
|
|
100
|
+
|
|
101
|
+
When your own template writes the spans (`title.split(" ").map(...)`), keep them one row of
|
|
102
|
+
the same tag, with whitespace or nothing between them, and no other element in the row:
|
|
103
|
+
|
|
104
|
+
```astro
|
|
105
|
+
<h1 data-bcms-field="hero.title" data-bcms-split="words">
|
|
106
|
+
{title.split(" ").map((word) => <span class="word">{word}</span>)}
|
|
107
|
+
</h1>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
A `<br>` between two word spans makes the row unreadable once the word count changes
|
|
111
|
+
(`SPLIT_PATTERN_UNKNOWN`): put the break in CSS, or split by lines.
|
|
112
|
+
|
|
113
|
+
Check: publish a title with one more word, reload, and count `.word`: it equals the new word
|
|
114
|
+
count and no word sits outside a span.
|
|
115
|
+
|
|
116
|
+
## 5. Framer Motion
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
<motion.section initial={{ opacity: 0 }} whileInView={{ opacity: 1 }}> {/* no field */}
|
|
120
|
+
<motion.h2 data-bcms-field="story.title" variants={rise}>{story.title}</motion.h2>
|
|
121
|
+
<p data-bcms-field="story.lead">{story.lead}</p>
|
|
122
|
+
</motion.section>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Put `data-bcms-field` straight on the motion element that renders the text: it passes
|
|
126
|
+
`data-*` through to the DOM. Do not wrap a motion component in `<BcmsField>`; a component child
|
|
127
|
+
keeps the wrapper span, and `layout` animations measure the wrong box. For words animated one
|
|
128
|
+
by one, map them to `motion.span` under a parent carrying `data-bcms-split="words"`, as in
|
|
129
|
+
section 4. The component hydrates, so section 9 applies to its props.
|
|
130
|
+
|
|
131
|
+
Check: the built HTML for the route shows `data-bcms-field` on the `h2` itself, with no
|
|
132
|
+
`display:contents` span around it.
|
|
133
|
+
|
|
134
|
+
## 6. Lenis and other smooth scroll
|
|
135
|
+
|
|
136
|
+
Lenis moves the page, not the content, so no binding changes. Two things do:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
const lenis = new Lenis();
|
|
140
|
+
window.addEventListener("message", (event) => {
|
|
141
|
+
if (event.data?.type === "bcms:content-applied") lenis.resize();
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
and any panel of yours that must scroll by itself inside the page takes `data-lenis-prevent`.
|
|
146
|
+
Never bind the element Lenis is given as `wrapper` or `content`.
|
|
147
|
+
|
|
148
|
+
Check: after an edit that adds two lines to a paragraph, the page still scrolls to the footer.
|
|
149
|
+
|
|
150
|
+
## 7. Marquees and duplicated tracks
|
|
151
|
+
|
|
152
|
+
Render the track once with fields, and mark every copy:
|
|
153
|
+
|
|
154
|
+
```astro
|
|
155
|
+
<div class="marquee">
|
|
156
|
+
<ul class="track">{logos.map((l, i) => <li class="logo" data-bcms-field={`logos[${i}].name`}>{l.name}</li>)}</ul>
|
|
157
|
+
<ul class="track" aria-hidden="true" data-bcms-clone>{logos.map((l) => <li class="logo">{l.name}</li>)}</ul>
|
|
158
|
+
</div>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
A copy under `aria-hidden="true"` or `data-bcms-clone` is not counted as unowned text, and a
|
|
162
|
+
publish writes it with the original when it is the same tag with the same classes and the same
|
|
163
|
+
text. A copy with neither marker is separate copy: it counts against the route's text coverage
|
|
164
|
+
and keeps the old words. A script that clones the track in the browser needs nothing: the clone
|
|
165
|
+
is made from the published HTML on every load.
|
|
166
|
+
|
|
167
|
+
Check: publish one logo name, then `document.querySelectorAll(".track")` shows the new name in
|
|
168
|
+
every track.
|
|
169
|
+
|
|
170
|
+
## 8. Swiper and carousels
|
|
171
|
+
|
|
172
|
+
Bind what is inside a slide, never `.swiper-wrapper` or `.swiper-slide`:
|
|
173
|
+
|
|
174
|
+
```html
|
|
175
|
+
<div class="swiper-slide"><h3 data-bcms-field="slides[0].title">Torque</h3></div>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Loop mode clones slides in the browser (`.swiper-slide-duplicate`); those are made from the
|
|
179
|
+
published HTML and need no field. On `bcms:content-applied`, call `swiper.update()`.
|
|
180
|
+
|
|
181
|
+
Check: `get_binding_report` lists no `slides[n]` path as pending for the route.
|
|
182
|
+
|
|
183
|
+
## 9. Hydrated islands and `client:only`
|
|
184
|
+
|
|
185
|
+
A hydrated component draws itself again from the props the build serialized. Keep the text it
|
|
186
|
+
shows equal to one string prop, whole:
|
|
187
|
+
|
|
188
|
+
```astro
|
|
189
|
+
<Hero client:load title={page.hero.title} />
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
export default function Hero({ title }) { return <h1 data-bcms-field="hero.title">{title}</h1>; }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
A publish then rewrites that prop with the text. It cannot when the component builds the text
|
|
197
|
+
(`${title}!`, `title.toUpperCase()`, a string literal in the component), or when the props hold
|
|
198
|
+
the same words more than once, under a key such as `id` or `slug`, or as `__html`: the publish reports
|
|
199
|
+
`ISLAND_PROPS_UNMATCHED` and the value goes live with the next build. Content passed as a
|
|
200
|
+
slot is plain HTML and needs no prop.
|
|
201
|
+
|
|
202
|
+
`client:only` renders nothing at build time, so the built page has no copy to own. The release
|
|
203
|
+
names every such island on its route and the setup gate lists it as a `client-only-island`
|
|
204
|
+
exception by itself. To make the copy editable, change the directive to `client:load` or
|
|
205
|
+
`client:visible`, or pass the copy in as a slot.
|
|
206
|
+
|
|
207
|
+
Check: view source on the built route, find the `astro-island`, and its `props` attribute holds
|
|
208
|
+
the published title after a publish.
|
|
209
|
+
|
|
210
|
+
## 10. `<BcmsField>` adds no wrapper around one element
|
|
211
|
+
|
|
212
|
+
```astro
|
|
213
|
+
<BcmsField path="hero.title"><h1 class="title">{page.hero.title}</h1></BcmsField>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
renders `<h1 class="title" data-bcms-field="hero.title">`: nothing sits between the parent and
|
|
217
|
+
the heading. Text, several children, and in Next a component child are still wrapped in a
|
|
218
|
+
`display:contents` span, and `as="div"` still gives a real wrapper. A site built with an older
|
|
219
|
+
package version keeps its wrapper spans until it is rebuilt.
|
|
220
|
+
|
|
221
|
+
## 11. Lottie, canvas, WebGL, video
|
|
222
|
+
|
|
223
|
+
Pixels drawn by a script are not in the HTML, so nothing in them is a field by itself. What is
|
|
224
|
+
editable is what you pass in:
|
|
225
|
+
|
|
226
|
+
| Drawn by | Make editable by |
|
|
227
|
+
|---|---|
|
|
228
|
+
| Lottie | Bind the animation file as an image-or-file field on the container and load `path` from it. For words inside the animation, keep a text layer named after the field and set it at load (`anim.renderer.elements[i].updateDocumentData({ t: value })`), reading the value from a hidden bound element or a prop. |
|
|
229
|
+
| Canvas / WebGL / three.js | Read every string and texture URL from `data-*` attributes or props on the canvas's container; bind a visually hidden element per string and an `<img hidden>` per texture, and re-read them on `bcms:content-applied`. |
|
|
230
|
+
| Video | The poster and the source are fields on the `<video>`; burned-in captions are not text. |
|
|
231
|
+
|
|
232
|
+
Give the canvas `role="img"` and an `aria-label` bound to a field, or an adjacent caption. A
|
|
233
|
+
scene with no text a visitor reads needs no field; record it under the `decorative` exception.
|
|
234
|
+
|
|
235
|
+
Check: change the bound string, publish, reload: the drawn text reads the new value.
|
|
@@ -41,8 +41,8 @@ build. Pass `apiUrl` and `workspace` as options, or let the snapshot supply them
|
|
|
41
41
|
|---|---|
|
|
42
42
|
| `@bettercms-ai/astro/components/BcmsBlocks.astro` | Server render of a block tree. Pass `components` or instances render nothing |
|
|
43
43
|
| `@bettercms-ai/astro/components/BcmsLiveBlocks.astro` | Same tree as a hydrated island that follows the editor's draft bridge |
|
|
44
|
-
| `@bettercms-ai/astro/components/BcmsField.astro` | `path`, `kind` (`richtext`/`image`), `as`; emits `data-bcms-field` + `data-bcms-kind` |
|
|
45
|
-
| `@bettercms-ai/astro/components/BcmsButton.astro` | A `button` field: `path`, `value`, `class`. One `<a>` with the label bound, the href in `data-bcms-props` and `data-variant` / `data-size
|
|
44
|
+
| `@bettercms-ai/astro/components/BcmsField.astro` | `path`, `kind` (`richtext`/`image`), `as`; emits `data-bcms-field` + `data-bcms-kind` on its single child element, with no wrapper ([animation.md](animation.md) §10) |
|
|
45
|
+
| `@bettercms-ai/astro/components/BcmsButton.astro` | A `button` field: `path`, `value`, `class`. One `<a>` with the label bound, the href in `data-bcms-props` and `data-variant` / `data-size`. Style only the looks the site has ([components.md](components.md) §5); renders nothing without a label |
|
|
46
46
|
| `@bettercms-ai/astro/components/BcmsForm.astro` | Renders a delivered form and submits it ([forms-and-interactions.md](forms-and-interactions.md)) |
|
|
47
47
|
|
|
48
48
|
`BcmsLiveBlocks` rules:
|
|
@@ -55,7 +55,11 @@ build. Pass `apiUrl` and `workspace` as options, or let the snapshot supply them
|
|
|
55
55
|
|
|
56
56
|
A section root your own code renders spreads `sectionRootAttrs(block)` from `@bettercms-ai/astro`
|
|
57
57
|
(`data-bcms-block`, `data-bcms-style` and the block's style), so the editor can move it and offer
|
|
58
|
-
its spacing handles ([components.md](components.md) §5).
|
|
58
|
+
its spacing handles ([components.md](components.md) §5). It adds `data-bcms-hide` when `style.hideOn` is set: put `RESPONSIVE_CSS` (exported here) in your global CSS.
|
|
59
|
+
|
|
60
|
+
`<BcmsBlocks>` does not render a block outside its `window`, read against the `now` prop (default:
|
|
61
|
+
render time). Pass `showScheduled` on a draft preview to draw it, marked `data-bcms-window`
|
|
62
|
+
([publishing.md](publishing.md) §6).
|
|
59
63
|
|
|
60
64
|
`bind` and `bindProps` from `@bettercms-ai/astro/bindings` produce the same attributes as the Next
|
|
61
65
|
helpers. Use them inside component instances so addresses resolve to the instance
|
|
@@ -20,8 +20,12 @@ cannot resolve an address guesses, which works once.
|
|
|
20
20
|
|
|
21
21
|
**Put the attributes on the element that renders the value and nothing else: the `<img>` itself,
|
|
22
22
|
the `<h1>` itself. Never a wrapper, never an element whose children you need.** A declared write
|
|
23
|
-
|
|
24
|
-
markup is what
|
|
23
|
+
owns what is inside the element it names. Bind a `<section>` or a card and the band's own
|
|
24
|
+
markup is what a rich text or image write overwrites; the page comes back as bare text where a
|
|
25
|
+
layout used to be. A plain-text write keeps the inline elements inside its element (a highlight
|
|
26
|
+
`<span>`, split-text spans, a `<br>`) on a project in the text-node publish mode, and replaces
|
|
27
|
+
them on an older one: [animation.md](animation.md) §1 has both, and the recipe per animation
|
|
28
|
+
library.
|
|
25
29
|
|
|
26
30
|
`data-bcms-kind` does not choose the editor; the MODEL does. The attribute says **"this element is
|
|
27
31
|
the declared CONTAINER for the field"**, so the canvas repaints it whole. Leave it off and the
|
|
@@ -92,6 +96,30 @@ the fallback; the build renders it and the publish writes the editor's image. Fr
|
|
|
92
96
|
images are in the Media Library, matched by file name. An image rendered inside a loop over a
|
|
93
97
|
list is not bound automatically: bind it in the list's data file.
|
|
94
98
|
|
|
99
|
+
The codemod knows an image component by its IMPORT, not its name: `Image` and `Picture` from
|
|
100
|
+
`astro:assets` (aliased, or through `import * as assets`), and the default export of
|
|
101
|
+
`next/image` under any name. An import that reaches the page through `getImage()`, `hero.src` or
|
|
102
|
+
a style is refused as `IMAGE_ASSET_UNRESOLVED`, and the row's message names the import: declare
|
|
103
|
+
the element that shows it by hand (`data-bcms-field` + `data-bcms-kind="image"`).
|
|
104
|
+
|
|
105
|
+
Images need no code at all on a text-matched site. Every release derives an image field for each
|
|
106
|
+
picture a page shows: an `<img>` (its `src`, or where that is a placeholder a lazy loader's
|
|
107
|
+
`data-src`, then the largest `srcset` candidate), a whole `<picture>` as ONE field, a
|
|
108
|
+
`<video poster>`, an inline or STYLESHEET `background-image` (Tailwind `bg-[url(...)]` included)
|
|
109
|
+
and an inline `<svg>`. What it declines is listed with a reason in the binding report's
|
|
110
|
+
`imageGaps` ([media-and-brand.md](media-and-brand.md) Where images hide).
|
|
111
|
+
|
|
112
|
+
Publishing an image keeps the page responsive. A library image re-points every `srcset`
|
|
113
|
+
candidate and each `<source>` at the new picture at that candidate's own width and format, and
|
|
114
|
+
leaves `sizes`, `width`, `loading`, classes, ids and `data-*` as written; never hand-edit those
|
|
115
|
+
after a swap. A stylesheet background gets one inline `background-image` over the rule, removed
|
|
116
|
+
again when the value returns to the sheet's own picture.
|
|
117
|
+
|
|
118
|
+
To keep an image editable in code you write yourself, render it with `BcmsImage` and its `path`
|
|
119
|
+
(Astro: `@bettercms-ai/astro/components/BcmsImage.astro`; Next: `BcmsImage` from
|
|
120
|
+
`@bettercms-ai/next`): one `<img>` that declares the field, reads the value's alt, crop and focal
|
|
121
|
+
point, and offers a width ladder when given `sizes`.
|
|
122
|
+
|
|
95
123
|
## 3. Addresses
|
|
96
124
|
|
|
97
125
|
The grammar is identical in `@bettercms-ai/next` and `@bettercms-ai/astro`; emit it with the SDK
|
|
@@ -103,7 +131,9 @@ helpers rather than by hand.
|
|
|
103
131
|
this list. An `href`, an `src` and a `poster` are kind `url` (`cta.href|url|href`, not
|
|
104
132
|
`|text|`); an `alt` is `text`. The same address on two elements is edited as two copies of one
|
|
105
133
|
value (numbered `data-node-index`).
|
|
106
|
-
- `<BcmsField path kind as>`
|
|
134
|
+
- `<BcmsField path kind as>` binds hand-rendered entry values. Around ONE element it adds no
|
|
135
|
+
wrapper: the attributes go on that element. Around text or several elements it renders a
|
|
136
|
+
`display:contents` span, and `as` names a real wrapper. `path` is dotted keys with any
|
|
107
137
|
number of `[n]` indices: `title`, `hero.title`, `columns[0].links[2].href`.
|
|
108
138
|
- Rich text is addressed per paragraph: `<base>#<_key>`. No write tool takes an address:
|
|
109
139
|
`update_content_entry` replaces the whole field (with `merge: true` it keeps the other
|
|
@@ -207,3 +237,50 @@ still renders hard-coded copy is not connected. Client components never receive
|
|
|
207
237
|
- `get_binding_report`'s `help.percent` is 100: every bound field also tells its editor what it
|
|
208
238
|
is for. Binding a leaf makes it editable; help text makes it usable
|
|
209
239
|
([content-modeling.md](content-modeling.md), "Writing help text").
|
|
240
|
+
|
|
241
|
+
## Reading the binding report and the conversion receipt
|
|
242
|
+
|
|
243
|
+
`get_binding_report` per slot (`slot` is `current` or `staging`; on a promote-gated project read
|
|
244
|
+
`current`, the tree the editor frames). Every figure, `unaddressable` included, is measured per
|
|
245
|
+
slot. `sha` is the build the report describes; `refreshRequired` is true when that is not the
|
|
246
|
+
build the slot serves. `builtRoutes` lists the routes the live build has HTML for (null = cannot
|
|
247
|
+
say). `layoutIncomplete` lists Layout paths the last content apply could not write (their publish
|
|
248
|
+
waits); `layoutUnread` lists Layout fields no route reads (`layoutUnreadJudged` false = not
|
|
249
|
+
judged).
|
|
250
|
+
|
|
251
|
+
- **Before any release** there is no report: pages 0, mode null, `refreshRequired` true.
|
|
252
|
+
`refreshReason` `never-generated` means only a release BetterCMS builds and serves writes a
|
|
253
|
+
report, so a headless site never gets one, redeploy or not; `outdated` means the next release
|
|
254
|
+
rewrites it.
|
|
255
|
+
- **`unmatched` reasons**: `not-declared`, `ambiguous-text`, `no-element`, `declared-only`,
|
|
256
|
+
`needs-wrap`, `refused`, `layout-owned`, `page-owned`, `layout-locator-stale`,
|
|
257
|
+
`locale-unvarianted`, `svg-unavailable`, `partial`, `unstamped` (a page list the content apply
|
|
258
|
+
left as built; `kind` reads `list <bound>/<total>`), `mismatch` (a text-match stamp whose element
|
|
259
|
+
shows more than the value, or no longer the text it was matched on, `data-bcms-from`; never
|
|
260
|
+
written) and `report-outdated`.
|
|
261
|
+
- A clean report proves each non-empty field has an element carrying its path, nothing more. A Global
|
|
262
|
+
Section counts only on the routes that render it (`layoutRoutes`).
|
|
263
|
+
- **`unaddressable`** is the copy the report cannot see: visible live text no field owns, by
|
|
264
|
+
route and bucket, biggest first (null = unmeasured: diff the visible text against the entry
|
|
265
|
+
values). Over `count / visible > 0.02`, fix it in the source (wrap the run in an element the
|
|
266
|
+
codemod can bind, or declare `<BcmsField path=…>`), deploy and read again (playbook §11).
|
|
267
|
+
- `canvas.lane` is the editor's live-preview lane (`bridge`, `draft-route`; `none` sends you to
|
|
268
|
+
`get_next_steps`). `coverage.error` `nothing-bound` means no element of this build is bound, so
|
|
269
|
+
its percentage describes another site.
|
|
270
|
+
|
|
271
|
+
`submit_conversion_receipt` answers `complete` (`sourceComplete` is the receipt's own verdict;
|
|
272
|
+
`liveChecks` gives the last release's `structureUnstamped` route count and `reportSha`) and
|
|
273
|
+
`pendingTotal`, and echoes the first 40 pending rows with their `fix`:
|
|
274
|
+
`{ action, file, line, col?, snippet, why?, kind? }`. `action` is one of `wrap-span`,
|
|
275
|
+
`declare-attr`, `bind-expression`, `declare-richtext`, `bind-data`, `manual`; `snippet` and `why`
|
|
276
|
+
say what to change. On a `declare-richtext` with `kind: document`, bind the one element wrapping
|
|
277
|
+
every block of the Body, never the paragraph holding its first. A 404 `unknown-brief` means that
|
|
278
|
+
digest was never issued here. The same run owes the `--forms` receipt, submitted as a second call
|
|
279
|
+
under the same `briefDigest`, and the `styleEdits` of `convert --verify-styles` inside this
|
|
280
|
+
receipt; one listing a changed class, className or style is refused with 409 `style-edits`.
|
|
281
|
+
|
|
282
|
+
The complete brief (`get_conversion_brief { complete: true }`) also carries `forms` (id, status,
|
|
283
|
+
fields, submitUrl). After the binding pass, run the codemod's forms mode (`npx
|
|
284
|
+
@bettercms-ai/convert --forms`, with the brief and a receipt file) to wire them into the
|
|
285
|
+
repository's own `<form>` elements. Read `forms.pending`, and publish every form `forms.notes` names in the Forms tab: a draft
|
|
286
|
+
form rejects every submission with a 403.
|
|
@@ -25,8 +25,9 @@ Inside a section, pick the smallest shape that holds the children:
|
|
|
25
25
|
|
|
26
26
|
- **Small identical items** (FAQ rows, feature bullets, logos): a `group` or `table` prop.
|
|
27
27
|
- **One replaceable child with its own layout or variants** (a hero's media treatment): a `slot`
|
|
28
|
-
prop. It holds
|
|
29
|
-
- **Several different children**: the section's ordered children
|
|
28
|
+
prop. It holds one instance, and never sits inside a `group` or `table` row.
|
|
29
|
+
- **Several different children**: the section's ordered children when the set is fixed; a `slot`
|
|
30
|
+
prop with `config.multiple` when the editor picks and orders them.
|
|
30
31
|
|
|
31
32
|
Never let a component contain itself, directly or through a child.
|
|
32
33
|
|
|
@@ -40,8 +41,9 @@ Never let a component contain itself, directly or through a child.
|
|
|
40
41
|
prose body and lists of object rows are sections. Read `pending` for what it will not offer:
|
|
41
42
|
`NO_GROUP_ROOT` (keys are still the derive lane's own, such as `h1-welcome`: rename them into
|
|
42
43
|
families first), `NOT_A_SECTION` (a lone scalar or page metadata; `detail: ENTRY_METADATA` is
|
|
43
|
-
a template's frontmatter,
|
|
44
|
-
(a text field holding an image path; it plans
|
|
44
|
+
a template's frontmatter: author, images, tags and dates stay entry fields; `detail: DECLINED`
|
|
45
|
+
is a group the user declined) and `IMAGE_AS_TEXT` (a text field holding an image path; it plans
|
|
46
|
+
once the field is an image). A nav, footer or
|
|
45
47
|
modal group is `NOT_A_SECTION` with `detail: LAYOUT_CHROME`: chrome is the Layout on every
|
|
46
48
|
site, never a page band. Author the real sections among those with `create_component` and
|
|
47
49
|
`set_page_content` instead.
|
|
@@ -23,9 +23,9 @@ reference is what the code does with what you write. Componentizing a derived si
|
|
|
23
23
|
A component declares `props`. Each prop is `{ key, label, type, target: { blockId, path },
|
|
24
24
|
defaultValue? }`.
|
|
25
25
|
|
|
26
|
-
- `type` is one of exactly
|
|
27
|
-
`
|
|
28
|
-
`
|
|
26
|
+
- `type` is one of exactly 30: `text`, `richtext`, `image`, `url`, `boolean`, `number`, `select`, `group`, `table`, `slot`,
|
|
27
|
+
`form`, then `date`, `color`, `file`, `link`, `icon`, `multiselect` and `button`, then `object`, `datetime`, `slug`,
|
|
28
|
+
`email`, `phone`, `json`, `location`, `tags`, `rating`, `code`, `reference` and `multi-reference`.
|
|
29
29
|
- The last seven store what the same-named model field stores: `date` is `YYYY-MM-DD`, `color` a
|
|
30
30
|
hex, `file` `{ url, name?, size? }`, `link` an href string or `{ href, rel?, newTab? }`, `icon` an icon name or an image
|
|
31
31
|
value, `multiselect` a `string[]` drawn from `config.options`. A bad `defaultValue` is a 400.
|
|
@@ -38,14 +38,37 @@ defaultValue? }`.
|
|
|
38
38
|
- Every prop takes `helpText` and the component a `description` ("Hero with a heading,
|
|
39
39
|
supporting text and two buttons."). Leave them out and the platform writes them; write your
|
|
40
40
|
own by the rules in [content-modeling.md](content-modeling.md) ("Writing help text").
|
|
41
|
-
- A prop takes `showIf` (`{ match, rules: [{ field, op, value? }] }`): hidden until SIBLING props match. `op` is `eq`, `neq`, `in` or `
|
|
41
|
+
- A prop takes `showIf` (`{ match, rules: [{ field, op, value? }] }`): hidden until SIBLING props match. `op` is `eq`, `neq`, `in`, `isSet`, `notEmpty` or `empty`; the last three take no `value`, the first three need one. Props and model fields share all six, and a sub-field's `showIf` names its sibling sub-fields.
|
|
42
|
+
- `object` holds ONE `{ childKey: value }` built from `config.fields` (required), like the model field `object`. Groups, tables and objects nest to 10 levels counting the prop; an 11th is refused `field_depth_exceeded`. Aim an `object` at `props` or at a key your component code reads, never at a text slot such as a heading's `props.text` (`prop_target_unrenderable`).
|
|
43
|
+
- `datetime`, `slug`, `email`, `phone`, `json`, `location` (`{ lat, lng }`), `tags` (`string[]`), `rating` (0..`config.max`) and `code` (`{ language?, code }` or a string) hold what the same-named model field holds, and a placement's value is checked by that field's rules (`prop_value_invalid`). Like `object`, a `tags` or `code` prop never targets a text slot.
|
|
44
|
+
- `reference` holds one CMS item's id and `multi-reference` a list of them, as the model fields do: `config.contentModelId`, or `polymorphic: true` with `allowedModelSlugs` (one of the two is required), and `min`/`max` on the list. A placement's new id must exist (`REF_MISSING`); an id already stored is never re-checked. Page reads (delivery, preview) hydrate the ids to entries at their `depth`, so component code reads the entry; on a heading or button text slot the entry's title renders. A `multi-reference` never targets a text slot.
|
|
42
45
|
- `update_component` replaces the whole `props` array. Read with `get_component`, change, write
|
|
43
46
|
all props back.
|
|
44
|
-
- `select` and `multiselect` need `config.options`; `slot` holds one nested component instance
|
|
45
|
-
`form` holds one Form document.
|
|
47
|
+
- `select` and `multiselect` need `config.options`; `slot` holds one nested component instance,
|
|
48
|
+
or a list of them with `config.multiple`; `form` holds one Form document.
|
|
46
49
|
- `slot` and `form` are reference-shaped: `target.path` is `props` (the whole envelope) on an
|
|
47
50
|
empty `component` or `form` block, and their values are `{ componentId, overrides? }` and
|
|
48
51
|
`{ formId }`. Neither may appear inside a `group` or `table` row.
|
|
52
|
+
- **A `slot` with `config.multiple: true` holds a list.** Its value is
|
|
53
|
+
`[{ componentId, overrides? }, ...]` and its `target.path` is a container's child list:
|
|
54
|
+
`props.children` on a `section` block, or `props.columns.<n>` on a `columns` block (never
|
|
55
|
+
`props` on a `component` block). Each item is drawn there as a child component, in order.
|
|
56
|
+
Until a placement sets the slot, the children the definition holds in that list are shown;
|
|
57
|
+
`[]` shows none.
|
|
58
|
+
- `config.maxItems` defaults to 12 and cannot pass 50. `config.minItems` and
|
|
59
|
+
`config.componentIds` (which components may be listed) are checked on every write.
|
|
60
|
+
- An item may also carry `style` (use it for `hideOn`) and `window` with `from` / `until`
|
|
61
|
+
as UTC to the second (`2026-11-27T00:00:00Z`). A `defaultValue` must be an array and takes
|
|
62
|
+
no `window`.
|
|
63
|
+
- A write is a 400 whose message starts with the code: `slot_too_many`, `slot_too_few`,
|
|
64
|
+
`slot_item_invalid`, `slot_component_not_allowed`, `slot_component_unknown` (not a
|
|
65
|
+
component of this project), `component_cycle` (a component would end up inside itself,
|
|
66
|
+
through any chain), `component_too_deep` (more than 10 levels), `slot_target_invalid`,
|
|
67
|
+
`slot_not_multiple` (a list sent to a single slot). A value that is already stored is not
|
|
68
|
+
refused when the page is saved again.
|
|
69
|
+
- A child's copy is not click-to-edit on the canvas. Change it in the placement's
|
|
70
|
+
`overrides.<slot key>` list with `update_page`.
|
|
71
|
+
- Publish every listed component: a draft child renders nothing, like any draft component.
|
|
49
72
|
- **A `form` prop stores an id, nothing more.** Modelling the questions again as props gives you
|
|
50
73
|
two answers to "what does this form ask?", and they diverge at the first edit on either side.
|
|
51
74
|
The save schema is `.strict()`, so posting the hydrated form object back is a 400, not a silent
|
|
@@ -81,6 +104,12 @@ source? } }`.
|
|
|
81
104
|
component with a `sectionType`. A true one-off is a `section` block whose `props.children` hold
|
|
82
105
|
its blocks.
|
|
83
106
|
- Components that share a `sectionType` are one family: its variants.
|
|
107
|
+
- **A built-in section is a starting point, not a thing you place.** `list_components` shows the
|
|
108
|
+
locked `builtin:*` blueprints; `create_component` with `from: "builtin:hero-centered"` copies
|
|
109
|
+
one into the project as an ordinary draft component, then edit and publish it like any other.
|
|
110
|
+
Whatever you send (`name`, `slug`, `props`, `blockJson`, `sectionType`, `category`) wins over
|
|
111
|
+
the copy. `list_components` `sourceKey` says which built-in a component came from. A copy
|
|
112
|
+
never follows later changes to the built-in. An unknown id is 404 `BUILTIN_NOT_FOUND`.
|
|
84
113
|
- **Give every component a library `category` as well.** Why: `sectionType` groups a family, the
|
|
85
114
|
category is how the component library lists it — an uncategorised component is created fine and
|
|
86
115
|
is then hard to find in the picker, which is where a human looks for it.
|
|
@@ -97,8 +126,9 @@ source? } }`.
|
|
|
97
126
|
Default, Featured and Outline is ONE component with a `select` prop keyed `variant` whose options
|
|
98
127
|
are the looks, and its markup picks classes from that value. Why: an editor switches the look in
|
|
99
128
|
place and the content stays put, and the library holds one card, not three. Families of looks
|
|
100
|
-
(`variantOf`, a style select on every section that places them) are for buttons
|
|
101
|
-
look is `get_component`, then `create_component` with
|
|
129
|
+
(`variantOf`, a style select on every section that places them) are for buttons. On a site
|
|
130
|
+
built in BetterCMS a new button look is `get_component`, then `create_component` with
|
|
131
|
+
`variantOf` naming the base. On an imported site the family is read, not written (§5).
|
|
102
132
|
- Read `warnings` and act on them; they never block the write:
|
|
103
133
|
- `create_component` / `update_component`: `VARIANT_PROP_KEYS_MISMATCH` (a key no sibling
|
|
104
134
|
declares, or one siblings declare that this variant lacks) and `VARIANT_PROP_TYPE_MISMATCH`
|
|
@@ -128,7 +158,7 @@ Handle by code, never by retrying the same call:
|
|
|
128
158
|
|---|---|---|
|
|
129
159
|
| `COMPONENT_BLUEPRINT_MATERIALIZATION_REQUIRED` | You tried to publish a platform built-in | Create a project-local component from it, publish that |
|
|
130
160
|
| `COMPONENT_PROJECT_SCOPED_RELEASE_REQUIRED` | The component is workspace-global | Create a project-local variant, publish that |
|
|
131
|
-
| `COMPONENT_IMPLEMENTATION_NOT_READY` | A REPO-managed component lacks exact evidence and human approval, or a variant-group member
|
|
161
|
+
| `COMPONENT_IMPLEMENTATION_NOT_READY` | A REPO-managed component lacks exact evidence and human approval, or a variant-group member (the provisioned navigation and footer) has a family contract that is unpublished. Never a `create_component` component outside a variant group: that one is dashboard-managed and publishes on the preflight alone | Report `readiness`; the owner resolves it in the dashboard |
|
|
132
162
|
| `COMPONENT_ORCHESTRATION_V2_REQUIRED` | The project runs implementation in Agent Dock | Continue there; stop here |
|
|
133
163
|
| `COMPONENT_PUBLISH_CONFLICT` | The component or its evidence changed mid-publish | Re-read with `get_component`, then publish once more |
|
|
134
164
|
| `COMPONENT_PROJECT_CONTEXT_MISMATCH` | The component's `projectId` differs from the request's project scope | Call with the component's own project |
|
|
@@ -188,8 +218,16 @@ A code-built site ships only what its templates draw, whatever the page's `block
|
|
|
188
218
|
the repo root (`--dry-run` first); give each root it reports as skipped (`OWN_STYLE`, `SPREAD`,
|
|
189
219
|
`BLOCK_ID_ONLY`, …) its block style by hand, confirm with `--verify-styles`, push and release.
|
|
190
220
|
- **A `button` field renders with `BcmsButton`** (`path`, `value`): one `<a data-bcms-button>`
|
|
191
|
-
carrying `data-variant` and `data-size`, so
|
|
192
|
-
|
|
221
|
+
carrying `data-variant` and `data-size`, so every look control (style, size, arrow, icon, new
|
|
222
|
+
tab) applies on the canvas and on publish.
|
|
223
|
+
- **A button's looks are the ones the site already has.** Each release measures the built pages
|
|
224
|
+
and keeps the project's Button family equal to the looks found there, named as the site names
|
|
225
|
+
them (`btn-outline` is Outline). Style offers those looks and no others, and a publish paints a
|
|
226
|
+
pick with the site's own classes. Never write CSS for a look so that a control "works": there
|
|
227
|
+
is no fixed list of variants to implement. A `button` value's `variant` is one of those looks;
|
|
228
|
+
a refused value comes back with the looks the project has. A look the site drops is flagged,
|
|
229
|
+
never deleted. A new look is the owner's design decision: add its class to the site, then
|
|
230
|
+
Rebuild.
|
|
193
231
|
Without the SDK, write that same anchor: `data-bcms-button="<path>"`, the label in its own
|
|
194
232
|
`<span data-bcms-field="<path>.label">`, the href through `data-bcms-props`. The arrow is the
|
|
195
233
|
`svg.bcms-btn-arrow` it draws; never leave a hand-written arrow `<svg>` beside it, or turning
|
|
@@ -311,4 +349,13 @@ stored list.
|
|
|
311
349
|
siblings.
|
|
312
350
|
2. `get_site_composition`: every page is sections; every component you meant to publish is
|
|
313
351
|
published.
|
|
352
|
+
- Per page: `kind` is `blocks`, `fields-only` or `empty`. `nonComponentBlocks` are loose
|
|
353
|
+
top-level bands the editor's section lane cannot outline.
|
|
354
|
+
- Per placement: the component's `status` is `published`, `draft` or `missing`. `inPicker` is
|
|
355
|
+
false for a component with no `sectionType`: "Add a section" does not offer it.
|
|
356
|
+
`emptyRequiredProps` is reported only here.
|
|
357
|
+
- Site-wide: `components` counts published and draft, `awaitingEvidence` and
|
|
358
|
+
`awaitingApproval`. A person approves in the dashboard, one component at a time. Then the
|
|
359
|
+
`notInPicker` and `unused` ids, and `layout.placements`: the chrome placed in the Global
|
|
360
|
+
Layout draft.
|
|
314
361
|
3. `get_binding_report` on a page that uses the component: instance values bind.
|
|
@@ -46,7 +46,11 @@ collection: the model and its entries both point at the page (`content_models.pa
|
|
|
46
46
|
A listing elsewhere (latest posts on home, related posts on an article) is a `collection` block
|
|
47
47
|
with `source: { modelId }`, never copied entries. It also takes `filters` (max 5, `{ field, op,
|
|
48
48
|
value }` with the content API operators), `sort` (`-publishedAt,title`), `pinnedIds` (max 10,
|
|
49
|
-
shown first), `excludeCurrent` and `limit
|
|
49
|
+
shown first), `excludeCurrent` and `limit` (default 20), beside `order` (`newest` or `oldest`) and
|
|
50
|
+
`emptyText`. `cardComponentId` names the component each card is drawn through; without one a card
|
|
51
|
+
shows the entry's `titleField` (default `title`) and `excerptField`. On a `dynamic` page the block
|
|
52
|
+
lists that page's collection and draws ONE entry on `/<page>/<entrySlug>`, through
|
|
53
|
+
`detailComponentId` when set. BetterCMS-rendered pages read it on every request;
|
|
50
54
|
a route the repo builds does not draw this block, so list there with the content API.
|
|
51
55
|
|
|
52
56
|
| Source pattern | BetterCMS destination |
|
|
@@ -126,7 +130,7 @@ sitemap), RSS (dated by `publishedAt`), `llms.txt` (one link per published item)
|
|
|
126
130
|
- **Lists are arrays; tags are taxonomy.** Features, steps, benefits and "what's included" are an
|
|
127
131
|
`array` (`itemType: "text"`), or a repeater when each item carries its own state (included or
|
|
128
132
|
not, an icon). Never a group holding one field. A label set that filters or classifies entries
|
|
129
|
-
is a
|
|
133
|
+
is a `tags` field; when each label has its own page, a collection referenced with `multi-reference` (§2).
|
|
130
134
|
- **Never freeze a feed.** A list of entries on the source stays a listing: copying the nine
|
|
131
135
|
visible cards into a repeater stops it updating. On a page BetterCMS renders, that is the
|
|
132
136
|
`collection` block: on the collection's own dynamic page it lists that collection, and with
|
|
@@ -301,7 +305,8 @@ production cannot store it. Translate before the first write.
|
|
|
301
305
|
| `string` | `text` with `richText: false` | `text` |
|
|
302
306
|
| `text` | `longtext` | `text` |
|
|
303
307
|
| `richtext` | `richtext` for section copy; `document` for the one body | `richtext` |
|
|
304
|
-
| `number
|
|
308
|
+
| `number` | `number` | `number` |
|
|
309
|
+
| `rating` | `number` with `config.min`, `max`, `step` and `ui.control: "rating"` (stars) | `number` |
|
|
305
310
|
| `boolean` | `boolean` | `boolean` |
|
|
306
311
|
| `slug` | `slug` (an entry's own slug stays on the entry) | `text` |
|
|
307
312
|
| `url`, `link` | `link` | `link`; `url` for a media or embed address |
|
|
@@ -309,10 +314,11 @@ production cannot store it. Translate before the first write.
|
|
|
309
314
|
| `date`, `datetime` | `date`, `datetime` | `date` (`YYYY-MM-DD`); a datetime stays `text` |
|
|
310
315
|
| `color` | `color` | `color` |
|
|
311
316
|
| `select` | `select` | `select` with `config.options` |
|
|
312
|
-
| `multiselect
|
|
317
|
+
| `multiselect` | `multiselect` with `config.options` (a closed `string[]` choice list; `min`/`max` count the choices) | `multiselect` with `config.options` |
|
|
318
|
+
| `tags` | `tags` (a `string[]`; `config.options` are suggestions, `allowCustom: false` closes the list); a taxonomy with its own pages is a collection plus `multi-reference` | `tags` |
|
|
313
319
|
| `image` | `image` | `image` |
|
|
314
320
|
| `file`, `video` | `file`; a hosted video URL is a `link` | `file`; `url` for a hosted video |
|
|
315
|
-
| `icon` | `
|
|
321
|
+
| `icon` | `icon` (an icon name or an uploaded image) | `icon` (an icon name or an image) |
|
|
316
322
|
| `button` | `button` | `button`: one prop targeting the `props` of a `button` block (§4) |
|
|
317
323
|
| `reference` | `reference`; `multi-reference` for many | none: keep it on the model |
|
|
318
324
|
| `code` | `longtext` | `text` |
|
|
@@ -323,12 +329,14 @@ production cannot store it. Translate before the first write.
|
|
|
323
329
|
| `array` of objects | `repeater` | `table` |
|
|
324
330
|
| `array` of strings | `array` (`itemType: "text"`) | `table` |
|
|
325
331
|
| `array` of several item shapes | `modular` with `config.blockSlugs` (create the block models first) | separate components |
|
|
326
|
-
| `slot` | `sections` or `component-ref` | `slot` (one instance) |
|
|
332
|
+
| `slot` | `sections` or `component-ref` | `slot` (one instance; a list with `config.multiple`) |
|
|
327
333
|
| a form `reference` | a `form` block inside the section | `form` |
|
|
328
334
|
|
|
329
|
-
V2 features with no production shape are dropped and reported, never faked:
|
|
330
|
-
per-breakpoint values
|
|
331
|
-
|
|
335
|
+
V2 features with no production shape are dropped and reported, never faked: a per-cell table
|
|
336
|
+
type, per-breakpoint values (only `style.hideOn` differs by breakpoint) and per-field
|
|
337
|
+
localization. Two have a shape now: typed table columns are a `repeater` with one field per column
|
|
338
|
+
and `ui.layout: "table"`, and a slot with several children is a `slot` prop with `config.multiple`
|
|
339
|
+
([components.md](components.md) §1).
|
|
332
340
|
|
|
333
341
|
| The plan says | Production word |
|
|
334
342
|
|---|---|
|