@bettercms-ai/mcp 0.70.0 → 0.72.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 +504 -76
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +16 -7
- package/dist/skills/bettercms/references/animation.md +251 -0
- package/dist/skills/bettercms/references/astro.md +47 -2
- package/dist/skills/bettercms/references/bindings.md +47 -4
- package/dist/skills/bettercms/references/componentize.md +65 -6
- package/dist/skills/bettercms/references/components.md +50 -11
- package/dist/skills/bettercms/references/content-modeling.md +14 -4
- package/dist/skills/bettercms/references/detection.md +2 -2
- package/dist/skills/bettercms/references/finish-site.md +188 -3
- package/dist/skills/bettercms/references/forms-and-interactions.md +7 -0
- package/dist/skills/bettercms/references/getting-started.md +2 -0
- package/dist/skills/bettercms/references/media-and-brand.md +39 -1
- package/dist/skills/bettercms/references/next.md +63 -3
- package/dist/skills/bettercms/references/publishing.md +100 -1
- package/dist/skills/bettercms/references/seo.md +2 -1
- package/dist/skills/bettercms/references/worked-examples.md +4 -2
- package/package.json +2 -2
|
@@ -59,7 +59,10 @@ These hold on every task. The reference in brackets has the detail and the codes
|
|
|
59
59
|
beside a link field. [components.md, content-modeling.md]
|
|
60
60
|
4. **Nothing is live until it is published, and a page is not its content.** A draft form,
|
|
61
61
|
component or entry renders nothing live. Read `warnings` on every write: `ENTRY_STILL_DRAFT`
|
|
62
|
-
and `ENTRY_SEED_FAILED` mean content did not ship.
|
|
62
|
+
and `ENTRY_SEED_FAILED` mean content did not ship. An answer that leads with `held`, and a
|
|
63
|
+
publish reading `outcome.status: "partial"`, did not reach the site: do the action it names
|
|
64
|
+
before you report it.
|
|
65
|
+
[publishing.md]
|
|
63
66
|
5. **Name the folder system every time you say "folder"**: Content sidebar folders never touch a
|
|
64
67
|
URL, URL page folders are part of the path, media folders organise assets.
|
|
65
68
|
[content-modeling.md]
|
|
@@ -126,8 +129,9 @@ resources: `bettercms://skill/bettercms` for this file, and each reference under
|
|
|
126
129
|
| Components, props, variants, component publish codes | [components.md](references/components.md) | creating, swapping, publishing or validating a component | component, prop, variant, sectionType, publish component |
|
|
127
130
|
| One section, source HTML to bound template: hero, feature grid, pricing table, testimonials, FAQ | [worked-examples.md](references/worked-examples.md) | you want the whole path for one section in production shapes | example, hero, pricing, FAQ, testimonial, source to schema |
|
|
128
131
|
| 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
|
-
| 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 |
|
|
132
|
+
| 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, BcmsSections, --sections |
|
|
130
133
|
| 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 |
|
|
134
|
+
| 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
135
|
| 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
136
|
| Astro adapter | [astro.md](references/astro.md) | the repo has `astro.config.*` | astro.config, Astro, BcmsBlocks, getStaticPaths |
|
|
133
137
|
| Next.js adapter | [next.md](references/next.md) | the repo has `next.config.*` | next.config, Next.js, App Router, ISR, notFound, static export |
|
|
@@ -139,7 +143,7 @@ resources: `bettercms://skill/bettercms` for this file, and each reference under
|
|
|
139
143
|
| SEO and AEO | [seo.md](references/seo.md) | meta, JSON-LD, sitemap, robots, redirects, AI crawlers, `list_seo_issues` | SEO, meta, JSON-LD, schema.org, sitemap, robots, OG image, redirect, 410, llms.txt |
|
|
140
144
|
| Accessibility: alt text, form labels, contrast, the audit codes and the manual checks | [accessibility.md](references/accessibility.md) | writing alt text; building a form; before reporting a site done | accessibility, WCAG, alt text, decorative, label, contrast, captions |
|
|
141
145
|
| Page families from data | [programmatic-seo.md](references/programmatic-seo.md) | a keyword list or CSV should become pages | programmatic SEO, CSV, dataset, landing pages, glossary |
|
|
142
|
-
| Publish, deploy, roll back, reconcile a re-import | [publishing.md](references/publishing.md) | a release; a code push that conflicts with CMS edits | publish, deploy, rollback, sync, webhook, schedule, automation |
|
|
146
|
+
| Publish, deploy, roll back, reconcile a re-import; add a section to a page or set its spacing | [publishing.md](references/publishing.md) | a release; a code push that conflicts with CMS edits; "add a section here", "more padding on this section"; a write that answers `held` or a publish that reads `partial` | publish, deploy, rollback, sync, webhook, schedule, automation, add section, insertSection, sectionStyle, spacing, padding, margin, held, partial |
|
|
143
147
|
| Read-only launch review | [site-review.md](references/site-review.md) | "is this ready to launch?" without fixing | launch review, audit, is it ready, QA |
|
|
144
148
|
| A/B tests on section variants | [content-experiments.md](references/content-experiments.md) | an experiment plan or readout | A/B test, experiment, variant test |
|
|
145
149
|
| Headless Shopify storefront | [shopify-headless.md](references/shopify-headless.md) | Shopify owns commerce, BetterCMS the editorial | Shopify, Hydrogen, storefront, product |
|
|
@@ -181,7 +185,12 @@ Stop at the first step that is not clean, fix, then restart. A report that does
|
|
|
181
185
|
3. `get_site_composition`: every page you touched is made of sections, with the intended
|
|
182
186
|
components.
|
|
183
187
|
4. `list_seo_issues`: no issue on a page you created or edited.
|
|
184
|
-
5. `get_next_steps`:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
5. `get_next_steps`: `receipt.complete` is `true`. While it is `false`, run
|
|
189
|
+
`receipt.gate.nextAction` and read again after each batch; nothing else ends an imported
|
|
190
|
+
site's setup, and an exception is recorded only for a reason that is true. The gate, its bars and the exceptions it accepts are in
|
|
191
|
+
[finish-site.md](references/finish-site.md). Suggestions still in `missing` after that, and
|
|
192
|
+
owner actions, go in your final message by name.
|
|
193
|
+
|
|
194
|
+
"Done" means the loop is clean and the gate reads complete, not that a tool call returned 200.
|
|
195
|
+
`bettercms init` reads the gate again when you exit and prints its numbers per page, so the
|
|
196
|
+
user sees them whatever your last message said.
|
|
@@ -0,0 +1,251 @@
|
|
|
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
|
+
The bound element picks the writer. One that holds text and nothing else is replaced whole, as
|
|
16
|
+
it always was. One that holds markup (inline elements, split spans, a `<br>`, or
|
|
17
|
+
`data-bcms-split`) is written into its text nodes, and every element inside it is kept. That is
|
|
18
|
+
what the recipes below rely on:
|
|
19
|
+
|
|
20
|
+
| The page has | A publish does |
|
|
21
|
+
|---|---|
|
|
22
|
+
| 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 |
|
|
23
|
+
| Text cut into `word` / `char` / `line` spans, same word count | writes each span in place |
|
|
24
|
+
| 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 |
|
|
25
|
+
| The same, spans written by your own template | rebuilds them from their own pattern, a new one copied from the last |
|
|
26
|
+
| A split headline under an `aria-label` that says the same words (SplitText's own output) | one field on the headline: writes the label and the words |
|
|
27
|
+
| A counter with its number in `data-target`, `data-count` or a like attribute | writes the attribute; writes the number shown only where it showed the old target, never a script's starting `0` |
|
|
28
|
+
| A copy of the value under `aria-hidden="true"` or `data-bcms-clone` | writes the copy too (coverage kind `duplicate-track`) |
|
|
29
|
+
| The value inside `astro-island` props, `__NEXT_DATA__` or the app router's flight scripts | rewrites the serialized string (coverage kind `island-props`) |
|
|
30
|
+
|
|
31
|
+
It refuses, and says so on the publish, when the change would have to remove something:
|
|
32
|
+
`TEXT_IN_CHILD_FIELD` (the words belong to a field declared inside this one),
|
|
33
|
+
`TEXT_EMPTIES_ELEMENT` (a link, a scripted element or a wrapper with attributes would be left empty),
|
|
34
|
+
`SPLIT_PATTERN_UNKNOWN` (split markup that is not one row of like tokens) and
|
|
35
|
+
`ISLAND_PROPS_UNMATCHED` (a hydrated island draws the text from props that do not hold it).
|
|
36
|
+
Each is fixed in the template with the recipe for its library.
|
|
37
|
+
|
|
38
|
+
The last two rows follow a value only where the element was written into its text nodes. An
|
|
39
|
+
operator can switch a project, or the fleet, to write every text value that way, so a plain-text
|
|
40
|
+
value's copies and props follow it too; and can switch the text-node writer off, after which a
|
|
41
|
+
publish replaces an element's content with plain text and refuses what that would break.
|
|
42
|
+
|
|
43
|
+
## 2. Two rules for every library
|
|
44
|
+
|
|
45
|
+
1. **Bind the element that holds the words, never the element the animation owns.** A pinned
|
|
46
|
+
section, a scroll wrapper, a slider track and a motion container are layout. Their text
|
|
47
|
+
leaves are the fields.
|
|
48
|
+
2. **Refresh measurements after an edit.** The editor posts `{ type: "bcms:content-applied" }`
|
|
49
|
+
to the page's window after it applies an edit. Longer copy changes heights, so anything that
|
|
50
|
+
measured the page must measure again:
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
window.addEventListener("message", (event) => {
|
|
54
|
+
if (event.data?.type !== "bcms:content-applied") return;
|
|
55
|
+
ScrollTrigger.refresh(); // or lenis.resize(), swiper.update(), split.revert() then split again
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`onContentApplied(handler)` from `@bettercms-ai/sdk` is the same listener with the check
|
|
60
|
+
written for you. On a published page nothing posts the message and the listener never runs.
|
|
61
|
+
|
|
62
|
+
## 3. GSAP and ScrollTrigger
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<section class="pin-wrap"> <!-- trigger and pin: no field -->
|
|
66
|
+
<div class="pin">
|
|
67
|
+
<h2 data-bcms-field="story.title">Built to last</h2>
|
|
68
|
+
<p data-bcms-field="story.lead">Forged, not cast.</p>
|
|
69
|
+
</div>
|
|
70
|
+
</section>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
gsap.to(".pin h2", { yPercent: -20, scrollTrigger: { trigger: ".pin-wrap", pin: ".pin", scrub: 1 } });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Never put `data-bcms-field` on `.pin-wrap` or `.pin`: ScrollTrigger wraps a pinned element in
|
|
78
|
+
a spacer and writes inline styles on it every frame. Use `<BcmsField>` only around a single
|
|
79
|
+
element, where it adds no wrapper (section 10); a wrapper span between `.pin` and its `h2`
|
|
80
|
+
would break `.pin > h2`.
|
|
81
|
+
|
|
82
|
+
Check: with the page open, `ScrollTrigger.getAll().length` is the same before and after a
|
|
83
|
+
publish of every field on the route.
|
|
84
|
+
|
|
85
|
+
## 4. SplitText, SplitType, Splitting.js
|
|
86
|
+
|
|
87
|
+
Bind the parent and say that it is split. Never bind a word or a character.
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<h1 class="js-split" data-bcms-field="hero.title" data-bcms-split="words">Fast screws, shipped</h1>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
let split = new SplitText(".js-split", { type: "words" });
|
|
95
|
+
window.addEventListener("message", (event) => {
|
|
96
|
+
if (event.data?.type !== "bcms:content-applied") return;
|
|
97
|
+
split.revert();
|
|
98
|
+
split = new SplitText(".js-split", { type: "words" });
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`data-bcms-split` takes `chars`, `words` or `lines`. It tells the editor that the spans under
|
|
103
|
+
this element are one value, and it tells a publish that the children are tokens even when their
|
|
104
|
+
class is not `word`, `char` or `line`.
|
|
105
|
+
|
|
106
|
+
When your own template writes the spans (`title.split(" ").map(...)`), keep them one row of
|
|
107
|
+
the same tag, with whitespace or nothing between them, and no other element in the row:
|
|
108
|
+
|
|
109
|
+
```astro
|
|
110
|
+
<h1 data-bcms-field="hero.title" data-bcms-split="words">
|
|
111
|
+
{title.split(" ").map((word) => <span class="word">{word}</span>)}
|
|
112
|
+
</h1>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A `<br>` between two word spans makes the row unreadable once the word count changes
|
|
116
|
+
(`SPLIT_PATTERN_UNKNOWN`): put the break in CSS, or split by lines.
|
|
117
|
+
|
|
118
|
+
Check: publish a title with one more word, reload, and count `.word`: it equals the new word
|
|
119
|
+
count and no word sits outside a span.
|
|
120
|
+
|
|
121
|
+
## 5. Framer Motion
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
<motion.section initial={{ opacity: 0 }} whileInView={{ opacity: 1 }}> {/* no field */}
|
|
125
|
+
<motion.h2 data-bcms-field="story.title" variants={rise}>{story.title}</motion.h2>
|
|
126
|
+
<p data-bcms-field="story.lead">{story.lead}</p>
|
|
127
|
+
</motion.section>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Put `data-bcms-field` straight on the motion element that renders the text: it passes
|
|
131
|
+
`data-*` through to the DOM. Do not wrap a motion component in `<BcmsField>`; a component child
|
|
132
|
+
keeps the wrapper span, and `layout` animations measure the wrong box. For words animated one
|
|
133
|
+
by one, map them to `motion.span` under a parent carrying `data-bcms-split="words"`, as in
|
|
134
|
+
section 4. The component hydrates, so section 9 applies to its props.
|
|
135
|
+
|
|
136
|
+
Check: the built HTML for the route shows `data-bcms-field` on the `h2` itself, with no
|
|
137
|
+
`display:contents` span around it.
|
|
138
|
+
|
|
139
|
+
## 6. Lenis and other smooth scroll
|
|
140
|
+
|
|
141
|
+
Lenis moves the page, not the content, so no binding changes. Two things do:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
const lenis = new Lenis();
|
|
145
|
+
window.addEventListener("message", (event) => {
|
|
146
|
+
if (event.data?.type === "bcms:content-applied") lenis.resize();
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
and any panel of yours that must scroll by itself inside the page takes `data-lenis-prevent`.
|
|
151
|
+
Never bind the element Lenis is given as `wrapper` or `content`.
|
|
152
|
+
|
|
153
|
+
Check: after an edit that adds two lines to a paragraph, the page still scrolls to the footer.
|
|
154
|
+
|
|
155
|
+
## 7. Marquees and duplicated tracks
|
|
156
|
+
|
|
157
|
+
Render the track once with fields, and mark every copy:
|
|
158
|
+
|
|
159
|
+
```astro
|
|
160
|
+
<div class="marquee">
|
|
161
|
+
<ul class="track">{logos.map((l, i) => <li class="logo" data-bcms-field={`logos[${i}].name`}>{l.name}</li>)}</ul>
|
|
162
|
+
<ul class="track" aria-hidden="true" data-bcms-clone>{logos.map((l) => <li class="logo">{l.name}</li>)}</ul>
|
|
163
|
+
</div>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
A copy under `aria-hidden="true"` or `data-bcms-clone` is not counted as unowned text, and a
|
|
167
|
+
publish writes it with the original when it is the same tag with the same classes and the same
|
|
168
|
+
text. A copy with neither marker is separate copy: it counts against the route's text coverage
|
|
169
|
+
and keeps the old words. A script that clones the track in the browser needs nothing: the clone
|
|
170
|
+
is made from the published HTML on every load.
|
|
171
|
+
|
|
172
|
+
Check: publish one logo name, then `document.querySelectorAll(".track")` shows the new name in
|
|
173
|
+
every track.
|
|
174
|
+
|
|
175
|
+
## 8. Swiper and carousels
|
|
176
|
+
|
|
177
|
+
Bind what is inside a slide, never `.swiper-wrapper` or `.swiper-slide`:
|
|
178
|
+
|
|
179
|
+
```html
|
|
180
|
+
<div class="swiper-slide"><h3 data-bcms-field="slides[0].title">Torque</h3></div>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Loop mode clones slides in the browser (`.swiper-slide-duplicate`); those are made from the
|
|
184
|
+
published HTML and need no field. On `bcms:content-applied`, call `swiper.update()`.
|
|
185
|
+
|
|
186
|
+
Check: `get_binding_report` lists no `slides[n]` path as pending for the route.
|
|
187
|
+
|
|
188
|
+
## 9. Hydrated islands and `client:only`
|
|
189
|
+
|
|
190
|
+
A hydrated component draws itself again from the props the build serialized. Keep the text it
|
|
191
|
+
shows equal to one string prop, whole:
|
|
192
|
+
|
|
193
|
+
```astro
|
|
194
|
+
<Hero client:load title={page.hero.title} />
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
export default function Hero({ title }) { return <h1 data-bcms-field="hero.title">{title}</h1>; }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
A publish then rewrites that prop with the text. It cannot when the component builds the text
|
|
202
|
+
(`${title}!`, `title.toUpperCase()`, a string literal in the component), or when the props hold
|
|
203
|
+
the same words more than once, under a key such as `id` or `slug`, or as `__html`: the publish reports
|
|
204
|
+
`ISLAND_PROPS_UNMATCHED` and the value goes live with the next build. Content passed as a
|
|
205
|
+
slot is plain HTML and needs no prop.
|
|
206
|
+
|
|
207
|
+
`client:only` renders nothing at build time, so the built page has no copy to own. The release
|
|
208
|
+
names every such island on its route and the setup gate lists it as a `client-only-island`
|
|
209
|
+
exception by itself. To make the copy editable, change the directive to `client:load` or
|
|
210
|
+
`client:visible`, or pass the copy in as a slot.
|
|
211
|
+
|
|
212
|
+
Check: view source on the built route, find the `astro-island`, and its `props` attribute holds
|
|
213
|
+
the published title after a publish.
|
|
214
|
+
|
|
215
|
+
## 10. `<BcmsField>` adds no wrapper around one element
|
|
216
|
+
|
|
217
|
+
```astro
|
|
218
|
+
<BcmsField path="hero.title"><h1 class="title">{page.hero.title}</h1></BcmsField>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
renders `<h1 class="title" data-bcms-field="hero.title">`: nothing sits between the parent and
|
|
222
|
+
the heading. Text, several children, and in Next a component child are still wrapped in a
|
|
223
|
+
`display:contents` span, and `as="div"` still gives a real wrapper. A site built with an older
|
|
224
|
+
package version keeps its wrapper spans until it is rebuilt.
|
|
225
|
+
|
|
226
|
+
`<BcmsSections>` follows the same rule for a whole section: the block id, the style marker and
|
|
227
|
+
the block's style go on the section's own root, and in Astro the list is delimited by two HTML
|
|
228
|
+
comments, never an element. `main > section`, `:first-child` and a ScrollTrigger on `.hero` match
|
|
229
|
+
what they matched before. The one exception is a section that is not one element (several roots,
|
|
230
|
+
an island, in Next a component child with no function form): it gets a box of ours, a node
|
|
231
|
+
between `main` and the section. With no style on the block the box is `display:contents` and
|
|
232
|
+
draws nothing. Once the block has a style the box is `display:flow-root` and carries it: one
|
|
233
|
+
block box, so in a flex or grid parent its children are one item instead of several, and their
|
|
234
|
+
margins stop collapsing with the neighbours'. Give such a section one root element, or the
|
|
235
|
+
function form ([next.md](next.md)), when a selector or the layout depends on it.
|
|
236
|
+
|
|
237
|
+
## 11. Lottie, canvas, WebGL, video
|
|
238
|
+
|
|
239
|
+
Pixels drawn by a script are not in the HTML, so nothing in them is a field by itself. What is
|
|
240
|
+
editable is what you pass in:
|
|
241
|
+
|
|
242
|
+
| Drawn by | Make editable by |
|
|
243
|
+
|---|---|
|
|
244
|
+
| 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. |
|
|
245
|
+
| 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`. |
|
|
246
|
+
| Video | The poster and the source are fields on the `<video>`; burned-in captions are not text. |
|
|
247
|
+
|
|
248
|
+
Give the canvas `role="img"` and an `aria-label` bound to a field, or an adjacent caption. A
|
|
249
|
+
scene with no text a visitor reads needs no field; record it under the `decorative` exception.
|
|
250
|
+
|
|
251
|
+
Check: change the bound string, publish, reload: the drawn text reads the new value.
|
|
@@ -40,9 +40,10 @@ build. Pass `apiUrl` and `workspace` as options, or let the snapshot supply them
|
|
|
40
40
|
| Import | Use |
|
|
41
41
|
|---|---|
|
|
42
42
|
| `@bettercms-ai/astro/components/BcmsBlocks.astro` | Server render of a block tree. Pass `components` or instances render nothing |
|
|
43
|
+
| `@bettercms-ai/astro/components/BcmsSections.astro` | The page's own sections as named slots, drawn in the CMS page's order (below) |
|
|
43
44
|
| `@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
|
|
45
|
+
| `@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) |
|
|
46
|
+
| `@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
47
|
| `@bettercms-ai/astro/components/BcmsForm.astro` | Renders a delivered form and submits it ([forms-and-interactions.md](forms-and-interactions.md)) |
|
|
47
48
|
|
|
48
49
|
`BcmsLiveBlocks` rules:
|
|
@@ -51,6 +52,48 @@ build. Pass `apiUrl` and `workspace` as options, or let the snapshot supply them
|
|
|
51
52
|
- Its CSS class names differ from `BcmsBlocks`. Swapping one for the other unstyles a page unless
|
|
52
53
|
your stylesheet covers both spellings.
|
|
53
54
|
|
|
55
|
+
## Sections in the CMS order
|
|
56
|
+
|
|
57
|
+
A page whose sections are written in its `.astro` file follows the CMS page's block list through
|
|
58
|
+
`<BcmsSections>`. `bettercms init` writes this wrap (`npx @bettercms-ai/convert --sections`);
|
|
59
|
+
write it by hand for a page the codemod refused ([componentize.md](componentize.md) §5):
|
|
60
|
+
|
|
61
|
+
```astro
|
|
62
|
+
---
|
|
63
|
+
import BcmsSections from "@bettercms-ai/astro/components/BcmsSections.astro";
|
|
64
|
+
---
|
|
65
|
+
<Layout>
|
|
66
|
+
<BcmsSections page="home">
|
|
67
|
+
<Fragment slot="hero"><Hero title={title} /></Fragment>
|
|
68
|
+
<Fragment slot="code:sec1"><LogoStrip /></Fragment>
|
|
69
|
+
<Fragment slot="pricing">{plans.length > 0 && <Pricing plans={plans} />}</Fragment>
|
|
70
|
+
</BcmsSections>
|
|
71
|
+
</Layout>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `page` is the CMS page's slug. Each section is one `<Fragment slot="…">` around markup you do
|
|
75
|
+
not change. Anything with its own `slot="…"` (it targets the layout) stays outside.
|
|
76
|
+
- A slot is named by its placement's bind: the family of the section's first bound field
|
|
77
|
+
(`pricing-title` → `pricing`), which is the `bind` on the page's `component` block.
|
|
78
|
+
- A block bound to a slot renders it at the block's position. `data-bcms-block`, `data-bcms-style`
|
|
79
|
+
and the block's style go on the section's own root element; no element is added, so the stamp
|
|
80
|
+
never comes between a parent and a child a selector or an animation names
|
|
81
|
+
([animation.md](animation.md) §10). A slot that renders several roots, or an island, is stamped
|
|
82
|
+
on a box: `display:contents` while its block has no style, one `display:flow-root` box once it
|
|
83
|
+
has. Give it one root element where the layout must not gain a box.
|
|
84
|
+
- A section added in the editor (a block with no bind) is drawn by `BcmsBlocks`, which
|
|
85
|
+
`BcmsSections` imports itself: nothing to pass, and it builds whether or not `bettercms()` is
|
|
86
|
+
registered on that build (a config that adds it only with credentials still draws the section).
|
|
87
|
+
- A slot no block names: `code:<bind>` always renders where it is written. A bare `<bind>` does
|
|
88
|
+
too until the list names one of the page's slots; from then on it is a removed section. Write
|
|
89
|
+
a slot bare only when the page already places that bind (`get_page`), otherwise `code:`.
|
|
90
|
+
- No snapshot (a local build, the API down): every slot renders in source order, untouched.
|
|
91
|
+
- The list is the published snapshot's, or the draft's when the Visual Editor frames a
|
|
92
|
+
server-rendered page (`prerender = false`). `blocks={…}` gives it yourself.
|
|
93
|
+
|
|
94
|
+
Check: build, then view source. `<!--bcms-sections:start page="home"-->` precedes the sections,
|
|
95
|
+
and each placed section's root carries `data-bcms-block` and `data-bcms-style`.
|
|
96
|
+
|
|
54
97
|
## Bindings
|
|
55
98
|
|
|
56
99
|
A section root your own code renders spreads `sectionRootAttrs(block)` from `@bettercms-ai/astro`
|
|
@@ -72,5 +115,7 @@ helpers. Use them inside component instances so addresses resolve to the instanc
|
|
|
72
115
|
| Resized images 404 on a non-production site | `mediaUrl` still the production default | Set `PUBLIC_BCMS_MEDIA_URL` to that rung's API origin |
|
|
73
116
|
| Resized images still on the CDN after moving media | `mediaUrl` not changed | Set it to `mediaBaseUrl` from `get_project` |
|
|
74
117
|
| Component instances empty | `components` prop missing | Pass definitions to `BcmsBlocks` |
|
|
118
|
+
| A code section vanished after a build | Its slot is bare and the page's block list does not place its bind | Name it `code:<bind>`, or place the bind on the page |
|
|
119
|
+
| Build fails on `bettercms:config` | A page imports `BcmsSearch` or the virtual module on a build where `bettercms()` is not registered | Register it unconditionally. `BcmsSections`, `BcmsBlocks`, `BcmsImage` and `BcmsForm` need no integration |
|
|
75
120
|
| Draft link 500 | `BCMS_DRAFT_SECRET` unset | Set it on the server env, redeploy |
|
|
76
121
|
| `ReferenceError: X is not defined` from a `[slug].astro` route | `getStaticPaths()` reads a `const` declared in the frontmatter; Astro runs it in its own scope, before the page, seeing only imports | Declare the client or reader call inside `getStaticPaths`, or import it from a module |
|
|
@@ -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
|
|
@@ -64,7 +68,19 @@ value over everything else inside it, so:
|
|
|
64
68
|
`button` field rendered with `BcmsButton` ([components.md](components.md)).
|
|
65
69
|
- **SVG text is never bound; the icon is.** No field is derived from the text inside an `<svg>`,
|
|
66
70
|
and no binding there is measured. Each inline icon IS an editable `image` field (`config.media:
|
|
67
|
-
"icon"`, an svg value): an editor swaps it. Never remove one to tidy a form; group it.
|
|
71
|
+
"icon"`, an svg value): an editor swaps it. Never remove one to tidy a form; group it. So is a
|
|
72
|
+
sprite icon (`<svg><use href="#id">`) whose `<symbol>` is in the same page, a wordmark set in
|
|
73
|
+
`<text>`, and an illustration up to 64 KB. A swap keeps the element's classes and size and
|
|
74
|
+
stays inline, so `currentColor` still applies; scripts and remote references are stripped.
|
|
75
|
+
- **Copy in an attribute is a field of its own.** Every release derives a text field for an
|
|
76
|
+
`aria-label`, a `title`, a `placeholder` outside a form, and the number a count-up script
|
|
77
|
+
counts to (`data-target`, `data-count`, `data-value` and their kin). Its `config.attr` names the
|
|
78
|
+
attribute; it is bound on `data-bcms-props` as `<path>|text|<attribute>` and published into that
|
|
79
|
+
attribute alone, never matched against the page's words. A label that only repeats the words
|
|
80
|
+
on its own element gets none. To declare one in code, write the same `data-bcms-props` entry.
|
|
81
|
+
- **Text beside an icon in a plain element** (`<span>Pages<svg/></span>`) is a field when it is
|
|
82
|
+
the element's one text node: the save writes that node and leaves the icon. Split over several
|
|
83
|
+
text nodes it stays `BARE_TEXT`, so wrap the label in its own `<span>` where you can.
|
|
68
84
|
- **Icon-only links in the nav and footer** take their field name from what a screen reader
|
|
69
85
|
reads: the anchor's `aria-label`, its `sr-only` text, a `title`, the SVG `<title>` or an `alt`
|
|
70
86
|
(never `<style>` or `<script>` text). Give each one an `aria-label` or `sr-only` label, or every
|
|
@@ -92,6 +108,31 @@ the fallback; the build renders it and the publish writes the editor's image. Fr
|
|
|
92
108
|
images are in the Media Library, matched by file name. An image rendered inside a loop over a
|
|
93
109
|
list is not bound automatically: bind it in the list's data file.
|
|
94
110
|
|
|
111
|
+
The codemod knows an image component by its IMPORT, not its name: `Image` and `Picture` from
|
|
112
|
+
`astro:assets` (aliased, or through `import * as assets`), and the default export of
|
|
113
|
+
`next/image` under any name. An import that reaches the page through `getImage()`, `hero.src` or
|
|
114
|
+
a style is refused as `IMAGE_ASSET_UNRESOLVED`, and the row's message names the import: declare
|
|
115
|
+
the element that shows it by hand (`data-bcms-field` + `data-bcms-kind="image"`).
|
|
116
|
+
|
|
117
|
+
Images need no code at all on a text-matched site. Every release derives an image field for each
|
|
118
|
+
picture a page shows: an `<img>` (its `src`, or where that is a placeholder a lazy loader's
|
|
119
|
+
`data-src`, then the largest `srcset` candidate), a whole `<picture>` as ONE field, a
|
|
120
|
+
`<video poster>`, an inline or STYLESHEET `background-image` (Tailwind `bg-[url(...)]` included)
|
|
121
|
+
and an inline `<svg>`. What it declines is listed with a reason in the binding report's
|
|
122
|
+
`imageGaps`, and everything else it declines in `elementGaps`
|
|
123
|
+
([media-and-brand.md](media-and-brand.md) Where images hide).
|
|
124
|
+
|
|
125
|
+
Publishing an image keeps the page responsive. A library image re-points every `srcset`
|
|
126
|
+
candidate and each `<source>` at the new picture at that candidate's own width and format, and
|
|
127
|
+
leaves `sizes`, `width`, `loading`, classes, ids and `data-*` as written; never hand-edit those
|
|
128
|
+
after a swap. A stylesheet background gets one inline `background-image` over the rule, removed
|
|
129
|
+
again when the value returns to the sheet's own picture.
|
|
130
|
+
|
|
131
|
+
To keep an image editable in code you write yourself, render it with `BcmsImage` and its `path`
|
|
132
|
+
(Astro: `@bettercms-ai/astro/components/BcmsImage.astro`; Next: `BcmsImage` from
|
|
133
|
+
`@bettercms-ai/next`): one `<img>` that declares the field, reads the value's alt, crop and focal
|
|
134
|
+
point, and offers a width ladder when given `sizes`.
|
|
135
|
+
|
|
95
136
|
## 3. Addresses
|
|
96
137
|
|
|
97
138
|
The grammar is identical in `@bettercms-ai/next` and `@bettercms-ai/astro`; emit it with the SDK
|
|
@@ -103,7 +144,9 @@ helpers rather than by hand.
|
|
|
103
144
|
this list. An `href`, an `src` and a `poster` are kind `url` (`cta.href|url|href`, not
|
|
104
145
|
`|text|`); an `alt` is `text`. The same address on two elements is edited as two copies of one
|
|
105
146
|
value (numbered `data-node-index`).
|
|
106
|
-
- `<BcmsField path kind as>`
|
|
147
|
+
- `<BcmsField path kind as>` binds hand-rendered entry values. Around ONE element it adds no
|
|
148
|
+
wrapper: the attributes go on that element. Around text or several elements it renders a
|
|
149
|
+
`display:contents` span, and `as` names a real wrapper. `path` is dotted keys with any
|
|
107
150
|
number of `[n]` indices: `title`, `hero.title`, `columns[0].links[2].href`.
|
|
108
151
|
- Rich text is addressed per paragraph: `<base>#<_key>`. No write tool takes an address:
|
|
109
152
|
`update_content_entry` replaces the whole field (with `merge: true` it keeps the other
|
|
@@ -46,7 +46,10 @@ Never let a component contain itself, directly or through a child.
|
|
|
46
46
|
once the field is an image). A nav, footer or
|
|
47
47
|
modal group is `NOT_A_SECTION` with `detail: LAYOUT_CHROME`: chrome is the Layout on every
|
|
48
48
|
site, never a page band. Author the real sections among those with `create_component` and
|
|
49
|
-
`set_page_content` instead.
|
|
49
|
+
`set_page_content` instead. The other codes need no decision: `ALREADY_COMPONENTIZED` (a
|
|
50
|
+
placement exists), `EMPTY_GROUP` (no field holds a value), `NESTED_REPEATER` (a repeater three
|
|
51
|
+
deep; two levels become a nested `table` sub-field) and `PAGE_NOT_EMPTY` (the page holds
|
|
52
|
+
blocks this lane does not own, and it will not overwrite them).
|
|
50
53
|
2. Show the user the sections, how many components it creates and which pages it rewrites.
|
|
51
54
|
Confirm before writing. A group the user says is not a section: `componentize_sections
|
|
52
55
|
{ digest, sections: [groupKey], pageIds, decline: true }` (`decline: false` undoes it). The
|
|
@@ -56,9 +59,11 @@ Never let a component contain itself, directly or through a child.
|
|
|
56
59
|
again and confirm again. Then run it for real.
|
|
57
60
|
4. What it writes: each component as a DRAFT, and each page's draft blocks as an ordered list of
|
|
58
61
|
instances. By default each instance carries `props.bind` and the words stay in the page's
|
|
59
|
-
field group, so bindings and the coverage meter do not move. `copy: "instance"`
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
field group, so bindings and the coverage meter do not move. `copy: "instance"` is the
|
|
63
|
+
page-builder model: each placement takes its group's CURRENT draft values into its own
|
|
64
|
+
`props.overrides` (a repeater becomes one `table` prop holding every row), records the page
|
|
65
|
+
path each came from in `props.source`, and drops `props.bind`. The old fields are kept and
|
|
66
|
+
marked `origin: "componentized"`; nothing is deleted. Running it twice is safe: an existing placement comes back as
|
|
62
67
|
`ALREADY_COMPONENTIZED`.
|
|
63
68
|
5. `publish_component` for each, then publish the pages. An unpublished component renders
|
|
64
69
|
nothing on the live site.
|
|
@@ -67,7 +72,8 @@ Never let a component contain itself, directly or through a child.
|
|
|
67
72
|
commit to). In a checkout, run the codemod with `--componentize` and a `--receipt` file, commit
|
|
68
73
|
and push the files it wrote, and hand that receipt verbatim to `submit_componentize_receipt`. It records
|
|
69
74
|
each extracted section as the source of the component with the same slug, so no
|
|
70
|
-
`set_component_source` per component is needed. Deploy.
|
|
75
|
+
`set_component_source` per component is needed. Deploy. A page whose bands could not be
|
|
76
|
+
lifted is wrapped where it stands instead (§5).
|
|
71
77
|
7. Output validation, from step 3 of §6.
|
|
72
78
|
|
|
73
79
|
## 3. Names and groups
|
|
@@ -75,7 +81,10 @@ Never let a component contain itself, directly or through a child.
|
|
|
75
81
|
Name a family by what it is (`Hero`, `CTA`, `Testimonials`) and each variant by how it looks
|
|
76
82
|
(`Hero / Split`, `CTA / Centered`). Never put a page name in a reusable definition; which pages
|
|
77
83
|
use it is a usage question (§8). The componentize lane files a section under its page's slug
|
|
78
|
-
(`home-hero`) but keeps the name the section's own (`Hero`).
|
|
84
|
+
(`home-hero`) but keeps the name the section's own (`Hero`). Its components go into the Component
|
|
85
|
+
Group you pass as `group` (a folder of the dashboard's Components tab), else into one named for
|
|
86
|
+
the section family: nav, header, footer or menu into Layout, an unnamed section into Sections,
|
|
87
|
+
any other into the family's own name (Hero, FAQ). Keep related fields together in the
|
|
79
88
|
order an editor reads them:
|
|
80
89
|
|
|
81
90
|
```text
|
|
@@ -111,3 +120,53 @@ and after publish:
|
|
|
111
120
|
- one chrome edit reaches every route.
|
|
112
121
|
|
|
113
122
|
Then `get_binding_report` and `get_site_composition` (§9).
|
|
123
|
+
|
|
124
|
+
## 5. The page wrap (`--sections`): sections that stay in the site's code
|
|
125
|
+
|
|
126
|
+
Lifting a band into a component (§2) needs an inline run and a plan. A page built from imported
|
|
127
|
+
section components with props has neither, and it does not need them: `--sections` wraps the
|
|
128
|
+
page's top-level sections in `<BcmsSections>`, one named slot each, so the site's own build draws
|
|
129
|
+
them in the CMS page's order, with each block's spacing and style, and draws a section added in
|
|
130
|
+
the editor between them. It changes no section's markup, props, classes or styles.
|
|
131
|
+
|
|
132
|
+
`bettercms init` runs it before the agent starts and commits it locally
|
|
133
|
+
(`chore(bettercms): render sections from BetterCMS`); the dispatch of step 6 runs it after
|
|
134
|
+
`--componentize`. Do not redo or revert that commit. Pushing it is the user's decision. To run
|
|
135
|
+
it yourself, in a clean checkout:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
npx @bettercms-ai/convert --sections --root . --dry-run # the plan, per page
|
|
139
|
+
npx @bettercms-ai/convert --sections --root .
|
|
140
|
+
npx @bettercms-ai/convert --verify-styles --root . # must report no style edits
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A page it leaves alone keeps rendering as before, and is listed with one of these:
|
|
144
|
+
|
|
145
|
+
| Reason | The page | What you do |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `SECTION_RUN_NOT_FOUND` | Returns markup from several places, or has loose text between sections | Give it one root, wrap loose text in an element, run again; or write the wrap by hand |
|
|
148
|
+
| `SECTION_RUN_NOT_CONTIGUOUS` | A child with `slot="…"`, a `<script>` or a `<style>` sits between two sections | Move it above or below the sections, run again |
|
|
149
|
+
| `SECTIONS_IN_LAYOUT` | Gives its layout no children: the layout draws the sections | Move the sections into the page, or wrap them by hand in the layout for that one page |
|
|
150
|
+
| `SECTION_RUN_DYNAMIC` | A `[slug]` route no collection page has for its item template (a parameter above the last segment, or no collection at that path) | Nothing: its sections stay in code. A collection's own `[slug]` file is wrapped once, for the collection page, and every entry draws that list |
|
|
151
|
+
| `CLIENT_COMPONENT` | A `"use client"` page that exports more than the page | Move those exports to a server file and run it again. Any other client page is wrapped: it moves into `page.client.tsx` and its page file becomes the server parent that reads the order |
|
|
152
|
+
| `PAGE_PROPS_IN_USE` | Pages Router page on `getInitialProps`, a data function it only re-exports, or a rest parameter | Add `bcmsSections` to its props by hand ([next.md](next.md)) |
|
|
153
|
+
| `PAGE_UNKNOWN` | No CMS page for the route | Create and publish the page, run again |
|
|
154
|
+
| `DIALECT_UNSUPPORTED` | Markdown, MDX or HTML | Nothing: the body is one `document` field |
|
|
155
|
+
| `NAME_TAKEN` | Already uses the name `BcmsSections` for something else | Rename that, run again |
|
|
156
|
+
| `SDK_REQUIRED` | The repo's SDK predates the component | Add `--add-sdk`, install, run again |
|
|
157
|
+
|
|
158
|
+
The by-hand markup is in [astro.md](astro.md) and [next.md](next.md), and it follows the rules of
|
|
159
|
+
[animation.md](animation.md) §10: slot tags around sections you do not edit, no new element. On
|
|
160
|
+
a wrapped page two notes ask for a hand edit. `ROOT_IS_COMPONENT` (Next): the section moves and
|
|
161
|
+
takes its spacing on a box around it, which is one block box once styled; write its child as a
|
|
162
|
+
function and spread the root props on the component's root element to style the root itself.
|
|
163
|
+
`EXPRESSION`: a `.map()` or a conditional is ONE section; to move its rows separately, give each
|
|
164
|
+
row its own slot.
|
|
165
|
+
|
|
166
|
+
Slots are written `code:<bind>` unless the page already places that bind; a `code:` slot is
|
|
167
|
+
never removed by the list. After the page's placements exist, run `--sections` again (or
|
|
168
|
+
`bettercms init`): it writes those slots bare, and from then on removing the block removes the
|
|
169
|
+
section. Any other framework gets `FRAMEWORK_UNSUPPORTED` and no write.
|
|
170
|
+
|
|
171
|
+
Check: after the build, the route's row in `get_binding_report` reads `stamped` equal to `placed`
|
|
172
|
+
and `ordered: true`.
|