@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
|
@@ -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`
|
|
@@ -188,16 +192,24 @@ component.
|
|
|
188
192
|
|
|
189
193
|
### When the site's own code renders the sections
|
|
190
194
|
|
|
191
|
-
A code-built site ships only what its templates draw, whatever the page's `blockJson` says.
|
|
195
|
+
A code-built site ships only what its templates draw, whatever the page's `blockJson` says. The
|
|
196
|
+
setup gate judges it per route (`sections` and `style`, [finish-site.md](finish-site.md)), and
|
|
197
|
+
adding or spacing one section is the recipe in [publishing.md](publishing.md).
|
|
192
198
|
|
|
193
199
|
- **Render the page's block list, never hard-coded bands.** A section added or moved in the
|
|
194
200
|
editor reaches the live site only when the template loops over the page's blocks in order and
|
|
195
|
-
each section root carries its block id as `data-bcms-block`. The codemod
|
|
196
|
-
|
|
201
|
+
each section root carries its block id as `data-bcms-block`. The codemod writes both: `--sections`
|
|
202
|
+
wraps a page as it is (below), and `--componentize` lifts its bands into components and
|
|
203
|
+
generates `src/lib/bcms-sections.ts` for the loop. Until one ships, the
|
|
197
204
|
editor still adds, moves, swaps and removes sections (saved, and drawn on the canvas at once),
|
|
198
205
|
tells the author that publishing leaves the live order to the site's code, and its "Let BetterCMS
|
|
199
206
|
control section order" button commits that same codemod to the connected repository. `submit_conversion_receipt` keeps
|
|
200
207
|
`complete: false` with one `SECTIONS_UNSTAMPED` row per route.
|
|
208
|
+
- **A page of imported section components is wrapped, not lifted.** `<BcmsSections>` takes each
|
|
209
|
+
top-level section as a named slot and draws the block list in order, stamping each section's
|
|
210
|
+
own root; a section added in the editor renders between them. `bettercms init` writes and
|
|
211
|
+
commits the wrap; what it refused, and the markup to write by hand, is in
|
|
212
|
+
[componentize.md](componentize.md) §5.
|
|
201
213
|
- **The order is measured, not assumed.** A release reads the stamped section roots in document order:
|
|
202
214
|
a build that draws them in the CMS order (`ordered: true` on the binding report's structure row)
|
|
203
215
|
owns nothing, whatever sections it has no code for; one that draws them in its own order
|
|
@@ -214,8 +226,35 @@ A code-built site ships only what its templates draw, whatever the page's `block
|
|
|
214
226
|
the repo root (`--dry-run` first); give each root it reports as skipped (`OWN_STYLE`, `SPREAD`,
|
|
215
227
|
`BLOCK_ID_ONLY`, …) its block style by hand, confirm with `--verify-styles`, push and release.
|
|
216
228
|
- **A `button` field renders with `BcmsButton`** (`path`, `value`): one `<a data-bcms-button>`
|
|
217
|
-
carrying `data-variant` and `data-size`, so
|
|
218
|
-
|
|
229
|
+
carrying `data-variant` and `data-size`, so every look control (style, size, arrow, icon, new
|
|
230
|
+
tab) applies on the canvas and on publish.
|
|
231
|
+
- **A button's looks are the ones the site already has.** Each release measures the built pages
|
|
232
|
+
and keeps the project's Button family equal to the looks found there. Style offers those looks
|
|
233
|
+
and no others, and a publish paints a pick with the site's own classes. Never write CSS for a
|
|
234
|
+
look so that a control "works": there is no fixed list of variants to implement. A `button`
|
|
235
|
+
value's `variant` is one of those looks; a refused value comes back with the looks the project
|
|
236
|
+
has. A look the site drops is flagged, never deleted. A new look is the owner's design
|
|
237
|
+
decision: add its class to the site, then Rebuild.
|
|
238
|
+
- **Each look is named for the role it plays on that site, and a site has only the roles it
|
|
239
|
+
uses.** One site's family is Primary, Secondary, Tertiary; another's is Primary, Secondary,
|
|
240
|
+
Ghost, Outline, Text Link. Never create, rename toward, or ask for a role the site does not
|
|
241
|
+
use, and never number one ("Outline 2"): the release gives the names, in this order.
|
|
242
|
+
|
|
243
|
+
| Role | The look that gets it |
|
|
244
|
+
|---|---|
|
|
245
|
+
| The site's own name | A look the site names keeps that name: `btn-primary` is Primary, `btn-ghost` is Ghost, `data-variant="link"` is Text Link. Its stored `variant` value stays the site's own. |
|
|
246
|
+
| Primary | Exactly one: the filled look used most (placements, then pages, then hero placements). On a site that fills nothing, its most used outline or ghost. |
|
|
247
|
+
| Brand | The most used look filled with a colour, when Primary is a neutral dark or light fill, or when the site names a less used look Primary. |
|
|
248
|
+
| Secondary | The look most often beside Primary in a button group; else the next most used look that is not a text link. |
|
|
249
|
+
| Tertiary | A third filled look. Further fills are Dark, Light, or the colour's name. |
|
|
250
|
+
| Outline | Bordered and not filled. |
|
|
251
|
+
| Ghost | No fill and no border, padded like a button or filled on hover. |
|
|
252
|
+
| Text Link | A plain link the site styles as a call to action. Menu and footer links and links in copy are not looks. |
|
|
253
|
+
|
|
254
|
+
A light and a dark rendition, a hover or disabled state, a size, and one paint spelled in two
|
|
255
|
+
sets of classes are all ONE look. Two looks left with one role are told apart in words from
|
|
256
|
+
their paint ("Outline on dark", "Pill Outline"). A role once given stays: a look added later
|
|
257
|
+
takes a role that is free and renames nothing, and a name a person gave is never changed back.
|
|
219
258
|
Without the SDK, write that same anchor: `data-bcms-button="<path>"`, the label in its own
|
|
220
259
|
`<span data-bcms-field="<path>.label">`, the href through `data-bcms-props`. The arrow is the
|
|
221
260
|
`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
|
|
@@ -175,7 +179,12 @@ fail, because the next redesign makes them lie.
|
|
|
175
179
|
|
|
176
180
|
Short labels named for what the editor sees: Heading, Body, Primary button, Label, Link, Image,
|
|
177
181
|
Alt text — or the name the site's code already uses (`cents` → Cents), never the copy itself
|
|
178
|
-
(`.00`).
|
|
182
|
+
(`.00`). An import names every field it derives the same way, by its section and its role
|
|
183
|
+
there: "Hero headline", "Hero intro", "Hero primary button", "Pricing plans", "Footer link 2
|
|
184
|
+
URL", "Header logo"; a list's columns by their role in the row ("Title", "Description", "Link
|
|
185
|
+
URL"). A role is numbered only where it repeats in one section, and labels are unique on a page.
|
|
186
|
+
Keys never change. Rename a label where the site's code has a better word: a name you give is
|
|
187
|
+
kept on every later release, and one the import gave follows the import. Most fields need no help line from you (below). Group by what renders
|
|
179
188
|
together, not by the DOM or the source file: core content first, optional appearance second, technical fields hidden. Repeatable items need a meaningful preview label (the FAQ question, the
|
|
180
189
|
button label), reorder, and stable item ids. An empty optional field removes its element without
|
|
181
190
|
leaving a gap; a long value wraps, and clamps only where the design intends it.
|
|
@@ -310,7 +319,8 @@ production cannot store it. Translate before the first write.
|
|
|
310
319
|
| `date`, `datetime` | `date`, `datetime` | `date` (`YYYY-MM-DD`); a datetime stays `text` |
|
|
311
320
|
| `color` | `color` | `color` |
|
|
312
321
|
| `select` | `select` | `select` with `config.options` |
|
|
313
|
-
| `multiselect
|
|
322
|
+
| `multiselect` | `multiselect` with `config.options` (a closed `string[]` choice list; `min`/`max` count the choices) | `multiselect` with `config.options` |
|
|
323
|
+
| `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
324
|
| `image` | `image` | `image` |
|
|
315
325
|
| `file`, `video` | `file`; a hosted video URL is a `link` | `file`; `url` for a hosted video |
|
|
316
326
|
| `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.
|
|
@@ -9,6 +9,13 @@ description: "Take an imported or converted site to fully editable: the gap ledg
|
|
|
9
9
|
verified with evidence. "Fully editable" is a measured result, never an inference from imported
|
|
10
10
|
schemas. New to the project, or no repo on disk yet: [getting-started.md](getting-started.md).
|
|
11
11
|
|
|
12
|
+
## Contents
|
|
13
|
+
|
|
14
|
+
- [Inputs and baseline](#inputs-and-baseline) · [The finish line for a convert or build job](#the-finish-line-for-a-convert-or-build-job)
|
|
15
|
+
- [The setup gate: what "finished" is measured by](#the-setup-gate-what-finished-is-measured-by)
|
|
16
|
+
- [The gap ledger](#the-gap-ledger-bettercmsledgerjson) · [Repair order](#repair-order-and-the-reference-for-each-step)
|
|
17
|
+
- [Proof loop](#proof-loop) · [Launch decision](#launch-decision)
|
|
18
|
+
|
|
12
19
|
## Inputs and baseline
|
|
13
20
|
|
|
14
21
|
Require the project, source repository or import snapshot, site URL, import manifest, hosting
|
|
@@ -31,8 +38,186 @@ answers `complete: true` (no path left in `paths.pending`), and after the deploy
|
|
|
31
38
|
`declared`. A build job works from `bettercms://playbook/schema` (the `build_site` prompt carries
|
|
32
39
|
the same flow on a local MCP) and is done when the site is deployed, `get_component_readiness`
|
|
33
40
|
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
|
-
|
|
41
|
+
Both end in the verify loop of SKILL.md, and an imported site ends only on the setup gate below.
|
|
42
|
+
One converted page, or a receipt with paths still pending, is not a finished site.
|
|
43
|
+
|
|
44
|
+
## The setup gate: what "finished" is measured by
|
|
45
|
+
|
|
46
|
+
`get_next_steps` returns `receipt.complete` and `receipt.gate`. The platform computes the gate
|
|
47
|
+
from the last release, one verdict per route of the live build. The CLI that launched you reads
|
|
48
|
+
it again when you exit and prints it to the user, so the gate is the report, not your summary.
|
|
49
|
+
While the gate is incomplete that command exits 3 (0 once it is complete; 2 is a wrong command
|
|
50
|
+
line), which is what a script waiting on the setup reads.
|
|
51
|
+
|
|
52
|
+
| Check | Passes when | Where it shows |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `page` | a CMS page, or a collection entry, serves the route | `routes.rows[].page` |
|
|
55
|
+
| `published` | that page, and its entry, is published | `routes.rows[].page` |
|
|
56
|
+
| `twin` | no second page claims the same route | `blocking.rows` |
|
|
57
|
+
| `text` | 2% or less of the visible characters sit outside a field | `routes.rows[].text` |
|
|
58
|
+
| `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` |
|
|
59
|
+
| `link` | 95% of the links have a field for the label or the destination | `routes.rows[].link` |
|
|
60
|
+
| `sections` | every section the page places is a stamped root on the built page, none twice, none published after the build, and a section added in BetterCMS has somewhere to show (`accepts` `list` or `in-place`) | `routes.rows[].sections`: `accepts`, `renderer` (`sdk`, `registry`, `annotator`), and when it fails `refusal` (why the release cannot write the page) and `wrap` (`wrapped`, or `refused:<REASON>` from the sections conversion) |
|
|
61
|
+
| `style` | every stamped section root carries the style marker (`data-bcms-style`, or `data-bcms-block-style` on a root that is a field's element) or is drawn by the route's block renderer, counted by setup on the served page, so a block's spacing, padding and background paint | `routes.rows[].style` |
|
|
62
|
+
| `svg` | every inline SVG a visitor sees (a logo, an icon, an illustration) belongs to a field; a hidden symbol sheet is not counted | `routes.rows[].svg` |
|
|
63
|
+
| `number` | every element whose whole text is a number (a stat, a price, a rating) belongs to a field | `routes.rows[].number` |
|
|
64
|
+
| `attribute` | every `alt`, `aria-label`, `title` and `placeholder` with words in it belongs to a field | `routes.rows[].attribute` |
|
|
65
|
+
| `names` | no field of the page, and no component, has a name a person cannot read: a slug, a hash, a structural word (`Section 2`, `wrap`), or a number beside the same name (`Icon 2`) | `blocking.rows` |
|
|
66
|
+
| `seo` | the page has a title of at most 70 characters and a description of at most 200, and a canonical where the build ships one for this site's origin; site-wide, every page has a share image, its own or the site's default | `blocking.rows` |
|
|
67
|
+
| `structuredData` | JSON-LD the build ships on the route is on the page or the entry | `blocking.rows` |
|
|
68
|
+
| `collections` | sibling routes that are one family (three under one parent, or two under `tags`, `category`, `author`) are a collection; reported on the family's listing route, built or not | `blocking.rows` |
|
|
69
|
+
| `buttons` | every button field is one of the site's live Button looks, the Button family is the looks setup measured on this build (the same looks, not the same count), and no look is named by a number or a build's class | `blocking.rows`, `route: null` |
|
|
70
|
+
| `components` | the authoring question is answered, and no section is left that a componentize run would place | `blocking.rows`, `route: null` |
|
|
71
|
+
| `forms` | every form of the customer's has an address to notify; a form setup ignored (sign-in, add to basket) is not counted | `blocking.rows`, `route: null` |
|
|
72
|
+
|
|
73
|
+
`pages` counts routes built, created and published. `coverage` sums `editable` of `total` across
|
|
74
|
+
the site, `svg`, `number` and `attribute` included, and `open` counts the rows still open per
|
|
75
|
+
check. `routes.rows` is ordered worst first, with `failing` per route and the reasons the
|
|
76
|
+
deriver `skipped` copy there. `blocking.rows` is the work list: each row carries a `detail` with
|
|
77
|
+
its number, a `fixCode` and a `fix`, the call that closes it.
|
|
78
|
+
|
|
79
|
+
`nextAction` is the one thing to do now: the fix a tool can make that closes the most rows
|
|
80
|
+
(`fixCode`, `count`, the `fix` of its first row, the first `routes`), two with the same count in a
|
|
81
|
+
fixed order. Run it on every row that carries that `fixCode`, call `get_next_steps`, and repeat
|
|
82
|
+
until `complete`: following it reaches `complete` with no step that changes nothing. A fix no
|
|
83
|
+
tool can make is named only when nothing else is left. A `fix` of `null` means no tool closes that row (a collection's listing
|
|
84
|
+
route, a URL that cannot be placed, two pages nobody but the owner can choose between): put it to
|
|
85
|
+
the user and stop retrying. Treat a row that its fix did not move the same way.
|
|
86
|
+
|
|
87
|
+
A `sections` or `style` row is a page nobody can add a section to, reorder or space, however
|
|
88
|
+
complete its copy is. Its `fixCode` names the one fix, and `fix` the call when a tool can make it:
|
|
89
|
+
|
|
90
|
+
| `fixCode` | The fix |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `run-sections-conversion` | Only the site's code can draw this page's sections: its app renders the route, a framework hydrates the page whole (`PAGE_HYDRATED`), or the release could not map its bands. In a checkout run `npx @bettercms-ai/convert@latest --sections --root .` ([componentize.md](componentize.md) §5), or call `componentize_sections` with `codemod: true` and the plan's `digest`; `--upgrade-sections` for a `style` row. Then push and release |
|
|
93
|
+
| `rebuild` | The build that serves is older than what makes the row pass: a published section, the wrap's commit (`wrap: "wrapped"` on a row that still fails), or the in-place switch. No tool starts one: ask the user to push and Rebuild |
|
|
94
|
+
| `enable-in-place-structure` | A static page the release can write by itself, and the project's in-place structure switch is off. Setup switches it on for a project it creates; on an existing one it is the operator's call. No repository change |
|
|
95
|
+
| `run-setup` | Nothing measured what the row reads, or a page is still to register: `editor_setup { force: true }`, then read again |
|
|
96
|
+
|
|
97
|
+
The other rows name their fix the same way:
|
|
98
|
+
|
|
99
|
+
| `fixCode` | The fix |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `bind-elements` | `get_conversion_brief { complete: true }` lists what no field owns on the route; bind each (an SVG as an image or icon field, a number as its own field, attribute copy with `data-bcms-props`) |
|
|
102
|
+
| `rename-fields` | `get_page`, then `update_field` with a name a person would say; `update_component` for a component |
|
|
103
|
+
| `fill-seo` | `update_page` with `metaTitle` and `metaDescription` (`update_content_entry` with `meta` for an entry; `generate_seo_meta` drafts one from text you give it); a missing canonical is setup's to copy from the built head, so that row carries `editor_setup { force: true }`; the site-wide row wants the site's default share image, `update_seo` with `seoDefaults.ogImage` |
|
|
104
|
+
| `import-structured-data` | `editor_setup { force: true }` imports the build's JSON-LD; `update_seo` when it has to be written by hand |
|
|
105
|
+
| `model-collection` | `create_content_model` with the family's `urlPattern` (`/tags/:slug`), then its entries: one item template for the family ([content-modeling.md](content-modeling.md)) |
|
|
106
|
+
| `derive-buttons` | `editor_setup { force: true }` mints a Button for each look the build draws, retires one it no longer draws and maps each button to its look; rename a look with `update_component` ([components.md](components.md)) |
|
|
107
|
+
| `componentize` | `set_authoring_preference` when nobody answered, then `get_componentize_plan` and `componentize_sections` ([componentize.md](componentize.md)) |
|
|
108
|
+
| `set-form-recipient` | ask the user where the form's submissions go, then `update_form` with `notifyEmails`; `update_form { ignored: true }` when it is not one of their forms |
|
|
109
|
+
|
|
110
|
+
A row nothing measured reads `unmeasured: true` with `run-setup`, and `gate.unmeasured` lists the
|
|
111
|
+
checks that have one: a release older than a count, or a project set up before setup recorded
|
|
112
|
+
what it found on the build (the style marker of each section root, the looks it measured, the
|
|
113
|
+
canonicals the build ships). It is not a failure of the site and not a pass either: run
|
|
114
|
+
`editor_setup { force: true }` once and read again.
|
|
115
|
+
|
|
116
|
+
Setup closes what is deterministic by itself, on every run, so your first receipt is already past
|
|
117
|
+
it and an open row of these means run setup again: `run-setup`, `derive-buttons` (the family and
|
|
118
|
+
the map), `import-structured-data`, `fill-seo` from the built head and the page's own copy,
|
|
119
|
+
`rename-fields` for a field still carrying a derived name, `componentize` once the authoring
|
|
120
|
+
answer is `components`, and `enable-in-place-structure` on a project it creates. What is left
|
|
121
|
+
takes judgement and is yours: `bind-elements`, `model-collection`, `set-form-recipient` (ask the
|
|
122
|
+
user), `run-sections-conversion`, a name a person gave, a title over its limit, a look named by a
|
|
123
|
+
number, and the authoring answer itself (the user's). `rebuild` is the user's.
|
|
124
|
+
|
|
125
|
+
Beside the gate, `data` lists what else is unfinished, each item by its `ruleId`: content
|
|
126
|
+
(`mcp-pages-draft`, `mcp-pages-missing-meta`, `mcp-model-empty`, `mcp-form-no-recipient`,
|
|
127
|
+
`mcp-pending-review`), bindings and structure (`conversion-pending`, `bindings-not-declared`,
|
|
128
|
+
`unaddressable-copy`, `upgrade-section-roots`, `upgrade-element-styles`, `componentize-pages`,
|
|
129
|
+
`components-unpublished`), the authoring answer (`authoring-undecided`), the Layout lane
|
|
130
|
+
(`layout-write-blocked`, `layout-field-unread`, `layout-locator-stale`, `layout-rebuild-required`,
|
|
131
|
+
`layout-unpublished`, `chrome-duplicated`), publishes (`publish-held`) and the Content sidebar
|
|
132
|
+
(`organise-content`). `setup-gate` is first while the gate is open and `publish-held` next.
|
|
133
|
+
|
|
134
|
+
Stamps are not enough to pass either check: the lane that serves a route must have PROVED it.
|
|
135
|
+
Each release adds a probe section to the middle of one built page of each kind (a page, a
|
|
136
|
+
listing, an item template) and a probe padding to one of its sections, in memory, finds both in
|
|
137
|
+
the result and takes them out again; the sections conversion proves each page it wraps the same
|
|
138
|
+
way. `routes.rows[].proof` is that record (`lane`, `route`, `insert`, `style`: `ok` or the
|
|
139
|
+
reason). A row whose proof was never run asks for it where it runs: `editor_setup { force: true }`
|
|
140
|
+
for the release, the conversion again for a wrapped page. `SLOTS_NOT_PLACED` on a wrapped page
|
|
141
|
+
means every slot is still `code:`: place the page's sections, then run the conversion again.
|
|
142
|
+
|
|
143
|
+
The gate picks the fix per route, in that order of evidence: a wrapped page waits for its build; a
|
|
144
|
+
page a script draws needs the conversion; a static page needs only the switch. Run the fix the
|
|
145
|
+
row names and no other: the conversion does nothing for a row that says `rebuild`.
|
|
146
|
+
|
|
147
|
+
Two rows have no `fixCode`. A page that places a section twice: remove the second placement with
|
|
148
|
+
`update_page`. A route neither lane serves: the release cannot write it (`refusal`) and the
|
|
149
|
+
conversion refused it (`wrap`); its `detail` names both. Put it to the user: write the wrap by
|
|
150
|
+
hand ([astro.md](astro.md), [next.md](next.md)) or change the page. No exception covers it
|
|
151
|
+
(`document-body` excuses only a route that places no section). Check a route's row again after
|
|
152
|
+
each release; never mark one done from the codemod's own output.
|
|
153
|
+
|
|
154
|
+
A `page` row on a route setup left without a page carries the pages lane's reason in its
|
|
155
|
+
`detail` (`invalid-path`, `html-too-large`, `page-deleted`, `slug-taken`, `folder-conflict`,
|
|
156
|
+
`entry-not-registered`, `authored-content` and the rest), with what closes it: a change in the
|
|
157
|
+
site's code, another page to move, or the user's decision. Only `unreadable`, `write-failed`
|
|
158
|
+
and `not-registered` carry a `fix`. An `image` row with a `null` fix names a picture by its code
|
|
159
|
+
([media-and-brand.md](media-and-brand.md) Where images hide): the change is in the site's code.
|
|
160
|
+
`imageGaps` also lists the decoration the import declined, which blocks nothing. On a
|
|
161
|
+
collection's listing route the rows that link to its entries are counted as the collection's,
|
|
162
|
+
so the listing page is judged on its own heading, intro and calls to action, and on the words
|
|
163
|
+
of a row the release bound to its entry (a field's there too). The release binds
|
|
164
|
+
each such row to its entry: the row carries `data-bcms-entry`, a part showing a field as stored
|
|
165
|
+
carries `data-bcms-entry-field` (`entry:<id>:<path>`, written when that entry publishes), and a
|
|
166
|
+
part the site's code made from a field (a cut excerpt, a reformatted date or price, a thumbnail,
|
|
167
|
+
the item link) carries `data-bcms-entry-from` with the transform, read-only: edit the entry.
|
|
168
|
+
Never add a field for a row to the listing page. A taxonomy's term routes (`/tags/<term>`, each
|
|
169
|
+
listing another collection's entries) are a small collection of terms, and their rows are bound
|
|
170
|
+
the same way. A term has a `body` only where its template has an article. Chrome the Layout
|
|
171
|
+
could not take (an icon with no Media asset) is template copy on item routes: one shared field
|
|
172
|
+
of the collection, as it is a page's own field on a page.
|
|
173
|
+
|
|
174
|
+
### Exceptions go on the record
|
|
175
|
+
|
|
176
|
+
A check you are leaving on purpose is recorded, one row per route and check, with
|
|
177
|
+
`editor_setup` and `exempt: [{ route, check, reason, note }]`. `note` states what is being left
|
|
178
|
+
in a sentence a person can verify. `reason: null` withdraws one.
|
|
179
|
+
|
|
180
|
+
| `reason` | May excuse |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `form-validation-copy` | `text` |
|
|
183
|
+
| `legal-boilerplate` | `text` |
|
|
184
|
+
| `third-party-widget` | `text`, `image`, `link`, `svg`, `number`, `attribute` |
|
|
185
|
+
| `decorative` | `text`, `image`, `svg` |
|
|
186
|
+
| `duplicate-track` | `text`, `image`, `link`, `svg`, `number`, `attribute` (the cloned track of a marquee or slider) |
|
|
187
|
+
| `client-only-island` | nothing: it lists an island the build cannot show, and the route still meets the bar outside it |
|
|
188
|
+
| `user-declined` | `page`, `published`, `text`, `image`, `link`, `svg`, `number`, `attribute`, `seo`, `structuredData`, `collections`, and only with the user's own words quoted in `note` (`User: "leave pricing in code"`) |
|
|
189
|
+
| `document-body` | `sections`, and only on a route that places no section at all: a page that is one document (a markdown post, a legal page) |
|
|
190
|
+
|
|
191
|
+
Nothing else excuses `sections`, and nothing excuses `style`, `twin`, `names`, `buttons`,
|
|
192
|
+
`components` or `forms`: not the user's decision either. A name is fixed by renaming, and a form
|
|
193
|
+
that is not the customer's is marked ignored.
|
|
194
|
+
|
|
195
|
+
A row excuses what its check read when it was recorded. When the route reads differently
|
|
196
|
+
later (more copy nobody owns, another picture), the row stops counting and the check blocks
|
|
197
|
+
again: record it again if it still stands. The CLI prints every `user-declined` row marked for
|
|
198
|
+
the user to confirm.
|
|
199
|
+
|
|
200
|
+
Refused whatever the note says: `out-of-time`, `out-of-context`, `too-many-routes`,
|
|
201
|
+
`tool-failed`, `not-asked`, `good-enough`. A sentence to the user about a gap is not an
|
|
202
|
+
exception either: the gate stays incomplete, and the CLI exits non-zero with that gap listed.
|
|
203
|
+
|
|
204
|
+
### What setup derives on a collection's item pages
|
|
205
|
+
|
|
206
|
+
Setup reads a few entry pages of each collection and makes one field of everything they render
|
|
207
|
+
at the same place: text, each image (an `<img>` with only a `srcset`, a `<picture>`, a
|
|
208
|
+
background its stylesheet paints), a call to action (one `button`, or a label and its `-href`
|
|
209
|
+
where the project derives pairs), a plain list, and a gallery or spec table as a repeater whose
|
|
210
|
+
rows each entry fills from its own page. The article's container becomes the place the entry's
|
|
211
|
+
`body` is read from and bound, so `body` holds the article alone. An element the site's code
|
|
212
|
+
already declares keeps its declaration and gets no second field. A collection from before this
|
|
213
|
+
gains the fields add-only; its `body` is bound only while no entry of it was edited. Check the
|
|
214
|
+
result per entry route in the gate, never by adding the same fields by hand.
|
|
215
|
+
|
|
216
|
+
Copy the template shows unchanged on every entry (a "Related posts" heading, an "Add to cart"
|
|
217
|
+
button) is one field marked `config.shared`, and its value lives on that field, once: no entry
|
|
218
|
+
holds it. Edit it as a field of any one entry and publish that entry: the value moves onto the
|
|
219
|
+
field and every item page is repainted. Until then it is that entry's draft, shown on no page.
|
|
220
|
+
A translated entry keeps its own words for it. Never write the same value into each entry.
|
|
36
221
|
|
|
37
222
|
Git belongs to the owner. Verification releases go out from the working tree, so the job never
|
|
38
223
|
needs a commit to prove itself. After the final report, ask whether they want the changes
|
|
@@ -71,7 +256,7 @@ and add the new result. Update the file as each row moves, so a reset loses at m
|
|
|
71
256
|
|---|---|---|
|
|
72
257
|
| 0 | Editor setup: `editor_setup { check: true }` reads `ready` for the live build (run `editor_setup` when it does not) | [getting-started.md](getting-started.md) |
|
|
73
258
|
| 1 | Routing and data shape: page / collection / entry classification and folders; keep public URLs | [content-modeling.md](content-modeling.md) |
|
|
74
|
-
| 2 | Sections and reuse: every band a section; dedupe compatible patterns into variants and overrides; nav and footer as globals (`get_componentize_plan`, `componentize_sections` on a components project) | [components.md](components.md), [componentize.md](componentize.md) |
|
|
259
|
+
| 2 | Sections and reuse: every band a section; dedupe compatible patterns into variants and overrides; nav and footer as globals (`get_componentize_plan`, `componentize_sections` on a components project). `bettercms init` has already wrapped each page's sections in `<BcmsSections>` and committed it: fix only the pages it refused ([componentize.md](componentize.md) §5) | [components.md](components.md), [componentize.md](componentize.md) |
|
|
75
260
|
| 3 | Editor contract: field types, labels, help text on every element (`get_next_steps` shows no `help-text-missing`), validation, Portable Text accept lists, nested groups | [portable-text.md](portable-text.md) |
|
|
76
261
|
| 4 | Design: media, logo roles, fonts and brand tokens, without changing visual intent | [media-and-brand.md](media-and-brand.md) |
|
|
77
262
|
| 5 | Bindings: the site reads BetterCMS data and each intended leaf or attribute is bound; build, preview and runtime agree | [bindings.md](bindings.md), [frameworks.md](frameworks.md) |
|
|
@@ -184,6 +184,13 @@ only reveals the next group, to `step` fields; keep one `required` per real ques
|
|
|
184
184
|
|
|
185
185
|
`update_form` with `fields` replaces the whole array: send every field you want to keep.
|
|
186
186
|
|
|
187
|
+
Setup imports two kinds of form already ignored, each kept as a restorable row: the site's own
|
|
188
|
+
sign-in, register and password forms, and a shop's add-to-basket, buy-now or checkout form (it
|
|
189
|
+
posts to the cart and asks only a quantity or a variant). Both stay the site's code. An imported
|
|
190
|
+
form is named for what it is for: its `aria-label`, its one legend, a heading inside it or in its
|
|
191
|
+
own band, else its purpose ("Newsletter signup", "Contact"). Rename it with `update_form` if the
|
|
192
|
+
site calls it something else.
|
|
193
|
+
|
|
187
194
|
### 4. Wiring a self-hosted site
|
|
188
195
|
|
|
189
196
|
- **Next**: `getForm('Name')` (or an id) reads the form from `bcms-content.json`; render
|
|
@@ -85,6 +85,8 @@ and `get_componentize_plan` proposes nothing more.
|
|
|
85
85
|
with `componentize_sections`, then the repository half (`componentize_sections { codemod: true }`
|
|
86
86
|
when there is no checkout) and `submit_componentize_receipt` ([componentize.md](componentize.md)).
|
|
87
87
|
5. Work the rest through [finish-site.md](finish-site.md) (gap ledger, repair order, proof loop).
|
|
88
|
+
The gate's `sections` and `style` rows are part of it: a setup whose pages cannot take a new
|
|
89
|
+
section, a new order or spacing is not finished.
|
|
88
90
|
|
|
89
91
|
### Phase 4 — Publish and verify
|
|
90
92
|
|
|
@@ -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,43 @@ 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>` past 64 KB | Ship it as an `.svg` file in an `<img>` |
|
|
130
|
+
| `SVG_SPRITE` | A `<use>` naming a symbol the page does not hold, or a sprite FILE the build does not ship (another host's, a relative path) | Inline the symbol in the page, serve the sprite from the site root, or ship the icon as its own `<svg>` |
|
|
131
|
+
| `IMAGE_NOT_ASSET` | A header or footer picture the Media library holds no asset for | Nothing lost: the rest of the region is the Layout's and this picture stays a field of each page. Setup tries it again on the next build |
|
|
132
|
+
| `SVG_NOT_ICON`, `SVG_DECORATIVE` | An `<svg>` holding a script, an HTML island or a remote image; an `aria-hidden` glyph in the nav or footer, or a nameless one on an older import | Decoration: leave it, or give a logo `role="img"` and an `aria-label` |
|
|
133
|
+
| `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 |
|
|
134
|
+
|
|
135
|
+
The first four rows count against the image coverage; the rest do not. A gap you cannot
|
|
136
|
+
close is reported with its code, never described as done.
|
|
137
|
+
|
|
138
|
+
An icon a sprite file draws (`<use href="/sprite.svg#shield">`, a same-site `.svg` the build
|
|
139
|
+
ships) is an image field like any inline icon. An icon a FONT draws (`<i class="ph ph-tag">`) is
|
|
140
|
+
a text field holding the class, written to the `class` attribute alone: change the class to
|
|
141
|
+
change the glyph.
|
|
142
|
+
|
|
143
|
+
`elementGaps` is the same record for everything that is not a picture (`rows` carry a `sample`
|
|
144
|
+
where an image row has a `src`). `EMBED_NOT_VIDEO` (an `<iframe>` that is no video provider's),
|
|
145
|
+
`AUDIO_SOURCE` and `ICON_FONT` (a font icon inside a paragraph that is one rich-text value) are
|
|
146
|
+
shown and editable nowhere, and count against coverage. The rest are declined on purpose and
|
|
147
|
+
never count: `SKIP_LINK`, `FRAMEWORK_UI` (a docs framework's pager, table of contents or
|
|
148
|
+
search), `FRAGMENT_LINK` (an in-page `#` destination under those rules), `ORDINAL_MARKER` (a
|
|
149
|
+
step number the template counts out, on a project imported before step numbers were fields),
|
|
150
|
+
`TEMPLATE_TEXT`, `NOSCRIPT_TEXT` and `HEAD_META_UNMAPPED` (a `<meta>` no native SEO field holds,
|
|
151
|
+
by name).
|
|
152
|
+
|
|
115
153
|
## Media host
|
|
116
154
|
|
|
117
155
|
Use `mediaBaseUrl` from `get_project`: media is served from the CDN origin or the API origin of
|
|
@@ -40,14 +40,17 @@ 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
|
+
| `BcmsSections`, `BcmsSection` (from `@bettercms-ai/next/sections`) | The page's own sections, drawn in the CMS page's order (below); a server component |
|
|
46
|
+
| `sectionsFor(slug)`, `sectionsStaticProps(slug, own?)`, `sectionsServerSideProps(slug, own)` | The block list, components and forms `BcmsSections` takes, from the snapshot |
|
|
45
47
|
| `sectionRootAttrs(block)` | Spread on a section root your own code renders: `data-bcms-block`, `data-bcms-style` and the block's style, plus `data-bcms-hide` when `style.hideOn` is set: ship `RESPONSIVE_CSS` (exported here) in your global CSS ([components.md](components.md) §5) |
|
|
46
48
|
| `bind`, `bindProps` | Binding attributes; instance-aware inside components |
|
|
47
49
|
| `BcmsForm` + `getForm(idOrName)` | Forms from the snapshot ([forms-and-interactions.md](forms-and-interactions.md)) |
|
|
48
50
|
| `buildMetadata(page, siteDefaults, location?)` | Next `Metadata` from native SEO fields |
|
|
49
51
|
| `llmsTxtRoute(config)` | An `llms.txt` route handler |
|
|
50
|
-
| `imageUrlBuilder({ baseUrl })` | Resize URLs
|
|
52
|
+
| `imageUrlBuilder({ baseUrl })` | Resize URLs; same media-host rules as Astro |
|
|
53
|
+
| `BcmsImage` | An image field as one `<img>`: `path`, `src` (the value), `sizes` or `width`; declares the field and keeps the value's alt, crop and focal point; server-safe |
|
|
51
54
|
|
|
52
55
|
Rules:
|
|
53
56
|
|
|
@@ -59,6 +62,63 @@ Rules:
|
|
|
59
62
|
- The package ships no sitemap or robots helper. Write `app/sitemap.ts` and `app/robots.ts`
|
|
60
63
|
yourself from published content, and leave out pages marked noindex.
|
|
61
64
|
|
|
65
|
+
## Sections in the CMS order
|
|
66
|
+
|
|
67
|
+
A page whose sections are written in its own file follows the CMS page's block list through
|
|
68
|
+
`<BcmsSections>`. `bettercms init` writes this wrap (`npx @bettercms-ai/convert --sections`);
|
|
69
|
+
write it by hand for a page the codemod refused ([componentize.md](componentize.md) §5).
|
|
70
|
+
|
|
71
|
+
App Router, in a server component:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { sectionsFor } from "@bettercms-ai/next";
|
|
75
|
+
import { BcmsSections, BcmsSection } from "@bettercms-ai/next/sections";
|
|
76
|
+
|
|
77
|
+
export default function Home() {
|
|
78
|
+
return (
|
|
79
|
+
<main>
|
|
80
|
+
<BcmsSections {...sectionsFor("home")}>
|
|
81
|
+
<BcmsSection bind="hero">{(root) => <Hero rootProps={root} />}</BcmsSection>
|
|
82
|
+
<BcmsSection bind="code:sec1"><LogoCloud /></BcmsSection>
|
|
83
|
+
<BcmsSection bind="features"><section className="features">…</section></BcmsSection>
|
|
84
|
+
</BcmsSections>
|
|
85
|
+
</main>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Pages Router: the page also renders in the browser, so the list is a page prop.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { sectionsStaticProps, type SectionsData } from "@bettercms-ai/next";
|
|
94
|
+
import { BcmsSections, BcmsSection } from "@bettercms-ai/next/sections";
|
|
95
|
+
|
|
96
|
+
export default function Home({ bcmsSections }: { bcmsSections: SectionsData }) {
|
|
97
|
+
return <BcmsSections {...bcmsSections}>…</BcmsSections>;
|
|
98
|
+
}
|
|
99
|
+
export const getStaticProps = sectionsStaticProps("home");
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A page with its own data function passes it in, and the list joins the props it answers:
|
|
103
|
+
`export const getStaticProps = sectionsStaticProps("home", pageStaticProps)`, or
|
|
104
|
+
`sectionsServerSideProps` for `getServerSideProps`. `--sections` writes this for you.
|
|
105
|
+
|
|
106
|
+
- A bind is the family of the section's first bound field (`features-title` → `features`): the
|
|
107
|
+
`bind` on the page's `component` block. A block bound to a section renders it at the block's
|
|
108
|
+
position; a section added in the editor (no bind) renders through `BcmsBlocks`.
|
|
109
|
+
- The stamp goes on the section's own root. One DOM element (`<section>`) gets
|
|
110
|
+
`data-bcms-block`, `data-bcms-style` and the block's style cloned onto it. A COMPONENT child
|
|
111
|
+
cannot be handed unknown props, so write it as a function and spread `root` on the element the
|
|
112
|
+
component renders (`<section {...rootProps}>`). Without the function it is stamped on a box:
|
|
113
|
+
`display:contents` while its block has no style, one `display:flow-root` box carrying the
|
|
114
|
+
style once it has ([animation.md](animation.md) §10).
|
|
115
|
+
- A section no block names: `code:<bind>` always renders where it is written. A bare `<bind>`
|
|
116
|
+
does too until the list names one of the page's sections; from then on it is a removed one.
|
|
117
|
+
Write a bind bare only when the page already places it (`get_page`), otherwise `code:`.
|
|
118
|
+
- `blocks` null (no snapshot) renders every child in source order, untouched.
|
|
119
|
+
- Never inside a `"use client"` file: it reads its children's own elements, which a client
|
|
120
|
+
component cannot do across the server boundary.
|
|
121
|
+
|
|
62
122
|
## Lists, pagination and missing content
|
|
63
123
|
|
|
64
124
|
- `listEntries(model, { page, perPage })`: `page` is 1-based; `perPage` defaults to 20 and the
|