@bettercms-ai/mcp 0.68.1 → 0.69.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 +133 -48
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +8 -4
- package/dist/skills/bettercms/references/accessibility.md +171 -0
- package/dist/skills/bettercms/references/components.md +20 -4
- package/dist/skills/bettercms/references/content-modeling.md +153 -24
- package/dist/skills/bettercms/references/detection.md +450 -0
- package/dist/skills/bettercms/references/media-and-brand.md +2 -1
- package/dist/skills/bettercms/references/publishing.md +22 -5
- package/dist/skills/bettercms/references/seo.md +226 -26
- package/dist/skills/bettercms/references/site-blueprints.md +633 -2
- package/dist/skills/bettercms/references/site-review.md +2 -1
- package/dist/skills/bettercms/references/worked-examples-advanced.md +495 -0
- package/dist/skills/bettercms/references/worked-examples.md +725 -0
- package/dist/skills/bettercms/references/write-limits.md +138 -4
- package/package.json +2 -2
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Detection: reading a site before modeling it"
|
|
3
|
+
description: "How to read a rendered site or repo before any write: render and structured sources, section boundaries, the section classifier with confidence and tie-breaks, element to field mapping, decoration that is never content, edge cases and review flags."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Detection: reading a site correctly
|
|
7
|
+
|
|
8
|
+
**Outcome.** Before the first write you hold a list of bands per page, each with a class, a
|
|
9
|
+
confidence, the fields it needs and the flags a reviewer must see. Nothing on it is invented.
|
|
10
|
+
|
|
11
|
+
The platform has no crawl or render tool. You read the site with your own browser and fetch
|
|
12
|
+
tools, or from the repo and its build output. The platform's own derive step (`editor_setup`)
|
|
13
|
+
reads the build's HTML files, never the live URL, so anything that exists only after client-side
|
|
14
|
+
JavaScript runs is yours to find and model.
|
|
15
|
+
|
|
16
|
+
What to do with the reading: [content-modeling.md](content-modeling.md) (pages, collections,
|
|
17
|
+
fields), [componentize.md](componentize.md) (bands into components),
|
|
18
|
+
[bindings.md](bindings.md) (attributes), [portable-text.md](portable-text.md) (rich text values).
|
|
19
|
+
|
|
20
|
+
## Contents
|
|
21
|
+
|
|
22
|
+
- Reading: [1. Read order](#1-read-order) · [2. Fetch and render](#2-fetch-and-render) ·
|
|
23
|
+
[3. Structured sources](#3-structured-sources) · [4. Landmarks and chrome](#4-landmarks-and-chrome) ·
|
|
24
|
+
[5. Section boundaries](#5-section-boundaries) · [6. Repeats, collections and links](#6-repeats-collections-and-links)
|
|
25
|
+
- Classifying: [7. Section classifier](#7-section-classifier) · [8. Tie-breaks](#8-tie-breaks) ·
|
|
26
|
+
[9. Fallbacks](#9-fallbacks)
|
|
27
|
+
- Mapping: [10. Element to field](#10-element-to-field) ·
|
|
28
|
+
[11. Decoration is never content](#11-decoration-is-never-content) · [12. Edge cases](#12-edge-cases)
|
|
29
|
+
- Reporting: [13. Review flags](#13-review-flags) · [14. Most reliable signals](#14-most-reliable-signals) ·
|
|
30
|
+
[Acceptance](#acceptance)
|
|
31
|
+
|
|
32
|
+
## 1. Read order
|
|
33
|
+
|
|
34
|
+
Each step uses the output of the one before. Do not model after step 2.
|
|
35
|
+
|
|
36
|
+
1. Fetch and render (§2); collect structured sources while the page is open (§3).
|
|
37
|
+
2. Find the landmarks (§4), then cut `main` into bands (§5).
|
|
38
|
+
3. Find repeated shapes, collections and link targets (§6).
|
|
39
|
+
4. Classify each band and record its confidence (§7 to §9).
|
|
40
|
+
5. Map each element to a field: structured sources first, visible text second (§10).
|
|
41
|
+
6. Drop decoration and runtime state (§11), handle the edge cases (§12), record flags (§13).
|
|
42
|
+
|
|
43
|
+
Read at least three pages: the home page, one inner marketing page, one detail page (a post or
|
|
44
|
+
a product). One page cannot tell chrome from content.
|
|
45
|
+
|
|
46
|
+
## 2. Fetch and render
|
|
47
|
+
|
|
48
|
+
| The raw HTML shows | It is | Do |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Real headings and paragraphs inside `<main>` | Server-rendered or static | Parse the raw HTML. Render once anyway to catch client-only bands |
|
|
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 |
|
|
53
|
+
|
|
54
|
+
When you render:
|
|
55
|
+
|
|
56
|
+
- Wait until the network is idle and the framework root has children with no skeleton classes.
|
|
57
|
+
- Scroll to the bottom in steps, so lazy images and observer-loaded bands appear.
|
|
58
|
+
- Read computed styles at 1440, 834 and 390 px wide, not the CSS source.
|
|
59
|
+
- Let count-up animations finish, or read `data-target` / `data-count`. A number read mid
|
|
60
|
+
animation is often "0".
|
|
61
|
+
- Dismiss the cookie banner with the most private choice. It is not page content.
|
|
62
|
+
- Open every hidden state: closed accordion rows, inactive tabs, both sides of a billing toggle,
|
|
63
|
+
`<template>` and `<dialog>` contents. Hidden copy is content.
|
|
64
|
+
|
|
65
|
+
When the raw HTML and the rendered DOM disagree, the rendered DOM is what a visitor sees. Note
|
|
66
|
+
the difference: the derive step sees only the build HTML, so a band that exists only after
|
|
67
|
+
hydration gets no derived fields and needs declared bindings ([bindings.md](bindings.md) §4).
|
|
68
|
+
|
|
69
|
+
## 3. Structured sources
|
|
70
|
+
|
|
71
|
+
Prefer a structured source over visible text for the same fact.
|
|
72
|
+
|
|
73
|
+
| Source | Holds |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `<script type="application/ld+json">` | Product, Recipe, Event, JobPosting, LocalBusiness, VideoObject, FAQPage facts |
|
|
76
|
+
| `__NEXT_DATA__`, `window.__NUXT__`, `<script type="application/json">` | Page props, product JSON, variants |
|
|
77
|
+
| Library config in inline scripts | Carousel slides per view and autoplay delay, chart series, map markers |
|
|
78
|
+
| `data-*` attributes | `data-target`, `data-end`, `data-lat` / `data-lng`, `data-variant-id`, `data-category` |
|
|
79
|
+
| `<link rel="alternate" type="application/rss+xml">` | Posts, episodes, releases: the cleanest list of entries |
|
|
80
|
+
| `<link rel="alternate" hreflang>` | Which locale copies to read. Never a field ([localization.md](localization.md)) |
|
|
81
|
+
|
|
82
|
+
JSON-LD you read is evidence for field values, not a field to create ([seo.md](seo.md) §3).
|
|
83
|
+
|
|
84
|
+
## 4. Landmarks and chrome
|
|
85
|
+
|
|
86
|
+
| Landmark | Signals | Becomes |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| Header and nav | `<header>` or `role="banner"` early in `<body>`, a `<nav>`, a logo link to `/` | Global Layout |
|
|
89
|
+
| Announcement bar | A thin full-width strip above the header, one sentence, often a close button | Global Layout, its own section |
|
|
90
|
+
| Main | `<main>`, `role="main"`, or the largest container between header and footer | The page's bands |
|
|
91
|
+
| Sidebar | `<aside>`, a docs nav tree, a filter panel | Docs nav: chrome. A filter panel belongs to its listing band |
|
|
92
|
+
| Footer | `<footer>` or `role="contentinfo"` | Global Layout |
|
|
93
|
+
| Overlays | `<dialog>`, `role="dialog"`, off-canvas drawers | A sitewide modal is chrome. A cart drawer is runtime UI, not modelled |
|
|
94
|
+
|
|
95
|
+
Chrome is never a page section. Read it with `get_layout`, write it with `update_layout`, bind
|
|
96
|
+
it with `data-bcms-layout-field`, never `data-bcms-field` ([bindings.md](bindings.md) §3,
|
|
97
|
+
[components.md](components.md) §4). `get_componentize_plan` reports a nav, footer or modal group
|
|
98
|
+
as `NOT_A_SECTION` with detail `LAYOUT_CHROME`.
|
|
99
|
+
|
|
100
|
+
| Comparing the pages you read | Decision |
|
|
101
|
+
|---|---|
|
|
102
|
+
| Header or footer markup is the same on two or more pages | One Layout section. Never one per page |
|
|
103
|
+
| A sitewide band differs on a few pages | Still chrome. Report the pages that differ; do not copy it into each page |
|
|
104
|
+
| More than half of a band's values differ on more than a third of pages | Not chrome. A component placed per page |
|
|
105
|
+
| Same structure and styling, different words, in three or more places | One component. `list_extraction_candidates` finds these |
|
|
106
|
+
| Headers differ in structure by locale | Report it. A per-market header is not a translation |
|
|
107
|
+
|
|
108
|
+
## 5. Section boundaries
|
|
109
|
+
|
|
110
|
+
A band is one unit a marketer would move, duplicate or swap. Cut `main` at these signals,
|
|
111
|
+
strongest first. Two signals at the same point make the boundary certain.
|
|
112
|
+
|
|
113
|
+
1. A direct child of `<main>` that is a `<section>`, an `<article>`, or a wrapper with its own
|
|
114
|
+
`id` or `aria-labelledby`.
|
|
115
|
+
2. A background change: colour, image, gradient or a theme class (`data-theme="dark"`).
|
|
116
|
+
3. A new `h2` (the `h1` in the first band) that opens a new topic with its own intro.
|
|
117
|
+
4. A vertical gap clearly larger than the gaps inside its neighbours, or a full-width divider.
|
|
118
|
+
5. A change of layout system: a two-column split followed by a three-column grid.
|
|
119
|
+
|
|
120
|
+
- Consecutive bands that repeat one shape are ONE band with a list (alternating feature rows).
|
|
121
|
+
- Every element belongs to a band. Never leave loose headings, paragraphs or images on a page.
|
|
122
|
+
- A wrapper that only adds padding, max width or a grid is design, not a level.
|
|
123
|
+
- A long-form article is not cut into bands. Its body is one `document` field (at most one per
|
|
124
|
+
schema, top level).
|
|
125
|
+
- In the model each band is a `group`, a repeater, or keys sharing one prefix (`"hero_title"`,
|
|
126
|
+
`"hero_subtitle"`), so the componentize lane can find it
|
|
127
|
+
([content-modeling.md](content-modeling.md) §4).
|
|
128
|
+
|
|
129
|
+
## 6. Repeats, collections and links
|
|
130
|
+
|
|
131
|
+
**Same shape or different shape.** Compare each sibling's sequence of content elements (heading
|
|
132
|
+
level, `p`, `img`, `a`, `svg`, `ul`), ignoring wrappers and classes that only style. Siblings
|
|
133
|
+
match when the sequence is equal and optional parts are missing only at the ends.
|
|
134
|
+
|
|
135
|
+
| Pattern | Model |
|
|
136
|
+
|---|---|
|
|
137
|
+
| Two or more siblings of one shape (cards, tiers, logos, questions, steps) | A repeater. Inside a component: a `group` or `table` prop |
|
|
138
|
+
| A list of plain strings (check-mark bullets) | An `array` of scalars. No icon field, no link field |
|
|
139
|
+
| Siblings of different kinds in one grid (a card, a quote, a big number) | The section's ordered children. A `slot` prop holds ONE instance, so it is not a list |
|
|
140
|
+
| Same shape, each item linking to its own page | Entries of a collection |
|
|
141
|
+
|
|
142
|
+
Count content levels, not DOM nodes. Component props nest at most three levels; a deeper subtree
|
|
143
|
+
becomes its own component, placed as a child.
|
|
144
|
+
|
|
145
|
+
**Collections.** A set of records with one shape and their own URLs.
|
|
146
|
+
|
|
147
|
+
| Signal | Collection |
|
|
148
|
+
|---|---|
|
|
149
|
+
| Card links share a prefix: `/blog/`, `/customers/`, `/integrations/`, `/events/`, `/careers/`, `/docs/` | One per prefix, named for it |
|
|
150
|
+
| `/authors/<slug>`, `rel="author"` | Authors, referenced from posts |
|
|
151
|
+
| A listing with pagination (`?page=2`, `rel="next"`, "Load more") | The listing's source. Read every page of it |
|
|
152
|
+
| `/products/<handle>`, `cdn.shopify.com` | Products stay in the store ([shopify-headless.md](shopify-headless.md)) |
|
|
153
|
+
|
|
154
|
+
- One collection and one dynamic detail page, never one page per entry. The pattern takes a
|
|
155
|
+
single slug segment (`/blog/{slug}`). A nested or dated pattern is not available: report any
|
|
156
|
+
source URL that needs one (flag `url-pattern`) and plan its redirect.
|
|
157
|
+
- Follow each card link for the full record (body, author, dates). The card is a summary.
|
|
158
|
+
- A listing band is not a field of copied cards: the list page reads the collection in code. On
|
|
159
|
+
a hosted page it is a `collection` block: with `source: { modelId }` it sits on any page and
|
|
160
|
+
takes filters, a sort, pinned entries and `excludeCurrent`
|
|
161
|
+
([content-modeling.md](content-modeling.md) §2). Pagination and grouping are not available.
|
|
162
|
+
- A hand-picked set (no shared rule, rarely changes) is a `multi-reference` field.
|
|
163
|
+
- No URL per item, one page, under about 30 items: a repeater, not a collection.
|
|
164
|
+
|
|
165
|
+
**Links.** Resolve each one before you create anything.
|
|
166
|
+
|
|
167
|
+
| Link | Becomes |
|
|
168
|
+
|---|---|
|
|
169
|
+
| To an author, category or other shared record | A reference. Create the record first: a missing target is refused (400, ref_missing) |
|
|
170
|
+
| To a page on the same site, `#id`, `mailto:`, `tel:`, absolute URL | A `link` value as written. Other schemes are refused |
|
|
171
|
+
| A `<form>` | A Form document placed by id ([forms-and-interactions.md](forms-and-interactions.md)). Never inputs copied into a section |
|
|
172
|
+
| Matches nothing you read | Keep the href, flag `link-unresolved` |
|
|
173
|
+
|
|
174
|
+
## 7. Section classifier
|
|
175
|
+
|
|
176
|
+
For each band, find the row whose strong signals all match. One match: classify. Several: §8.
|
|
177
|
+
None: §9. The class picks the component family (`sectionType`) and the fields; it is never a
|
|
178
|
+
field itself.
|
|
179
|
+
|
|
180
|
+
| Confidence | Evidence | Action |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| High | A unique structural or vendor signal: `<form action="/cart/add">`, `role="tablist"`, JSON-LD of the right type | Model it |
|
|
183
|
+
| Medium | Layout and wording only | Model it, check it against a second page |
|
|
184
|
+
| Low | Wording only, or two rows tie | Model the simpler shape and flag it (§13) |
|
|
185
|
+
|
|
186
|
+
| Group | Band | Strong signals |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| Chrome | Header | Short link list, logo link, one filled button at the end, same on every page |
|
|
189
|
+
| Chrome | Mega menu | A top-level item holding a hidden panel with headed link columns or cards |
|
|
190
|
+
| Chrome | Announcement or promo bar | Strip above the header, close button, a code in `<code>`, digits with `data-end` |
|
|
191
|
+
| Chrome | Footer | Headed link lists, newsletter form, socials, a copyright row; minimal: one row, under about eight links |
|
|
192
|
+
| Hero | Split | Two columns: `h1`, text, one or two buttons, and one media column |
|
|
193
|
+
| Hero | Centered | Centered text at a narrow max width, a pill link above the `h1`, a screenshot below |
|
|
194
|
+
| Hero | Video | `<video autoplay muted loop playsinline>` behind the `h1`, an overlay element |
|
|
195
|
+
| Hero | Slider | `swiper-slide`, `splide__slide`, `aria-roledescription="carousel"`, full-width slides |
|
|
196
|
+
| Hero | Product | `<form action="/cart/add">`, option pickers, product JSON |
|
|
197
|
+
| Hero | Search or lead form | `input type="search"` as the main control; or a form of three to six inputs with a privacy line |
|
|
198
|
+
| Hero | Stats or event | Two to four large numerals with `data-count`; or `<time datetime>`, a place, Event JSON-LD |
|
|
199
|
+
| Features | Icon grid | Three to twelve same-shaped cards: icon of 24 to 48 px, `h3`, one `p`; no image over 64 px |
|
|
200
|
+
| Features | Alternating rows | Text and media rows that flip with `row-reverse`. One band, a rows list |
|
|
201
|
+
| Features | Bento | Grid children with different spans and different inner shapes |
|
|
202
|
+
| Features | Steps or timeline | Items led by "01" or "Step 1" with connector lines; or years on a vertical line |
|
|
203
|
+
| Features | Tabs with media | `role="tablist"` and same-shaped panels |
|
|
204
|
+
| Features | Accordion with image swap | `<details>` or `aria-expanded` rows beside one image per row |
|
|
205
|
+
| Features | Comparison | A table whose header holds brand names, check and cross cells |
|
|
206
|
+
| Features | Stats band | Two to five large numbers with small labels, footnote marks |
|
|
207
|
+
| Proof | Testimonials | Three or more cards with quote, name, role, avatar; `<blockquote>` and `<cite>` |
|
|
208
|
+
| Proof | Case study cards | Customer logo, a bold metric, "Read story" to `/customers/` |
|
|
209
|
+
| Proof | Logo cloud | Four to thirty small logos, one label, no other text |
|
|
210
|
+
| Proof | Press or awards | Publication logos with quotes linking out; or badge images (SOC 2, ISO, "Best of") |
|
|
211
|
+
| Proof | Review summary | A big average, stars, "Based on N reviews", review-app classes (`jdgm-`, `okendo-`, `yotpo-`) |
|
|
212
|
+
| Media | Gallery | Six or more images, CSS columns or a masonry script; filter buttons with `data-filter` |
|
|
213
|
+
| Media | Video | YouTube or Vimeo iframe, `<video><source>`, VideoObject JSON-LD |
|
|
214
|
+
| Media | Before and after | Two stacked images, the top one clipped, a handle or range input |
|
|
215
|
+
| Commerce | Product grid | Cards with image, a `/products/` title link, a currency price, "Shop all" |
|
|
216
|
+
| Commerce | Product detail | One `h1`, a price block, a cart form with a hidden variant id, a specs table |
|
|
217
|
+
| Commerce | Store locator | A map beside a scrolling address list, postcode search, `data-lat` / `data-lng` |
|
|
218
|
+
| Pricing | Plan tiers | Two to four columns: plan name, price with "/mo", a button, a check list; a billing switch |
|
|
219
|
+
| Pricing | Feature matrix | A large table under the plan cards, plan columns, check and cross cells |
|
|
220
|
+
| Pricing | Calculator | Range sliders with listeners, a live total in `<output>`, no submit |
|
|
221
|
+
| Conversion | CTA band | Full-width high-contrast band, one heading, at most one sentence, one or two buttons, no inputs |
|
|
222
|
+
| Conversion | Newsletter | A form with exactly one `type="email"` input, a consent line, a mail-provider action URL |
|
|
223
|
+
| Conversion | Lead capture | Copy and bullets beside a multi-field form; fieldsets with Next and Back for multi-step |
|
|
224
|
+
| Conversion | Contact | `mailto:` and `tel:` links by label, a form, a map iframe, hours |
|
|
225
|
+
| Conversion | Popup | A hidden `role="dialog"` shown on a timer, scroll or mouse leave |
|
|
226
|
+
| Listing | Blog index | `<article>` cards with image, heading link, `<time>`, excerpt; a shared href prefix |
|
|
227
|
+
| Listing | Related posts | "Related" or "Keep reading", two to four cards after `</article>` |
|
|
228
|
+
| Listing | Changelog | Version or date headings, New/Improved/Fixed badges, per-version anchors |
|
|
229
|
+
| Long-form | Blog post | `<article>` with one `h1`, a byline, a hero image, a rich body |
|
|
230
|
+
| Long-form | Docs page | Left nav tree, article, right "On this page" anchors, prev and next |
|
|
231
|
+
| Long-form | Recipe, job, event detail | JSON-LD of that type, or the matching plugin classes |
|
|
232
|
+
| Company | Team or leadership | Headshots with name and role; leadership adds C-level titles, bio modals, Board headings |
|
|
233
|
+
| Data | Table | `<table>` with `<th scope="col">`, more than two rows, a `<caption>` |
|
|
234
|
+
| Data | Chart or map | An `<svg>` with axis ticks or a chart library global; a Leaflet, Mapbox or Google container |
|
|
235
|
+
| Data | Embed or code | A third-party `<iframe>` or script widget; `<pre><code class="language-*">` |
|
|
236
|
+
|
|
237
|
+
## 8. Tie-breaks
|
|
238
|
+
|
|
239
|
+
| Close call | Test | Answer |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| Hero or CTA band | First band after the header and holds the page `h1` | Hero. Its type follows its control (form, search, video, slider, product form, stats) |
|
|
242
|
+
| | One heading, at most one sentence, one or two buttons, no inputs | CTA band |
|
|
243
|
+
| | Holds an email input | Newsletter or lead form, never a CTA band |
|
|
244
|
+
| | Two or more paragraphs, or a list | A feature or content band |
|
|
245
|
+
| Grid, tiers, table or collection (stop at the first yes) | Each item has a price with a period ("/mo") and its own button | Pricing tiers, a repeater. A "Compare all features" grid below is a separate table band |
|
|
246
|
+
| | Two-dimensional facts: rows of one kind, columns of one attribute, short values | A table. A layout `<table>` (nested tables, images, one row) is not |
|
|
247
|
+
| | Each item links to its own page under a shared prefix | A collection and a listing |
|
|
248
|
+
| | Images over 64 px dominate each card | Image card grid; an icon of 24 to 48 px with `h3` and `p` is an icon grid |
|
|
249
|
+
| Testimonials or reviews | Curated quotes with a named person, role, company | Testimonials: a collection, picked by reference |
|
|
250
|
+
| | Many ratings from a review app, an aggregate score | Reviews: the app stays the source. Never type the average or count in as content |
|
|
251
|
+
| Logos | Each links to `/integrations/<slug>` or carries a name and one line | Integrations: a collection |
|
|
252
|
+
| | Each sits with a quote and an external article link | Press mentions |
|
|
253
|
+
| | Badges with certification or award words | Awards |
|
|
254
|
+
| | Logos and one label ("Trusted by") | Logo cloud. Under the hero copy it is part of the hero |
|
|
255
|
+
| Accordion, tabs or FAQ | `role="tablist"`, panels of one shape | Tabs: a repeater. Panels of different shapes: separate children |
|
|
256
|
+
| | Tabs that set a value (sort, unit, billing cycle) | A `select`, not panels |
|
|
257
|
+
| | `<details>` rows with one image per row beside them | Accordion with image swap, not an FAQ |
|
|
258
|
+
| | `<details>` rows phrased as questions | FAQ. FAQPage structured data is never automatic: it needs an explicit `schemaType` ([seo.md](seo.md) §3) |
|
|
259
|
+
| Stats band or stats hero | Holds the page `h1`? | Hero if yes |
|
|
260
|
+
| Code snippet or body code | Inside a long-form article? | A code block in the body if yes |
|
|
261
|
+
| Embed or video | YouTube or Vimeo? | Video; any other host is an embed |
|
|
262
|
+
| Popup or site setting | Cookie banner or age gate? | Not an overlay band. Report the consent tool |
|
|
263
|
+
| Changelog band or collection | Over about 30 releases, or each has its own URL? | Collection |
|
|
264
|
+
| Mega menu or simple nav | Any top-level item with a hidden panel? | Mega menu for that item; the others stay links |
|
|
265
|
+
|
|
266
|
+
## 9. Fallbacks
|
|
267
|
+
|
|
268
|
+
| Situation | Do |
|
|
269
|
+
|---|---|
|
|
270
|
+
| No row matches; the band has a heading, text, optional media and buttons | The closest generic shape (split or centered content). Flag `unclassified` |
|
|
271
|
+
| Two rows tie at medium confidence | Take the one with fewer fields. Flag `tie` and name both classes |
|
|
272
|
+
| A widget you cannot read (closed iframe, canvas with no data) | Keep it in code as written. Model only the copy around it. Flag `third-party` |
|
|
273
|
+
| Article modules of unknown shape | Convert what Portable Text accepts; a foreign block needs a renderer and a mapping, or it is named in the report ([portable-text.md](portable-text.md) §5) |
|
|
274
|
+
| Logic you cannot express as fields (formulas, multi-condition rules, quiz weights) | The logic stays in code. Model only the copy. Never approximate. Flag `logic-in-code` |
|
|
275
|
+
|
|
276
|
+
## 10. Element to field
|
|
277
|
+
|
|
278
|
+
Field keys are flat and prefixed by their band: `"hero_title"`, `"pricing_tiers"`. Labels name
|
|
279
|
+
what the editor sees ([content-modeling.md](content-modeling.md) §5).
|
|
280
|
+
|
|
281
|
+
**Text, links and images**
|
|
282
|
+
|
|
283
|
+
| Element | Field | Rule |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| Small uppercase or tinted text directly above a heading | text | Only when visually separate from the heading |
|
|
286
|
+
| The band heading | text | A highlighted `<span>` inside it stays in the value with its classes. Never restyle it |
|
|
287
|
+
| Paragraphs with links, bold, lists, or more than one paragraph | richtext | A Portable Text array |
|
|
288
|
+
| Item heading (`h3`, `h4`) | text | Drop typed numbering: "01 Start" is "Start" |
|
|
289
|
+
| Small meta text in a card ("2 min", "Remote") | Its own field | Never inside the title |
|
|
290
|
+
| Quote, with `<cite>` or `figcaption` | text, plus separate name, role and company | Drop quotation marks the markup draws |
|
|
291
|
+
| `<a>` or `<button>` drawn as a button | One `button` field | Never a text field beside a link field: refused as `cta_pair` |
|
|
292
|
+
| A standalone text link ("Learn more") | `link` | An inline link in running text is a mark inside the rich text |
|
|
293
|
+
| Submit button, "Add to cart" | No field | The Form owns its label; a store action is wired in code |
|
|
294
|
+
| `<img>` | image | Take the largest `srcset` entry or the lightbox href, never the thumbnail |
|
|
295
|
+
| `alt=""` or `role="presentation"` | Empty alt text | The empty alt is the signal on the value. Decorative wherever it is used: `update_media` `decorative: true` on the asset |
|
|
296
|
+
| `alt` is "logo", "image", a file name, or missing on a meaningful image | Write one from visible context | Flag `alt-written`. No context: leave it empty, flag `alt-missing`. Never from the file name |
|
|
297
|
+
| `<picture>` with a different photo per width | A second image field for the small screen | Art direction is another photo, not a crop |
|
|
298
|
+
| `object-position` or a focal class | The image value's `focal: { x, y }`, fractions 0 to 1 | "30% 70%" is 0.3 and 0.7. Never a focal field in the model ([media-and-brand.md](media-and-brand.md)) |
|
|
299
|
+
| CSS `background-image` carrying a photo | image | The overlay and gradient are design |
|
|
300
|
+
| A screenshot inside a phone or browser frame | The screenshot only | The frame is design. Baked into the file: keep it, flag |
|
|
301
|
+
| App store badges, payment marks, QR codes | Links by destination | A QR code is its decoded URL. Undecodable: flag |
|
|
302
|
+
|
|
303
|
+
**Icons**
|
|
304
|
+
|
|
305
|
+
| Source | Do |
|
|
306
|
+
|---|---|
|
|
307
|
+
| Inline `<svg>` of 16 to 48 px beside a heading or label | An `image` field with `config.media: "icon"`, holding the SVG. An editor swaps it |
|
|
308
|
+
| Icon font class (`ti-*`, `lucide-*`, `fa-*`, `bi-*`) | The same field. The prefix names the pack to export the glyph from |
|
|
309
|
+
| `aria-label`, `sr-only` text or `<title>` on the icon | The field's name, and the label of an icon-only link |
|
|
310
|
+
| Check or cross glyphs in a list or table | Not icons. A list marker (drop it) or a yes/no cell |
|
|
311
|
+
| Icon colour, size, background; text inside an `<svg>` | Design, never fields; SVG text is never bound |
|
|
312
|
+
|
|
313
|
+
**Video, audio and embeds**
|
|
314
|
+
|
|
315
|
+
| Source | Do |
|
|
316
|
+
|---|---|
|
|
317
|
+
| `<video>` with `<source>` | The best source as the value, bound through `data-bcms-props` as kind `url` |
|
|
318
|
+
| `poster` | An image field. Captured from a frame because none exists: flag `data-estimated` |
|
|
319
|
+
| `<track>` captions | Keep the file reference. Never generate captions. Speech with none: flag `captions-missing` |
|
|
320
|
+
| A transcript on the page | The asset's transcript, set with `update_media` |
|
|
321
|
+
| YouTube or Vimeo iframe or facade | The canonical watch URL, with any start offset. Do not copy the video into the library |
|
|
322
|
+
| `<audio>`, podcast host iframe | The audio file named by the RSS enclosure, not the iframe |
|
|
323
|
+
| Third-party `<iframe>` | Stays in code; model its `src` only where an editor changes it |
|
|
324
|
+
| Script widget (booking, chat, reviews), tracking pixels | Report provider and id. Never store the script, a key or the widget's HTML |
|
|
325
|
+
|
|
326
|
+
**Prices, numbers, dates, hours, badges**
|
|
327
|
+
|
|
328
|
+
| Value | Read as | Never |
|
|
329
|
+
|---|---|---|
|
|
330
|
+
| Price from a store | A reference to the store's product | Copy the price, compare-at price, stock or options |
|
|
331
|
+
| Plan price | Amount, currency and period as separate values. Read both sides of a billing toggle | Derive yearly from monthly by a percentage |
|
|
332
|
+
| "Contact us", "Custom" | The label as text | A made-up number |
|
|
333
|
+
| Stat "$2.4B", "94%", "10k+" | Prefix, value and suffix apart when the site animates the number; else one text value | Expand "1.2M" to 1200000 |
|
|
334
|
+
| Count-up number | The `data-target` / `data-count` value | The mid-animation text |
|
|
335
|
+
| Aggregate rating, review count, "n people joined" | Stays with its connected source. Typed into copy: flag `stale-risk` | Type it in as content |
|
|
336
|
+
| Date | A `date` field, ISO. A bare year or "Spring 2016" stays text | Put "2014" in a date field |
|
|
337
|
+
| Date and time (events, sales, countdowns) | A `datetime` field, ISO with its offset | Guess a zone. None on the page: the value as printed, flag `zone-assumed` |
|
|
338
|
+
| Opening hours | A repeater of seven day rows: opens, closes (24-hour HH:MM), closed. Closes before opens means past midnight | One text line. "Open now" is runtime state |
|
|
339
|
+
| Badge ("New", "Sale", "Beta") | A `select` of the values the site uses | Free text that breaks a filter |
|
|
340
|
+
|
|
341
|
+
**Lists and tables**
|
|
342
|
+
|
|
343
|
+
| Source | Field |
|
|
344
|
+
|---|---|
|
|
345
|
+
| `<ul>` of short benefits with check icons | An `array` of strings. The icon is the marker; bind the text's own `<span>` |
|
|
346
|
+
| `<ul>` / `<ol>` inside prose | Flat list items in the rich text |
|
|
347
|
+
| `<ol>` of steps with headings, `<dl>` | A repeater (the step number is derived); a repeater of term and definition pairs |
|
|
348
|
+
| A real data table | In an article: a `table` block in the rich text, where the field accepts it. In a section: a `table` prop, or a repeater of rows |
|
|
349
|
+
| A layout table | Its contents become bands |
|
|
350
|
+
| A div grid that behaves like a table (equal children per row, a header row, `data-label`) | Read it as a table |
|
|
351
|
+
|
|
352
|
+
Table cells: check, "Yes" and "Included" read as true; cross, "No", a dash or empty read as
|
|
353
|
+
false in a yes/no column. A merged body cell: copy the value into each covered cell and flag
|
|
354
|
+
`merged-cells`. Over about 60 rows: a collection, loaded by code against the Management API.
|
|
355
|
+
|
|
356
|
+
## 11. Decoration is never content
|
|
357
|
+
|
|
358
|
+
Create no field for any of these.
|
|
359
|
+
|
|
360
|
+
- Dividers, `<hr>` used as spacing, connector lines between steps, timeline lines and dots.
|
|
361
|
+
- Blobs, blurred circles, mesh backgrounds, gradients, noise, patterns. A section surface is a
|
|
362
|
+
closed style token ([media-and-brand.md](media-and-brand.md)), never a hex or a CSS string.
|
|
363
|
+
- Cloned DOM for marquees and looping carousels (`aria-hidden` copies, `.swiper-slide-duplicate`,
|
|
364
|
+
`.slick-cloned`).
|
|
365
|
+
- Hamburger buttons, carousel arrows and dots, copy buttons, "Print", share bars.
|
|
366
|
+
- Derived content: table of contents, letter bars, totals ("6 modules, 42 lessons"), step and
|
|
367
|
+
footnote numbers, prev and next pagers, breadcrumbs.
|
|
368
|
+
- Runtime state: cart counts, customer names, live tallies, search results, facet values.
|
|
369
|
+
- Animation choreography: count-up, stagger, scroll reveal, parallax speed.
|
|
370
|
+
- Card backgrounds, borders, shadows, hover and focus states.
|
|
371
|
+
- `sr-only` copies of visible text. When it is the only text of an icon, it is that icon's label.
|
|
372
|
+
|
|
373
|
+
Arrangement (media left or right, grid or carousel, centered or split) is a variant of the
|
|
374
|
+
component, not content ([components.md](components.md) §3). Never edit a class or a style to
|
|
375
|
+
make the site fit the model.
|
|
376
|
+
|
|
377
|
+
## 12. Edge cases
|
|
378
|
+
|
|
379
|
+
| Case | Detect | Do |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| Duplicate mobile and desktop markup | Two copies, one `hidden md:block`, the other `md:hidden` | Model ONE: the more complete copy. Both elements carry the same address and are edited as one value. A different phone photo is a second image field. There are no per-breakpoint values |
|
|
382
|
+
| Carousel clones, marquees | Swiper, Splide, Slick, Embla classes; a duplicated track with an `aria-hidden` copy | Read originals only. Dedupe by image URL plus heading |
|
|
383
|
+
| Hidden states | `role="tablist"`, `<details>`, `aria-expanded="false"`, `hidden` panels | Read every panel and answer. Note which opens first |
|
|
384
|
+
| Lazy content | `loading="lazy"`, `data-src`, skeletons | Scroll and wait. Read `data-src` / `data-srcset` when `src` is a placeholder |
|
|
385
|
+
| Count-up numbers | `data-count`, `countup`, `odometer` | The data attribute, not the visible text |
|
|
386
|
+
| Locale copies | `/de/`, `/fr/`, hreflang, `<html lang>` | Match bands by position and structure, never by text. Each locale is its own row; never translate, never add a locale field ([localization.md](localization.md)) |
|
|
387
|
+
| Experiments and personalization | Hidden copies with `data-variant`, an experiment SDK, an anti-flicker snippet | Model the version served with no parameters. Report the SDK. Never invent audiences |
|
|
388
|
+
| Pagination, "Load more", infinite scroll | `?page=2`, `rel="next"`, a button fetching JSON | Read every page or the JSON endpoint, so the collection is complete |
|
|
389
|
+
| Client-fetched lists | A recommendations or search API, `data-collection` | Stays in code with its owner ([external-content.md](external-content.md)). Never freeze it into a typed list. Unsure: flag `snapshot-of-feed` |
|
|
390
|
+
| Canvas and WebGL | `<canvas>`, a 3D runtime | Stays in code. A chart: recover its data from the script first |
|
|
391
|
+
| Compliance claims | "HIPAA certified", badges with no date | Keep as written, flag `claim-unverified` |
|
|
392
|
+
| Fabricated urgency | Per-visitor countdown resets, fake counters, pre-ticked consent | Do not migrate a fabricated number. Flag with a neutral rewrite |
|
|
393
|
+
|
|
394
|
+
## 13. Review flags
|
|
395
|
+
|
|
396
|
+
The skill asks the user two questions only (authoring preference, media delivery). Detection
|
|
397
|
+
adds none. When the reading leaves a doubt, take the safer shape, write it, and record a flag in
|
|
398
|
+
your report with the page, the band and what you chose. Publishing still needs authorization.
|
|
399
|
+
|
|
400
|
+
| Flag | When | Default taken |
|
|
401
|
+
|---|---|---|
|
|
402
|
+
| `zone-assumed` | A date and time with no zone on the page | The value as printed, no invented offset |
|
|
403
|
+
| `default-locale-assumed` | Locale copies disagree on which is the default | The `<html lang>` of the unprefixed route |
|
|
404
|
+
| `chrome-unsure` | A chrome candidate that differs on many pages | A per-page component |
|
|
405
|
+
| `variant-unread` | An experiment whose targeting you cannot read | The no-parameter version |
|
|
406
|
+
| `alt-missing` | A QR code you cannot decode, a logo you cannot name, an image with no context | Keep the image, empty alt |
|
|
407
|
+
| `snapshot-of-feed` | A list that may be hand-picked or rule-based | Hand-picked references |
|
|
408
|
+
| `unclassified` / `tie` | No classifier row matched; or two matched at medium confidence | Generic shape; the class with fewer fields, both named |
|
|
409
|
+
| `alt-written` | You wrote alt text, or replaced "logo" or "image" | |
|
|
410
|
+
| `label-from-placeholder` | A form input had only a placeholder | Promoted to its label |
|
|
411
|
+
| `captions-missing` | A video with speech has no `<track>` | |
|
|
412
|
+
| `data-estimated` | A chart read from axis ticks, a first-frame poster | |
|
|
413
|
+
| `merged-cells` | A table body had merged cells | The value copied into each cell |
|
|
414
|
+
| `claim-unverified` | Compliance badges, comparative claims with no date | Kept as written |
|
|
415
|
+
| `link-unresolved` | A link that matches no record or page | The href kept |
|
|
416
|
+
| `stale-risk` | A price, rating or count typed into copy ("Just $165!") | Kept as written |
|
|
417
|
+
| `logic-in-code` | A formula, rule or weight you kept in code | |
|
|
418
|
+
| `third-party` | A vendor widget, consent tool or form you reported and did not rebuild | |
|
|
419
|
+
| `url-pattern` | A source URL the single-slug pattern cannot keep | A redirect planned |
|
|
420
|
+
|
|
421
|
+
Never:
|
|
422
|
+
|
|
423
|
+
- Invent captions, audiences, discount codes, review numbers or usage rights.
|
|
424
|
+
- Copy store prices, stock, cart counts or facet values into content.
|
|
425
|
+
- Store scripts, API keys, tracking ids or a provider's HTML.
|
|
426
|
+
- Create SEO, meta, hreflang or JSON-LD fields ([seo.md](seo.md)).
|
|
427
|
+
- Model one fact twice (the footer email and the contact page email). One source, read by both.
|
|
428
|
+
|
|
429
|
+
## 14. Most reliable signals
|
|
430
|
+
|
|
431
|
+
When two sources disagree, the one higher in this list wins.
|
|
432
|
+
|
|
433
|
+
1. JSON-LD of the right type. Trust it over visible text.
|
|
434
|
+
2. Form actions: `/cart/add` (store), a mail-provider host (newsletter), a CRM form host (lead).
|
|
435
|
+
3. ARIA roles: `tablist`, `dialog`, `table`, `search`, `banner`, `contentinfo`, `aria-current`.
|
|
436
|
+
4. A shared URL prefix on card links (a collection); RSS for the full list of entries.
|
|
437
|
+
5. Library class names (`swiper-slide`, `wprm-recipe`, `jdgm-`, `leaflet-container`) and data
|
|
438
|
+
attributes (`data-target`, `data-end`, `data-lat`, `data-variant-id`).
|
|
439
|
+
6. The same markup on two or three pages: chrome.
|
|
440
|
+
7. Background and theme changes: band boundaries.
|
|
441
|
+
8. Sibling shape: equal means a list, different means separate children.
|
|
442
|
+
|
|
443
|
+
## Acceptance
|
|
444
|
+
|
|
445
|
+
- Three or more pages read, rendered where the raw HTML was empty, hidden states opened.
|
|
446
|
+
- Every band on every page has a class, a confidence and its fields. None is loose.
|
|
447
|
+
- Chrome is in the Global Layout, on no page.
|
|
448
|
+
- Every repeated shape is one list; every set with its own URLs is a collection.
|
|
449
|
+
- No field holds decoration, runtime state, a clone or a derived value.
|
|
450
|
+
- Every low-confidence call and every default taken is a named flag in the report.
|
|
@@ -73,7 +73,8 @@ H1. Expose typography variants only where the design allows a real choice.
|
|
|
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
75
|
`srcset`/`sizes`, poster, lazy/preload intent and art direction. Decorative images render an
|
|
76
|
-
empty `alt`; informative ones need text that fits the context.
|
|
76
|
+
empty `alt`; informative ones need text that fits the context. `update_media` with
|
|
77
|
+
`decorative: true` marks a library asset decorative, so the audit stops asking for its alt text.
|
|
77
78
|
- **Request the size you render.** Mint resize URLs with `@bettercms-ai/image-url`
|
|
78
79
|
(`urlFor = imageUrlBuilder({ baseUrl: mediaUrl })`, then `urlFor(asset).size(800, 450).url()`;
|
|
79
80
|
also `width`, `format`, `quality`, `fit`, `dpr` 1–3) and a
|
|
@@ -59,7 +59,21 @@ On a site BetterCMS hosts, a publish applies in place with no build: values, Lay
|
|
|
59
59
|
page with HTML on disk, and a reorder or removal of sections the build draws from the CMS (every band
|
|
60
60
|
stamped `data-bcms-block`, measured in CMS order). Only what needs new markup builds, by itself: a
|
|
61
61
|
new or removed page, a new section or one whose component changed, a Layout structure change, SEO
|
|
62
|
-
of a
|
|
62
|
+
of a page with no HTML on disk, and a list whose published row count the page cannot take (it
|
|
63
|
+
renders rows no address names, or was built empty). That publish waits for its build ("Rebuilding so
|
|
64
|
+
the list shows N rows") and fails with the reason if the build still shows the page's own rows: the
|
|
65
|
+
site's code renders the list itself. `get_next_steps` names what is waiting.
|
|
66
|
+
|
|
67
|
+
The SEO a page or entry stores is what its `<head>` says, whenever it was set: the title, the
|
|
68
|
+
description, the canonical, the card fields, a `noindex` that is on. A field left empty keeps what
|
|
69
|
+
the build rendered, and a site default only fills a tag the build has none of. Only a real choice
|
|
70
|
+
replaces what the build renders: a meta title that merely repeats the page's name (whatever its
|
|
71
|
+
case), a description that repeats the title or is the entry's auto-filled first paragraph, and
|
|
72
|
+
anything on a row no person or agent has ever written (`origin` `system:*`) are read as not set. To
|
|
73
|
+
change such a head, set the title or description yourself. On a route the site's app renders, no write
|
|
74
|
+
reaches the head: a publish whose SEO the page does not show fails with "Published, but this page's
|
|
75
|
+
code doesn't read this field yet: SEO title", and the fix is to render the head from the published
|
|
76
|
+
values.
|
|
63
77
|
|
|
64
78
|
Builds run `build:bcms` instead of `build` when `package.json` declares it. Keep CI-only checks out
|
|
65
79
|
of it and every step the build needs in: `"build": "astro check && astro build"` beside
|
|
@@ -106,9 +120,12 @@ Identify the previous source snapshot or commit, the new one, the import manifes
|
|
|
106
120
|
source-to-target id map and the current BetterCMS state. `list_activity` with `since` reads the
|
|
107
121
|
project's durable change feed from an ISO timestamp (up to 100 events per call): call it with the
|
|
108
122
|
time of the previous sync to see every dashboard or agent edit made since. Compare by stable identity, never
|
|
109
|
-
by display name or row order, and confirm it is the same project and source branch.
|
|
110
|
-
|
|
111
|
-
`
|
|
123
|
+
by display name or row order, and confirm it is the same project and source branch. Entries
|
|
124
|
+
written with `externalKey` (the source id) are their own id map: `list_content_entries` with
|
|
125
|
+
`externalKey` finds one, and re-sending the row answers `created`, `updated` or `unchanged`
|
|
126
|
+
([write-limits.md](write-limits.md)). Media, and references to items not yet created, still
|
|
127
|
+
need the map. Without a manifest, keys or an id map this is no longer an automatic sync; treat
|
|
128
|
+
it as a reviewed migration (the `bettercms-migrate` skill).
|
|
112
129
|
|
|
113
130
|
Classify each difference and keep a three-way view where you can (the value last imported, the
|
|
114
131
|
current source value, the current CMS value). Never flatten a person's CMS edit back to imported
|
|
@@ -139,7 +156,7 @@ for a content value the sync got wrong is §4.
|
|
|
139
156
|
| Notify another system of an event | Event webhooks, `list_webhooks` / `create_webhook`: `content.publish`, `content.unpublish`, `content.stage_changed` (workflow stage), `content.created`, `content.updated`, `content.deleted` and `deployment.triggered`; HMAC-SHA256 signed in `X-BetterCMS-Signature`; payloads carry ids, never content. The signing `secret` is returned ONCE, on create. A receiver that writes back on `content.updated` loops unless it skips its own writes. Per-subscription payload filters: dashboard | You |
|
|
140
157
|
| Send form submissions somewhere | Per-form integrations: Google Sheets, Slack, a webhook, email | Dashboard only |
|
|
141
158
|
| Go live or retire at a time | `scheduledAt` (publish a draft at a time; needs publish permission) and `unpublishAt` (return a published row to draft) on `update_content_entry` and `update_page`; `null` cancels either. Campaigns: `publish_campaign` takes both | You. An edit that omits `scheduledAt` CANCELS the pending publish, so resend it with every edit before it fires; omitting `unpublishAt` never cancels it |
|
|
142
|
-
| Load many entries | `create_content_entries`: up to 200 drafts of one collection per call, each row `created` or `failed
|
|
159
|
+
| Load many entries | `create_content_entries`: up to 200 drafts of one collection per call, each row `created` or `failed` (with `externalKey` also `updated` or `unchanged`, so a re-run is safe); then `publish_entries` | You |
|
|
143
160
|
| React to edits made elsewhere | Poll `list_activity` with `since` set to the time of your last read (1–100 events per call) | You |
|
|
144
161
|
| Generate or rewrite in bulk with a human gate | AI jobs: `list_ai_jobs`, `get_ai_job`, then `approve_ai_job` or `reject_ai_job` on the user's word | User approves |
|
|
145
162
|
|