@bettercms-ai/mcp 0.83.0 → 0.83.2

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.
Files changed (40) hide show
  1. package/README.md +86 -86
  2. package/SKILL.md +160 -160
  3. package/dist/index.js +38 -9
  4. package/dist/index.js.map +1 -1
  5. package/dist/kit-catalog.json +1509 -1509
  6. package/dist/skills/bettercms/SKILL.md +206 -206
  7. package/dist/skills/bettercms/references/accessibility.md +171 -171
  8. package/dist/skills/bettercms/references/animation.md +296 -296
  9. package/dist/skills/bettercms/references/astro.md +148 -148
  10. package/dist/skills/bettercms/references/bindings.md +366 -366
  11. package/dist/skills/bettercms/references/componentize.md +185 -185
  12. package/dist/skills/bettercms/references/components.md +415 -415
  13. package/dist/skills/bettercms/references/content-experiments.md +62 -62
  14. package/dist/skills/bettercms/references/content-modeling.md +382 -382
  15. package/dist/skills/bettercms/references/detection.md +465 -465
  16. package/dist/skills/bettercms/references/dock-input.md +121 -121
  17. package/dist/skills/bettercms/references/editor-chrome.md +92 -92
  18. package/dist/skills/bettercms/references/external-content.md +78 -78
  19. package/dist/skills/bettercms/references/finish-site.md +377 -377
  20. package/dist/skills/bettercms/references/forms-and-interactions.md +245 -245
  21. package/dist/skills/bettercms/references/frameworks.md +159 -159
  22. package/dist/skills/bettercms/references/getting-started.md +156 -156
  23. package/dist/skills/bettercms/references/integrations.md +364 -364
  24. package/dist/skills/bettercms/references/kit-catalog.md +3259 -3259
  25. package/dist/skills/bettercms/references/kit-scenarios.md +712 -712
  26. package/dist/skills/bettercms/references/localization.md +85 -85
  27. package/dist/skills/bettercms/references/media-and-brand.md +181 -181
  28. package/dist/skills/bettercms/references/next.md +189 -189
  29. package/dist/skills/bettercms/references/other-frameworks.md +85 -85
  30. package/dist/skills/bettercms/references/portable-text.md +213 -213
  31. package/dist/skills/bettercms/references/programmatic-seo.md +98 -98
  32. package/dist/skills/bettercms/references/publishing.md +289 -289
  33. package/dist/skills/bettercms/references/seo.md +357 -357
  34. package/dist/skills/bettercms/references/shopify-headless.md +52 -52
  35. package/dist/skills/bettercms/references/site-blueprints.md +895 -895
  36. package/dist/skills/bettercms/references/site-review.md +74 -74
  37. package/dist/skills/bettercms/references/worked-examples-advanced.md +509 -509
  38. package/dist/skills/bettercms/references/worked-examples.md +885 -885
  39. package/dist/skills/bettercms/references/write-limits.md +217 -217
  40. package/package.json +1 -1
