@bettercms-ai/mcp 0.70.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 +161 -52
- 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 +2 -2
- package/dist/skills/bettercms/references/bindings.md +33 -3
- package/dist/skills/bettercms/references/components.md +20 -8
- package/dist/skills/bettercms/references/content-modeling.md +8 -3
- package/dist/skills/bettercms/references/detection.md +2 -2
- package/dist/skills/bettercms/references/finish-site.md +80 -2
- package/dist/skills/bettercms/references/media-and-brand.md +22 -1
- package/dist/skills/bettercms/references/next.md +4 -3
- package/dist/skills/bettercms/references/publishing.md +31 -0
- package/dist/skills/bettercms/references/worked-examples.md +4 -2
- 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:
|
|
@@ -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
|
|
@@ -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,7 +38,10 @@ 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
47
|
- `select` and `multiselect` need `config.options`; `slot` holds one nested component instance,
|
|
@@ -123,8 +126,9 @@ source? } }`.
|
|
|
123
126
|
Default, Featured and Outline is ONE component with a `select` prop keyed `variant` whose options
|
|
124
127
|
are the looks, and its markup picks classes from that value. Why: an editor switches the look in
|
|
125
128
|
place and the content stays put, and the library holds one card, not three. Families of looks
|
|
126
|
-
(`variantOf`, a style select on every section that places them) are for buttons
|
|
127
|
-
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).
|
|
128
132
|
- Read `warnings` and act on them; they never block the write:
|
|
129
133
|
- `create_component` / `update_component`: `VARIANT_PROP_KEYS_MISMATCH` (a key no sibling
|
|
130
134
|
declares, or one siblings declare that this variant lacks) and `VARIANT_PROP_TYPE_MISMATCH`
|
|
@@ -214,8 +218,16 @@ A code-built site ships only what its templates draw, whatever the page's `block
|
|
|
214
218
|
the repo root (`--dry-run` first); give each root it reports as skipped (`OWN_STYLE`, `SPREAD`,
|
|
215
219
|
`BLOCK_ID_ONLY`, …) its block style by hand, confirm with `--verify-styles`, push and release.
|
|
216
220
|
- **A `button` field renders with `BcmsButton`** (`path`, `value`): one `<a data-bcms-button>`
|
|
217
|
-
carrying `data-variant` and `data-size`, so
|
|
218
|
-
|
|
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.
|
|
219
231
|
Without the SDK, write that same anchor: `data-bcms-button="<path>"`, the label in its own
|
|
220
232
|
`<span data-bcms-field="<path>.label">`, the href through `data-bcms-props`. The arrow is the
|
|
221
233
|
`svg.bcms-btn-arrow` it draws; never leave a hand-written arrow `<svg>` beside it, or turning
|
|
@@ -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
|
|
@@ -310,7 +314,8 @@ production cannot store it. Translate before the first write.
|
|
|
310
314
|
| `date`, `datetime` | `date`, `datetime` | `date` (`YYYY-MM-DD`); a datetime stays `text` |
|
|
311
315
|
| `color` | `color` | `color` |
|
|
312
316
|
| `select` | `select` | `select` with `config.options` |
|
|
313
|
-
| `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` |
|
|
314
319
|
| `image` | `image` | `image` |
|
|
315
320
|
| `file`, `video` | `file`; a hosted video URL is a `link` | `file`; `url` for a hosted video |
|
|
316
321
|
| `icon` | `icon` (an icon name or an uploaded image) | `icon` (an icon name or an image) |
|
|
@@ -49,7 +49,7 @@ a product). One page cannot tell chrome from content.
|
|
|
49
49
|
|---|---|---|
|
|
50
50
|
| Real headings and paragraphs inside `<main>` | Server-rendered or static | Parse the raw HTML. Render once anyway to catch client-only bands |
|
|
51
51
|
| An empty `#root`, `#__next`, `#app`, and large script bundles | Client-rendered | Render in a browser and read the rendered DOM |
|
|
52
|
-
| `<astro-island>`, `__NEXT_DATA__`, `window.__NUXT__` | Hydrated islands | Parse the HTML, then render to read the islands. Their inline JSON often holds the data |
|
|
52
|
+
| `<astro-island>`, `__NEXT_DATA__`, `window.__NUXT__` | Hydrated islands | Parse the HTML, then render to read the islands. Their inline JSON often holds the data. Binding their copy: [animation.md](animation.md) §9 |
|
|
53
53
|
|
|
54
54
|
When you render:
|
|
55
55
|
|
|
@@ -139,7 +139,7 @@ match when the sequence is equal and optional parts are missing only at the ends
|
|
|
139
139
|
| Siblings of different kinds in one grid (a card, a quote, a big number) | The section's ordered children. When the editor picks and orders them, a `slot` prop with `config.multiple`; a plain `slot` holds ONE instance |
|
|
140
140
|
| Same shape, each item linking to its own page | Entries of a collection |
|
|
141
141
|
|
|
142
|
-
Count content levels, not DOM nodes. Component props nest at most
|
|
142
|
+
Count content levels, not DOM nodes. Component props nest at most ten levels, counting the prop; a deeper subtree
|
|
143
143
|
becomes its own component, placed as a child.
|
|
144
144
|
|
|
145
145
|
**Collections.** A set of records with one shape and their own URLs.
|
|
@@ -31,8 +31,86 @@ answers `complete: true` (no path left in `paths.pending`), and after the deploy
|
|
|
31
31
|
`declared`. A build job works from `bettercms://playbook/schema` (the `build_site` prompt carries
|
|
32
32
|
the same flow on a local MCP) and is done when the site is deployed, `get_component_readiness`
|
|
33
33
|
is clean for every component it placed, and `get_next_steps` lists nothing left to do from code.
|
|
34
|
-
Both end in the verify loop of SKILL.md
|
|
35
|
-
|
|
34
|
+
Both end in the verify loop of SKILL.md, and an imported site ends only on the setup gate below.
|
|
35
|
+
One converted page, or a receipt with paths still pending, is not a finished site.
|
|
36
|
+
|
|
37
|
+
## The setup gate: what "finished" is measured by
|
|
38
|
+
|
|
39
|
+
`get_next_steps` returns `receipt.complete` and `receipt.gate`. The platform computes the gate
|
|
40
|
+
from the last release, one verdict per route of the live build. The CLI that launched you reads
|
|
41
|
+
it again when you exit and prints it to the user, so the gate is the report, not your summary.
|
|
42
|
+
While the gate is incomplete that command exits 3 (0 once it is complete; 2 is a wrong command
|
|
43
|
+
line), which is what a script waiting on the setup reads.
|
|
44
|
+
|
|
45
|
+
| Check | Passes when | Where it shows |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `page` | a CMS page, or a collection entry, serves the route | `routes.rows[].page` |
|
|
48
|
+
| `published` | that page, and its entry, is published | `routes.rows[].page` |
|
|
49
|
+
| `twin` | no second page claims the same route | `blocking.rows` |
|
|
50
|
+
| `text` | 2% or less of the visible characters sit outside a field | `routes.rows[].text` |
|
|
51
|
+
| `image` | each image a visitor sees belongs to a field, and the release named no picture it could not make one | `routes.rows[].image`, `imageGaps` |
|
|
52
|
+
| `link` | 95% of the links have a field for the label or the destination | `routes.rows[].link` |
|
|
53
|
+
|
|
54
|
+
`pages` counts routes built, created and published. `coverage` sums `editable` of `total` across
|
|
55
|
+
the site. `routes.rows` is ordered worst first, with `failing` per route and the reasons the
|
|
56
|
+
deriver `skipped` copy there. `blocking.rows` is the work list: each row carries a `detail` with
|
|
57
|
+
its number and a `fix`, the single call to run next. Run the first `fix`, call `get_next_steps`,
|
|
58
|
+
repeat until `complete`. A `fix` of `null` means no tool closes that row (a collection's listing
|
|
59
|
+
route, a URL that cannot be placed, two pages nobody but the owner can choose between): put it to
|
|
60
|
+
the user and stop retrying. Treat a row that its fix did not move the same way.
|
|
61
|
+
|
|
62
|
+
A `page` row on a route setup left without a page carries the pages lane's reason in its
|
|
63
|
+
`detail` (`invalid-path`, `html-too-large`, `page-deleted`, `slug-taken`, `folder-conflict`,
|
|
64
|
+
`entry-not-registered`, `authored-content` and the rest), with what closes it: a change in the
|
|
65
|
+
site's code, another page to move, or the user's decision. Only `unreadable`, `write-failed`
|
|
66
|
+
and `not-registered` carry a `fix`. An `image` row with a `null` fix names a picture by its code
|
|
67
|
+
([media-and-brand.md](media-and-brand.md) Where images hide): the change is in the site's code.
|
|
68
|
+
`imageGaps` also lists the decoration the import declined, which blocks nothing. On a
|
|
69
|
+
collection's listing route the rows that link to its entries are counted as the collection's,
|
|
70
|
+
so the listing page is judged on its own heading, intro and calls to action.
|
|
71
|
+
|
|
72
|
+
### Exceptions go on the record
|
|
73
|
+
|
|
74
|
+
A check you are leaving on purpose is recorded, one row per route and check, with
|
|
75
|
+
`editor_setup` and `exempt: [{ route, check, reason, note }]`. `note` states what is being left
|
|
76
|
+
in a sentence a person can verify. `reason: null` withdraws one.
|
|
77
|
+
|
|
78
|
+
| `reason` | May excuse |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `form-validation-copy` | `text` |
|
|
81
|
+
| `legal-boilerplate` | `text` |
|
|
82
|
+
| `third-party-widget` | `text`, `image`, `link` |
|
|
83
|
+
| `decorative` | `text`, `image` |
|
|
84
|
+
| `duplicate-track` | `text`, `image`, `link` (the cloned track of a marquee or slider) |
|
|
85
|
+
| `client-only-island` | nothing: it lists an island the build cannot show, and the route still meets the bar outside it |
|
|
86
|
+
| `user-declined` | every check except `twin`, and only with the user's own words quoted in `note` (`User: "leave pricing in code"`) |
|
|
87
|
+
|
|
88
|
+
A row excuses what its check read when it was recorded. When the route reads differently
|
|
89
|
+
later (more copy nobody owns, another picture), the row stops counting and the check blocks
|
|
90
|
+
again: record it again if it still stands. The CLI prints every `user-declined` row marked for
|
|
91
|
+
the user to confirm.
|
|
92
|
+
|
|
93
|
+
Refused whatever the note says: `out-of-time`, `out-of-context`, `too-many-routes`,
|
|
94
|
+
`tool-failed`, `not-asked`, `good-enough`. A sentence to the user about a gap is not an
|
|
95
|
+
exception either: the gate stays incomplete, and the CLI exits non-zero with that gap listed.
|
|
96
|
+
|
|
97
|
+
### What setup derives on a collection's item pages
|
|
98
|
+
|
|
99
|
+
Setup reads a few entry pages of each collection and makes one field of everything they render
|
|
100
|
+
at the same place: text, each image (an `<img>` with only a `srcset`, a `<picture>`, a
|
|
101
|
+
background its stylesheet paints), a call to action (one `button`, or a label and its `-href`
|
|
102
|
+
where the project derives pairs), a plain list, and a gallery or spec table as a repeater whose
|
|
103
|
+
rows each entry fills from its own page. The article's container becomes the place the entry's
|
|
104
|
+
`body` is read from and bound, so `body` holds the article alone. An element the site's code
|
|
105
|
+
already declares keeps its declaration and gets no second field. A collection from before this
|
|
106
|
+
gains the fields add-only; its `body` is bound only while no entry of it was edited. Check the
|
|
107
|
+
result per entry route in the gate, never by adding the same fields by hand.
|
|
108
|
+
|
|
109
|
+
Copy the template shows unchanged on every entry (a "Related posts" heading, an "Add to cart"
|
|
110
|
+
button) is one field marked `config.shared`, and its value lives on that field, once: no entry
|
|
111
|
+
holds it. Edit it as a field of any one entry and publish that entry: the value moves onto the
|
|
112
|
+
field and every item page is repainted. Until then it is that entry's draft, shown on no page.
|
|
113
|
+
A translated entry keeps its own words for it. Never write the same value into each entry.
|
|
36
114
|
|
|
37
115
|
Git belongs to the owner. Verification releases go out from the working tree, so the job never
|
|
38
116
|
needs a commit to prove itself. After the final report, ask whether they want the changes
|
|
@@ -72,7 +72,8 @@ H1. Expose typography variants only where the design allows a real choice.
|
|
|
72
72
|
not in each page.
|
|
73
73
|
- **Images.** A stored image value is an object (`url`, `altText`, asset id), never a bare
|
|
74
74
|
string; read it as one. Alt text, decorative state and caption belong to the usage. Keep
|
|
75
|
-
`srcset`/`sizes`, poster, lazy/preload intent and art direction
|
|
75
|
+
`srcset`/`sizes`, poster, lazy/preload intent and art direction (a publish keeps them too:
|
|
76
|
+
[bindings.md](bindings.md) Images). Decorative images render an
|
|
76
77
|
empty `alt`; informative ones need text that fits the context. `update_media` with
|
|
77
78
|
`decorative: true` marks a library asset decorative, so the audit stops asking for its alt text.
|
|
78
79
|
- **Request the size you render.** Mint resize URLs with `@bettercms-ai/image-url`
|
|
@@ -112,6 +113,26 @@ H1. Expose typography variants only where the design allows a real choice.
|
|
|
112
113
|
every video into the media library by default; keep a streaming provider reference where the
|
|
113
114
|
site depends on one.
|
|
114
115
|
|
|
116
|
+
## Where images hide
|
|
117
|
+
|
|
118
|
+
Setup makes every picture an image field backed by a library asset, in code, with no step for
|
|
119
|
+
you: the release adopts each media file the build ships (alt text from the markup, its size, and
|
|
120
|
+
a default focal point you can move with `update_media`), derives the fields, and links each
|
|
121
|
+
value to its asset. Check it, never redo it: the binding report's `elementCoverage.image` is
|
|
122
|
+
`editable / total`, and `imageGaps` names what is not a field, misses first.
|
|
123
|
+
|
|
124
|
+
| Code | The picture | What to do |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `IMG_DATA_URI`, `IMG_NO_SOURCE` | An `<img>` inlined as `data:`, or filled in by a script | Ship it as a file with a real `src` |
|
|
127
|
+
| `BG_PSEUDO_ELEMENT` | A background on `::before` / `::after` | Move it onto the element, or render an `<img>` |
|
|
128
|
+
| `BG_ON_TEXT` | A background on an element that is itself a text field | Wrap the text, so the box and the words are two elements |
|
|
129
|
+
| `SVG_TOO_LARGE` | An inline `<svg>` illustration past the icon size | Ship it as an `.svg` file in an `<img>` |
|
|
130
|
+
| `SVG_SPRITE`, `SVG_NOT_ICON`, `SVG_DECORATIVE` | A `<use>` sprite, a chart, a nameless glyph | Decoration: leave it, or give a logo `role="img"` and an `aria-label` |
|
|
131
|
+
| `BG_SPRITE`, `BG_TINY`, `BG_PATTERN`, `BG_DATA_URI`, `BG_STATE` | A sprite, an icon-sized box, a tile, an inlined texture, a hover image | Decoration: leave it |
|
|
132
|
+
|
|
133
|
+
The first four rows count against the image coverage; the last two do not. A gap you cannot
|
|
134
|
+
close is reported with its code, never described as done.
|
|
135
|
+
|
|
115
136
|
## Media host
|
|
116
137
|
|
|
117
138
|
Use `mediaBaseUrl` from `get_project`: media is served from the CDN origin or the API origin of
|
|
@@ -40,14 +40,15 @@ webhook. Use the project's webhook secret from the dashboard (the package docs n
|
|
|
40
40
|
|---|---|
|
|
41
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
42
|
| `readComponents()` | Component definitions from `bcms-content.json` |
|
|
43
|
-
| `BcmsField` | `path`, `kind` (`richtext`/`image`), `as`; server-safe |
|
|
44
|
-
| `BcmsButton` | A `button` field: `path`, `value`, `className`; one `<a>` with `data-variant` / `data-size
|
|
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
45
|
| `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) |
|
|
46
46
|
| `bind`, `bindProps` | Binding attributes; instance-aware inside components |
|
|
47
47
|
| `BcmsForm` + `getForm(idOrName)` | Forms from the snapshot ([forms-and-interactions.md](forms-and-interactions.md)) |
|
|
48
48
|
| `buildMetadata(page, siteDefaults, location?)` | Next `Metadata` from native SEO fields |
|
|
49
49
|
| `llmsTxtRoute(config)` | An `llms.txt` route handler |
|
|
50
|
-
| `imageUrlBuilder({ baseUrl })` | Resize URLs
|
|
50
|
+
| `imageUrlBuilder({ baseUrl })` | Resize URLs; same media-host rules as Astro |
|
|
51
|
+
| `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 |
|
|
51
52
|
|
|
52
53
|
Rules:
|
|
53
54
|
|