@@ -1,85 +1,85 @@
1
- ---
2
- title: "Localization"
3
- description: "Document-per-locale translations (locale, translationOf, translate_entry, delivery translations), URL-prefixed locale copies of imported builds, Layout locale variants, locale-unvarianted, hreflang, and what never to model."
4
- ---
5
-
6
- # Localization
7
-
8
- **Outcome.** A multilingual site keeps every language rendering, each language's copy is
9
- editable, and nothing invents a locale model the platform does not read.
10
-
11
- Two things are called a "language" here, and they are separate:
12
-
13
- - **A translation** is its own entry or page row, one per locale: `locale` is its BCP-47 tag (one
14
- of the project's locales; null = the default language) and `translationOf` is the SOURCE row's
15
- id. Each has its own slug, draft and publish state.
16
- - **A locale copy of an imported build** is a URL prefix (`/fr`, `/pt-br`) proven by the page's own
17
- `<html lang>`. It is how a converted site's languages arrive, before anyone models them.
18
-
19
- ## 1. What exists
20
-
21
- | Piece | How it works |
22
- |---|---|
23
- | The project's locales | `localeConfig.locales`: the owner's list (project settings) plus every page language an import finds. A `locale` outside it is refused with 400; an empty list means a single-language project. Tags are stored canonical (`zh-CN` → `zh-cn`) |
24
- | A page's language | `list_pages` / `get_page` return `language` (its `locale`, else the `<html lang>` its import declared; null = the default) and `path`. Set it with `create_page` / `update_page` `locale` |
25
- | Address a page | By id or `path` (`/de/construction/safety`). A slug repeats across language folders, so a bare slug works only when one page has it; otherwise the tool refuses with each match's id, path and language. Never guess one |
26
- | Translate an entry with AI | `translate_entry` { entryId, targetLocale, slug?, fields? } creates the translation as a DRAFT with `locale` and `translationOf` set (AI credits: 402/403 when out or not on the plan). Text and longtext are translated; rich text and documents are copied for you to translate; `meta` lists which. `409 locale_conflict`: that translation exists, so edit it instead |
27
- | Create one by hand | `create_content_entry` or `create_page` with `locale` and `translationOf` = the source's id, never another translation's. One row per locale per source (a second is `409 locale_conflict`); `update_content_entry` / `update_page` change either, null clears |
28
- | Find them | `list_content_entries` `where: { locale: 'fr' }`; default-language rows have a null `locale`, so they match `{ locale: { ne: 'fr' } }`, never `eq` |
29
- | Deliver them | Every delivered entry carries `locale`, `translationOf` and `translations`: `[{ id, locale, slug }]` of its group's other PUBLISHED rows (drafts only when previewing). A delivered page carries `locale` and `translationOf` |
30
- | Translate one string | `write_content` with `action: "translate"` and `targetLang` returns the text; apply it with `set_page_content` or `update_content_entry` |
31
- | Locale copy of an imported page | A page whose route opens with a language prefix its own `<html lang>` confirms (`/fr` on a French page). The prefix is proven by the page's language, never by the segment's shape: `/de` is a locale only on a German page |
32
- | Componentizing a locale copy | `get_componentize_plan` places `/fr` on its default-locale twin's components, band by band, when each pair holds the same prop types in the same order; a mismatched band stays its own section. `componentize_sections` `joinTwins` moves older copies onto their twin's components |
33
- | Layout locale variants | A Global Layout Section can have a variant per locale (`navigation--fr`). A page renders the variant whose locale is its route's first segment, else the base |
34
- | `locale-unvarianted` | A `get_binding_report` unmatched reason: the route resolved in a locale, the Section has no variant for it, and the base still renders there. Named, never written |
35
-
36
- Import derives deeper locale routes (`/fr/about`) as pages in a URL folder (`fr`), each recording
37
- the language its `<html lang>` declares, and the Content sidebar files them under Pages › <language>.
38
- A locale prefix is never a collection, but a collection's localized posts (`/fr/blog/post-1`, its
39
- `<html lang>` agreeing) are entries of that collection with `locale` set, `translationOf` the post
40
- their hreflang alternate (else their slug) names, slug `<slug>-<locale>`, served at
41
- `/<locale>/<collection>/<slug>`; the project's locales gain each language found. An edit to `/about` never reaches `/fr/about`; edit each page or entry.
42
-
43
- ## 2. Never model
44
-
45
- - **No `language`, `locale` or `lang` field** on a model, page or component, and no
46
- `translations` reference field between entries. The language is the native `locale`, the link
47
- is the native `translationOf`, and delivery derives `translations` from them.
48
- - **No hreflang, alternate-URL or canonical-per-language field.** SEO is native
49
- ([seo.md](seo.md)), and the native columns have no locale slot.
50
- - **No per-language copy of a field** (`titleFr`, `titleEn`). It doubles every edit and leaves
51
- the page's language implicit in a key name.
52
-
53
- ## 3. Where hreflang comes from
54
-
55
- The frontend writes the `<head>` links. For each entry, render `<link rel="alternate" hreflang="…">`
56
- for itself and for every row in its delivered `translations` (the locale is the tag, the slug
57
- builds the URL on the site's own route pattern), plus `x-default` for the default-language row.
58
- Hosted `<head>` output emits none.
59
-
60
- The hosted sitemap does emit them. Every listed page or entry in a translation group (the source
61
- plus its `translationOf` rows) carries `<xhtml:link rel="alternate" hreflang>` for each listed
62
- sibling, itself included, plus `x-default` for the source. A row with no `locale` takes the
63
- project's `localeConfig.defaultLanguage`; with neither, it gets no link of its own. Unpublished
64
- and noindex siblings are not listed, so they get no link either. Check the live `sitemap.xml`.
65
-
66
- ## 4. Workflow
67
-
68
- For a site modelled in the CMS:
69
-
70
- 1. Confirm the owner has set the project's locales; you cannot.
71
- 2. Per source entry, `translate_entry` into each locale (or create the row by hand), then
72
- translate the copied rich text with `write_content` and write it with `update_content_entry`.
73
- 3. Review each draft with the user, publish it (`publish_entries` takes many), and check the
74
- source's delivered `translations` lists it.
75
- 4. Emit hreflang from `translations` in the frontend (§3) and fetch each language's URL.
76
-
77
- For a multilingual import:
78
-
79
- 1. List the prefixes the build serves and confirm each page's `<html lang>` matches its prefix.
80
- 2. Componentize the default language first; the plan then places each locale copy on its twin.
81
- 3. Read `get_binding_report` on each prefixed route: `locale-unvarianted` rows are the Layout
82
- Sections still showing the default language there. `update_layout` takes no locale, so name
83
- those Sections to the user rather than writing translated copy onto the base.
84
- 4. Translate with `write_content`, apply, publish, and fetch each prefixed URL.
85
- 5. Report the routes that stay build-only (the editor setup's `pages` note names any) with their count.
1
+ ---
2
+ title: "Localization"
3
+ description: "Document-per-locale translations (locale, translationOf, translate_entry, delivery translations), URL-prefixed locale copies of imported builds, Layout locale variants, locale-unvarianted, hreflang, and what never to model."
4
+ ---
5
+
6
+ # Localization
7
+
8
+ **Outcome.** A multilingual site keeps every language rendering, each language's copy is
9
+ editable, and nothing invents a locale model the platform does not read.
10
+
11
+ Two things are called a "language" here, and they are separate:
12
+
13
+ - **A translation** is its own entry or page row, one per locale: `locale` is its BCP-47 tag (one
14
+ of the project's locales; null = the default language) and `translationOf` is the SOURCE row's
15
+ id. Each has its own slug, draft and publish state.
16
+ - **A locale copy of an imported build** is a URL prefix (`/fr`, `/pt-br`) proven by the page's own
17
+ `<html lang>`. It is how a converted site's languages arrive, before anyone models them.
18
+
19
+ ## 1. What exists
20
+
21
+ | Piece | How it works |
22
+ |---|---|
23
+ | The project's locales | `localeConfig.locales`: the owner's list (project settings) plus every page language an import finds. A `locale` outside it is refused with 400; an empty list means a single-language project. Tags are stored canonical (`zh-CN` → `zh-cn`) |
24
+ | A page's language | `list_pages` / `get_page` return `language` (its `locale`, else the `<html lang>` its import declared; null = the default) and `path`. Set it with `create_page` / `update_page` `locale` |
25
+ | Address a page | By id or `path` (`/de/construction/safety`). A slug repeats across language folders, so a bare slug works only when one page has it; otherwise the tool refuses with each match's id, path and language. Never guess one |
26
+ | Translate an entry with AI | `translate_entry` { entryId, targetLocale, slug?, fields? } creates the translation as a DRAFT with `locale` and `translationOf` set (AI credits: 402/403 when out or not on the plan). Text and longtext are translated; rich text and documents are copied for you to translate; `meta` lists which. `409 locale_conflict`: that translation exists, so edit it instead |
27
+ | Create one by hand | `create_content_entry` or `create_page` with `locale` and `translationOf` = the source's id, never another translation's. One row per locale per source (a second is `409 locale_conflict`); `update_content_entry` / `update_page` change either, null clears |
28
+ | Find them | `list_content_entries` `where: { locale: 'fr' }`; default-language rows have a null `locale`, so they match `{ locale: { ne: 'fr' } }`, never `eq` |
29
+ | Deliver them | Every delivered entry carries `locale`, `translationOf` and `translations`: `[{ id, locale, slug }]` of its group's other PUBLISHED rows (drafts only when previewing). A delivered page carries `locale` and `translationOf` |
30
+ | Translate one string | `write_content` with `action: "translate"` and `targetLang` returns the text; apply it with `set_page_content` or `update_content_entry` |
31
+ | Locale copy of an imported page | A page whose route opens with a language prefix its own `<html lang>` confirms (`/fr` on a French page). The prefix is proven by the page's language, never by the segment's shape: `/de` is a locale only on a German page |
32
+ | Componentizing a locale copy | `get_componentize_plan` places `/fr` on its default-locale twin's components, band by band, when each pair holds the same prop types in the same order; a mismatched band stays its own section. `componentize_sections` `joinTwins` moves older copies onto their twin's components |
33
+ | Layout locale variants | A Global Layout Section can have a variant per locale (`navigation--fr`). A page renders the variant whose locale is its route's first segment, else the base |
34
+ | `locale-unvarianted` | A `get_binding_report` unmatched reason: the route resolved in a locale, the Section has no variant for it, and the base still renders there. Named, never written |
35
+
36
+ Import derives deeper locale routes (`/fr/about`) as pages in a URL folder (`fr`), each recording
37
+ the language its `<html lang>` declares, and the Content sidebar files them under Pages › <language>.
38
+ A locale prefix is never a collection, but a collection's localized posts (`/fr/blog/post-1`, its
39
+ `<html lang>` agreeing) are entries of that collection with `locale` set, `translationOf` the post
40
+ their hreflang alternate (else their slug) names, slug `<slug>-<locale>`, served at
41
+ `/<locale>/<collection>/<slug>`; the project's locales gain each language found. An edit to `/about` never reaches `/fr/about`; edit each page or entry.
42
+
43
+ ## 2. Never model
44
+
45
+ - **No `language`, `locale` or `lang` field** on a model, page or component, and no
46
+ `translations` reference field between entries. The language is the native `locale`, the link
47
+ is the native `translationOf`, and delivery derives `translations` from them.
48
+ - **No hreflang, alternate-URL or canonical-per-language field.** SEO is native
49
+ ([seo.md](seo.md)), and the native columns have no locale slot.
50
+ - **No per-language copy of a field** (`titleFr`, `titleEn`). It doubles every edit and leaves
51
+ the page's language implicit in a key name.
52
+
53
+ ## 3. Where hreflang comes from
54
+
55
+ The frontend writes the `<head>` links. For each entry, render `<link rel="alternate" hreflang="…">`
56
+ for itself and for every row in its delivered `translations` (the locale is the tag, the slug
57
+ builds the URL on the site's own route pattern), plus `x-default` for the default-language row.
58
+ Hosted `<head>` output emits none.
59
+
60
+ The hosted sitemap does emit them. Every listed page or entry in a translation group (the source
61
+ plus its `translationOf` rows) carries `<xhtml:link rel="alternate" hreflang>` for each listed
62
+ sibling, itself included, plus `x-default` for the source. A row with no `locale` takes the
63
+ project's `localeConfig.defaultLanguage`; with neither, it gets no link of its own. Unpublished
64
+ and noindex siblings are not listed, so they get no link either. Check the live `sitemap.xml`.
65
+
66
+ ## 4. Workflow
67
+
68
+ For a site modelled in the CMS:
69
+
70
+ 1. Confirm the owner has set the project's locales; you cannot.
71
+ 2. Per source entry, `translate_entry` into each locale (or create the row by hand), then
72
+ translate the copied rich text with `write_content` and write it with `update_content_entry`.
73
+ 3. Review each draft with the user, publish it (`publish_entries` takes many), and check the
74
+ source's delivered `translations` lists it.
75
+ 4. Emit hreflang from `translations` in the frontend (§3) and fetch each language's URL.
76
+
77
+ For a multilingual import:
78
+
79
+ 1. List the prefixes the build serves and confirm each page's `<html lang>` matches its prefix.
80
+ 2. Componentize the default language first; the plan then places each locale copy on its twin.
81
+ 3. Read `get_binding_report` on each prefixed route: `locale-unvarianted` rows are the Layout
82
+ Sections still showing the default language there. `update_layout` takes no locale, so name
83
+ those Sections to the user rather than writing translated copy onto the base.
84
+ 4. Translate with `write_content`, apply, publish, and fetch each prefixed URL.
85
+ 5. Report the routes that stay build-only (the editor setup's `pages` note names any) with their count.
@@ -1,181 +1,181 @@
1
- ---
2
- title: "Brand kit and media"
3
- description: "Brand tokens, fonts, logo kit, image sizing and focal point, SVG and video rules, and the media host."
4
- ---
5
-
6
- # Brand kit, section styles and media
7
-
8
- **Outcome.** An imported site keeps its appearance, and editors change the intended brand and
9
- media controls through one coherent system instead of scattered CSS values.
10
-
11
- ## Audit sources and the rendered design
12
-
13
- Inspect CSS variables, the Tailwind or theme config, design-token files, `@font-face`, layout
14
- components, media directories, responsive assets and computed styles on representative routes.
15
- Compare desktop and mobile screenshots. Record where each proposed token came from and how often
16
- it is used. One colour on one button is not a brand identity.
17
-
18
- Build a token inventory: role, source value, proposed BetterCMS value, affected components,
19
- confidence, approval status. Cover colours, type families, weights and scale, spacing, container
20
- widths, radius, borders, elevation, motion and breakpoints. Group colours into semantic roles
21
- (primary, accent, background, surface, text, muted, status colours). Keep raw source values as
22
- evidence, never as every component's editable prop.
23
-
24
- ## What the brand kit is, and who writes it
25
-
26
- - `get_project` returns `brandKit`: colours (`primary`, `accent`, `background`, `surface`,
27
- `text`, `muted`), `typography` (`headingFont`, `bodyFont`), `radius` (`sharp`/`rounded`/`pill`),
28
- and optional `typeScale`, `spacing`, `elevation`, `motion` and gradients. A project that never
29
- saved one still returns the default kit.
30
- - **Colours and typography are edited in the dashboard**; no MCP tool writes them. Prepare a
31
- precise proposal and map the source CSS to it; never say you changed them.
32
- - **The logo mark and favicon** are written with `set_brand_assets`, by the id of a media asset
33
- of this project (upload it first). A slot a person chose is refused unless you pass
34
- `replace: true`; ask before passing it.
35
- - **Gradients** are written with `set_brand_graphics`: a kind, an optional angle and 2-8 stops
36
- that name kit colour keys, never a hex or a CSS string.
37
- - Every brand write is a version a person can restore in the dashboard.
38
- - The hosted renderer emits the kit as `--bcms-*` and `--brand-*` CSS custom properties, which
39
- reach hosted pages only. A self-hosted site owns its tokens: bind its own presentation
40
- variables to the approved kit, and declare its presentation in `bcms-presentation.json` at the
41
- root of the deployed artifact.
42
- - `update_project` `presentation` themes the platform's own renderer with the same closed knobs
43
- as a release's `bcms-presentation.json`. It does not restyle a deployed build.
44
-
45
- ## Section styles
46
-
47
- Section style props are closed tokens, not free CSS: space is `none|xs|sm|md|lg|xl`, colour is
48
- `none|white|black|muted|mutedBg|primary|accent`. Never put a hex value from the brand into block
49
- props: a token follows a brand change, a copied hex does not. Style inspector properties:
50
- `CM-27`. Expose a curated choice (for example
51
- `Surface: Light / Dark / Brand`); never ask an editor for a hex, a class name or a font path.
52
- Say whether a change is brand-wide or one instance, and test that a token change keeps contrast
53
- and layout intact in every variant. Offer only controls the renderer actually applies; a missing
54
- style binding is a code or platform task, not a dashboard promise.
55
-
56
- ## Typography and fonts
57
-
58
- Inventory family stacks, weights, styles, variable axes, local files, remote providers and
59
- where each is used. Match heading and body roles to the rendered code; do not swap a display
60
- face for the body face because it appears less often. Keep fallbacks and the loading strategy.
61
- Self-host a font file only when its licence allows it; otherwise keep the provider reference and
62
- list the dependency. Record licence evidence or "licence unknown".
63
-
64
- Keep semantic HTML apart from visual size: an H2 may use a display token without becoming an
65
- H1. Expose typography variants only where the design allows a real choice.
66
-
67
- ## Logos, images, icons and video
68
-
69
- - **Logo kit.** Roles: primary, reverse, dark, compact mark, wordmark, favicon, where the source
70
- proves them; mark the rest unclassified. Keep vector originals and transparency. The navigation
71
- logo is a stored image on the layout; replace it there ([components.md](components.md) §4),
72
- not in each page.
73
- - **Images.** A stored image value is an object (`url`, `altText`, asset id), never a bare
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 (a publish keeps them too:
76
- [bindings.md](bindings.md) Images). Decorative images render an
77
- empty `alt`; informative ones need text that fits the context. `update_media` with
78
- `decorative: true` marks a library asset decorative, so the audit stops asking for its alt text.
79
- - **Request the size you render.** Mint resize URLs with `@bettercms-ai/image-url`
80
- (`urlFor = imageUrlBuilder({ baseUrl: mediaUrl })`, then `urlFor(asset).size(800, 450).url()`;
81
- also `width`, `format`, `quality`, `fit`, `dpr` 1–3) and a
82
- `srcset` of the widths the layout actually uses. Why: the original upload is often several
83
- thousand pixels wide, and an unresized `url` ships all of it.
84
- - **Framing lives on the value, never in the model.** An asset carries one focal point (0–1
85
- fractions), set with `update_media` `focalX`/`focalY` (null resets to the centre) or in the
86
- dashboard; a URL with both dimensions and `fit` cover crops around it automatically. One
87
- PLACEMENT can differ: the image value itself may carry `focal: { x, y }` and
88
- `crop: { top, right, bottom, left }` (0–1, cut from each edge; the crop must leave a visible
89
- window). The value's focal wins over the asset's; absent keys fall back to the asset. Draw a
90
- value with `imageValueUrl(value, { w, h })` and `imageValueObjectPosition(value)` from
91
- `@bettercms-ai/image-url`, never `.url` alone, or the crop is lost. The dashboard's asset Crop
92
- rewrites the stored bytes for every usage. Never add focal or crop FIELDS to a model: they
93
- belong on the image value.
94
- - **Image block width.** An image block's `props.width` is pixels, and every lane caps it at
95
- the column (`max-width:100%`, height auto). Leave it unset to fill the column.
96
- - **Rich-text images.** A `builtin:image` block takes `align` (`full` | `center` | `left` |
97
- `right`) and `size` (`small` | `medium` | `large` | `full`) in its fields. Both render on every
98
- lane, with inline widths, so no site CSS is needed.
99
- - **Image facts ride on the delivered value.** Delivery adds `width`, `height`, `thumbHash` (a
100
- ThumbHash placeholder) and `focalX`/`focalY` to each image value, read at request time and
101
- omitted while unknown (object values only: a bare URL string stays a string). Reserve the box from `width`/`height`, decode `thumbHash` for a blur-up,
102
- and fall back to a tiny variant (`width(24)`) when it is absent.
103
- - **Generate or edit an image.** `generate_image` { prompt, width?, height?, altText } adds an AI
104
- image to the Media Library; `transform_image` { mediaId, prompt } saves an edited copy as a NEW
105
- asset and keeps the original. Each costs 5 AI credits (free with the workspace's own Together
106
- key). Always set `altText`, and use them only where the user wants generated imagery: never to
107
- stand in for a real product, person or place.
108
- - **SVG.** Every SVG upload is sanitised on the way in, and SVGs are served as vectors (no
109
- raster variants). Keep safe icons as SVG and expose icon choice, size and colour token.
110
- Functional icons need an accessible label or adjacent text. Editing SVG paths is not a CMS
111
- feature; the text inside an SVG is not a bindable field ([bindings.md](bindings.md) §2).
112
- - **Video.** Inventory format, host, poster, captions, autoplay/mute/loop and cost. Do not copy
113
- every video into the media library by default; keep a streaming provider reference where the
114
- site depends on one.
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, or on a section whose copy sits directly in it. A section that keeps its copy in one wrapper is never listed: the picture is the section's field and the copy the wrapper's | 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
-
153
- ## Media host
154
-
155
- Use `mediaBaseUrl` from `get_project`: media is served from the CDN origin or the API origin of
156
- the backend that owns the asset ids. `set_media_delivery` is the owner's decision, asked every
157
- time. If a stored URL 404s, find the owning asset and report it; never re-save content to "move"
158
- assets. How templates and resize URLs use it: [frameworks.md](frameworks.md) Media host.
159
-
160
- ## Presentation-mode UX
161
-
162
- Show only meaningful controls, and state the scope before a change:
163
-
164
- ```text
165
- Brand [sitewide, reviewed]
166
- Logos · Colors · Typography · Spacing / Radius / Motion
167
-
168
- Selected hero image [this instance]
169
- Asset · Alt text / Decorative · Caption
170
- ```
171
-
172
- Keep provenance and licence status visible to developers, away from everyday editing.
173
-
174
- ## Acceptance
175
-
176
- - Every local asset resolves from the project's media store; external dependencies are listed.
177
- - Logo roles and fonts carry evidence and licence status.
178
- - Colour, type and spacing changes go through tokens and keep contrast and responsive layout.
179
- - Editing a usage's alt text or caption changes the right rendered image.
180
- - Icons work in every intended theme and state.
181
- - Baseline and changed screenshots are compared; material differences are explained.
1
+ ---
2
+ title: "Brand kit and media"
3
+ description: "Brand tokens, fonts, logo kit, image sizing and focal point, SVG and video rules, and the media host."
4
+ ---
5
+
6
+ # Brand kit, section styles and media
7
+
8
+ **Outcome.** An imported site keeps its appearance, and editors change the intended brand and
9
+ media controls through one coherent system instead of scattered CSS values.
10
+
11
+ ## Audit sources and the rendered design
12
+
13
+ Inspect CSS variables, the Tailwind or theme config, design-token files, `@font-face`, layout
14
+ components, media directories, responsive assets and computed styles on representative routes.
15
+ Compare desktop and mobile screenshots. Record where each proposed token came from and how often
16
+ it is used. One colour on one button is not a brand identity.
17
+
18
+ Build a token inventory: role, source value, proposed BetterCMS value, affected components,
19
+ confidence, approval status. Cover colours, type families, weights and scale, spacing, container
20
+ widths, radius, borders, elevation, motion and breakpoints. Group colours into semantic roles
21
+ (primary, accent, background, surface, text, muted, status colours). Keep raw source values as
22
+ evidence, never as every component's editable prop.
23
+
24
+ ## What the brand kit is, and who writes it
25
+
26
+ - `get_project` returns `brandKit`: colours (`primary`, `accent`, `background`, `surface`,
27
+ `text`, `muted`), `typography` (`headingFont`, `bodyFont`), `radius` (`sharp`/`rounded`/`pill`),
28
+ and optional `typeScale`, `spacing`, `elevation`, `motion` and gradients. A project that never
29
+ saved one still returns the default kit.
30
+ - **Colours and typography are edited in the dashboard**; no MCP tool writes them. Prepare a
31
+ precise proposal and map the source CSS to it; never say you changed them.
32
+ - **The logo mark and favicon** are written with `set_brand_assets`, by the id of a media asset
33
+ of this project (upload it first). A slot a person chose is refused unless you pass
34
+ `replace: true`; ask before passing it.
35
+ - **Gradients** are written with `set_brand_graphics`: a kind, an optional angle and 2-8 stops
36
+ that name kit colour keys, never a hex or a CSS string.
37
+ - Every brand write is a version a person can restore in the dashboard.
38
+ - The hosted renderer emits the kit as `--bcms-*` and `--brand-*` CSS custom properties, which
39
+ reach hosted pages only. A self-hosted site owns its tokens: bind its own presentation
40
+ variables to the approved kit, and declare its presentation in `bcms-presentation.json` at the
41
+ root of the deployed artifact.
42
+ - `update_project` `presentation` themes the platform's own renderer with the same closed knobs
43
+ as a release's `bcms-presentation.json`. It does not restyle a deployed build.
44
+
45
+ ## Section styles
46
+
47
+ Section style props are closed tokens, not free CSS: space is `none|xs|sm|md|lg|xl`, colour is
48
+ `none|white|black|muted|mutedBg|primary|accent`. Never put a hex value from the brand into block
49
+ props: a token follows a brand change, a copied hex does not. Style inspector properties:
50
+ `CM-27`. Expose a curated choice (for example
51
+ `Surface: Light / Dark / Brand`); never ask an editor for a hex, a class name or a font path.
52
+ Say whether a change is brand-wide or one instance, and test that a token change keeps contrast
53
+ and layout intact in every variant. Offer only controls the renderer actually applies; a missing
54
+ style binding is a code or platform task, not a dashboard promise.
55
+
56
+ ## Typography and fonts
57
+
58
+ Inventory family stacks, weights, styles, variable axes, local files, remote providers and
59
+ where each is used. Match heading and body roles to the rendered code; do not swap a display
60
+ face for the body face because it appears less often. Keep fallbacks and the loading strategy.
61
+ Self-host a font file only when its licence allows it; otherwise keep the provider reference and
62
+ list the dependency. Record licence evidence or "licence unknown".
63
+
64
+ Keep semantic HTML apart from visual size: an H2 may use a display token without becoming an
65
+ H1. Expose typography variants only where the design allows a real choice.
66
+
67
+ ## Logos, images, icons and video
68
+
69
+ - **Logo kit.** Roles: primary, reverse, dark, compact mark, wordmark, favicon, where the source
70
+ proves them; mark the rest unclassified. Keep vector originals and transparency. The navigation
71
+ logo is a stored image on the layout; replace it there ([components.md](components.md) §4),
72
+ not in each page.
73
+ - **Images.** A stored image value is an object (`url`, `altText`, asset id), never a bare
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 (a publish keeps them too:
76
+ [bindings.md](bindings.md) Images). Decorative images render an
77
+ empty `alt`; informative ones need text that fits the context. `update_media` with
78
+ `decorative: true` marks a library asset decorative, so the audit stops asking for its alt text.
79
+ - **Request the size you render.** Mint resize URLs with `@bettercms-ai/image-url`
80
+ (`urlFor = imageUrlBuilder({ baseUrl: mediaUrl })`, then `urlFor(asset).size(800, 450).url()`;
81
+ also `width`, `format`, `quality`, `fit`, `dpr` 1–3) and a
82
+ `srcset` of the widths the layout actually uses. Why: the original upload is often several
83
+ thousand pixels wide, and an unresized `url` ships all of it.
84
+ - **Framing lives on the value, never in the model.** An asset carries one focal point (0–1
85
+ fractions), set with `update_media` `focalX`/`focalY` (null resets to the centre) or in the
86
+ dashboard; a URL with both dimensions and `fit` cover crops around it automatically. One
87
+ PLACEMENT can differ: the image value itself may carry `focal: { x, y }` and
88
+ `crop: { top, right, bottom, left }` (0–1, cut from each edge; the crop must leave a visible
89
+ window). The value's focal wins over the asset's; absent keys fall back to the asset. Draw a
90
+ value with `imageValueUrl(value, { w, h })` and `imageValueObjectPosition(value)` from
91
+ `@bettercms-ai/image-url`, never `.url` alone, or the crop is lost. The dashboard's asset Crop
92
+ rewrites the stored bytes for every usage. Never add focal or crop FIELDS to a model: they
93
+ belong on the image value.
94
+ - **Image block width.** An image block's `props.width` is pixels, and every lane caps it at
95
+ the column (`max-width:100%`, height auto). Leave it unset to fill the column.
96
+ - **Rich-text images.** A `builtin:image` block takes `align` (`full` | `center` | `left` |
97
+ `right`) and `size` (`small` | `medium` | `large` | `full`) in its fields. Both render on every
98
+ lane, with inline widths, so no site CSS is needed.
99
+ - **Image facts ride on the delivered value.** Delivery adds `width`, `height`, `thumbHash` (a
100
+ ThumbHash placeholder) and `focalX`/`focalY` to each image value, read at request time and
101
+ omitted while unknown (object values only: a bare URL string stays a string). Reserve the box from `width`/`height`, decode `thumbHash` for a blur-up,
102
+ and fall back to a tiny variant (`width(24)`) when it is absent.
103
+ - **Generate or edit an image.** `generate_image` { prompt, width?, height?, altText } adds an AI
104
+ image to the Media Library; `transform_image` { mediaId, prompt } saves an edited copy as a NEW
105
+ asset and keeps the original. Each costs 5 AI credits (free with the workspace's own Together
106
+ key). Always set `altText`, and use them only where the user wants generated imagery: never to
107
+ stand in for a real product, person or place.
108
+ - **SVG.** Every SVG upload is sanitised on the way in, and SVGs are served as vectors (no
109
+ raster variants). Keep safe icons as SVG and expose icon choice, size and colour token.
110
+ Functional icons need an accessible label or adjacent text. Editing SVG paths is not a CMS
111
+ feature; the text inside an SVG is not a bindable field ([bindings.md](bindings.md) §2).
112
+ - **Video.** Inventory format, host, poster, captions, autoplay/mute/loop and cost. Do not copy
113
+ every video into the media library by default; keep a streaming provider reference where the
114
+ site depends on one.
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, or on a section whose copy sits directly in it. A section that keeps its copy in one wrapper is never listed: the picture is the section's field and the copy the wrapper's | 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
+
153
+ ## Media host
154
+
155
+ Use `mediaBaseUrl` from `get_project`: media is served from the CDN origin or the API origin of
156
+ the backend that owns the asset ids. `set_media_delivery` is the owner's decision, asked every
157
+ time. If a stored URL 404s, find the owning asset and report it; never re-save content to "move"
158
+ assets. How templates and resize URLs use it: [frameworks.md](frameworks.md) Media host.
159
+
160
+ ## Presentation-mode UX
161
+
162
+ Show only meaningful controls, and state the scope before a change:
163
+
164
+ ```text
165
+ Brand [sitewide, reviewed]
166
+ Logos · Colors · Typography · Spacing / Radius / Motion
167
+
168
+ Selected hero image [this instance]
169
+ Asset · Alt text / Decorative · Caption
170
+ ```
171
+
172
+ Keep provenance and licence status visible to developers, away from everyday editing.
173
+
174
+ ## Acceptance
175
+
176
+ - Every local asset resolves from the project's media store; external dependencies are listed.
177
+ - Logo roles and fonts carry evidence and licence status.
178
+ - Colour, type and spacing changes go through tokens and keep contrast and responsive layout.
179
+ - Editing a usage's alt text or caption changes the right rendered image.
180
+ - Icons work in every intended theme and state.
181
+ - Baseline and changed screenshots are compared; material differences are explained.