@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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "SEO and AEO"
3
- description: "Native page and entry meta, Markdown twins, route audit, structured data, sitemap and robots, AEO, validators and a generated OG image route."
3
+ description: "Native page and entry meta, carrying over a source site's meta, redirects, Markdown twins, route audit, structured data by content type, sitemap, robots and AI crawlers, AEO, validators, a generated OG image route and the SEO/AEO gate."
4
4
  ---
5
5
 
6
6
  # SEO and AEO
@@ -12,16 +12,27 @@ no one can guarantee.
12
12
  Doctrine: SEO is native and never modelled (§5b of `bettercms://playbook/schema`); flow:
13
13
  `seo_optimize` prompt.
14
14
 
15
+ ## Contents
16
+
17
+ - 1. Where each value lives (carry over the source's meta, Markdown twins)
18
+ - 2. Redirects and URLs
19
+ - 3. Route-level audit
20
+ - 4. Structured data (content to schema.org type, rules)
21
+ - 5. Sitemap, robots, llms.txt (AI crawlers)
22
+ - 6. AEO and content quality
23
+ - 7. Publish and verify (validators, generated OG image)
24
+ - SEO and AEO gate
25
+
15
26
  ## 1. Where each value lives
16
27
 
17
28
  | Value | Writer | Notes |
18
29
  |---|---|---|
19
30
  | Page title, description, `schemaType`, `schema`, noindex, canonical, OG, Twitter | `update_page` | Merges into what is stored: an absent key keeps its value, `null` or `""` clears it |
20
- | Entry meta (title, description, noindex, canonical, OG, Twitter, schema) | `update_content_entry` with `meta` | Same merge rule |
31
+ | Entry meta (title, description, noindex, canonical, OG, Twitter, `schemaType`, `schema`) | `update_content_entry` with `meta` | Same merge rule |
21
32
  | Site defaults (`seoDefaults`: title, description, OG image, Twitter handle, `og`, `twitter`, `collectionSchemaTypes`), site JSON-LD (`siteSchema`), `robotsConfig`, `sitemapConfig`, `rssConfig`, `indexnowConfig`, `searchConfig` | `update_seo`, read with `get_seo` | Each key REPLACES its whole object, so read and send it complete. `collectionSchemaTypes` maps a collection's model id to its entries' schema.org type; a key the plan lacks answers 403 `FEATURE_LOCKED` |
22
33
  | Suggestions | `generate_seo_meta` | Returns a suggestion only; apply it with the writers above |
23
- | Audit | `list_seo_issues` | Computed live from published pages; nothing is stored |
24
- | Redirects | `list_redirects`, `create_redirect`, `update_redirect`, `delete_redirect` | 301, 302, 307 or 308; 301 when omitted |
34
+ | Audit | `list_seo_issues` | Computed live from published pages; nothing is stored. Also reports accessibility issues as `a11y.` codes (image alt text, heading order, generic link text, video transcripts), each naming the page and its block ids |
35
+ | Redirects | `list_redirects`, `create_redirect`, `import_redirects`, `update_redirect`, `delete_redirect` | 301, 302, 307 or 308; 301 when omitted. 410 takes no destination: the path answers Gone and leaves the sitemap. For a list of rules use `import_redirects` (rows, CSV, a Netlify `_redirects` file or a vercel.json) with `dryRun: true` first; each row comes back created, existing or skipped with its reason |
25
36
 
26
37
  Never create `seoTitle`-style fields on a model, page or component: the renderer, the sitemap,
27
38
  `llms.txt` and `list_seo_issues` read the native values only. Self-hosted sites own their
@@ -31,6 +42,30 @@ There is no hreflang field: translations are native rows (`locale`, `translation
31
42
  sitemap emits the hreflang alternates from them; a frontend emits the `<head>` links from each
32
43
  entry's delivered `translations` ([localization.md](localization.md)).
33
44
 
45
+ ### Carry over the source's meta
46
+
47
+ On an import or conversion, read every source page's `<head>` before writing. Copy published
48
+ values as they are; rewriting copy is a separate, reported recommendation.
49
+
50
+ | Source | Native writer and key | Rule |
51
+ |---|---|---|
52
+ | `<title>` | `metaTitle` (`update_page`; `meta.metaTitle` on an entry) | Copy verbatim. Report one over 60 characters; `list_seo_issues` flags missing, over 60 and duplicates |
53
+ | `<meta name="description">` | `metaDescription` | Copy verbatim. Aim for 120–160 characters; the audit flags only a missing one and one over 160. When the source has none, take a `generate_seo_meta` suggestion and list it for review |
54
+ | `og:title`, `og:description`, `og:image`, `og:type` | `og.title`, `og.description`, `og.image`, `og.type` | Write a key only when it differs from the meta title, the description or the site default. Upload the image first and store its media URL |
55
+ | `twitter:card`, `twitter:title`, `twitter:description`, `twitter:image` | `twitter.card`, `twitter.title`, `twitter.description`, `twitter.image` | Same rule as OG |
56
+ | `twitter:site` | `update_seo` `seoDefaults.twitterHandle` | Once per site |
57
+ | A site-wide share image | `update_seo` `seoDefaults.ogImage` | Then pages inherit it and `og.image` stays empty |
58
+ | Canonical pointing at the page itself | nothing | A hosted page gets its own canonical |
59
+ | Canonical pointing at another URL | `canonical` | Keep it: someone consolidated those URLs on purpose |
60
+ | `<meta name="robots" content="noindex">` | `noindex: true` | Also drops the route from the sitemap, `llms.txt`, its Markdown twin and site search |
61
+ | `nofollow`, `noarchive`, `max-snippet` and other robots directives | nothing | No native key stores them. Report each page that used one |
62
+ | `<link rel="alternate" hreflang>` | nothing on the page; one row per language with `locale` and `translationOf` | Pair the languages by URL and structure, never by matching text ([localization.md](localization.md)) |
63
+ | JSON-LD on a page | nothing | Read it as evidence and compare it with what BetterCMS emits (§4) |
64
+ | Organization JSON-LD on the home page | `update_seo` `siteSchema` | Keep name, URL, logo and `sameAs`; drop any value the site does not show |
65
+ | robots.txt | `update_seo` `robotsConfig` | Stay on `auto` unless the source has rules worth keeping, then `custom` with that text (§5) |
66
+ | sitemap.xml | `update_seo` `sitemapConfig` | Stay on `auto`. URLs the CMS does not own (a docs path, a static app) go in `extraUrls` |
67
+ | RSS or Atom feed | `update_seo` `rssConfig` `{ enabled: true }` | Item links come from the collection's `urlPattern` |
68
+
34
69
  ### Markdown twins
35
70
 
36
71
  Every published page and entry is also served as Markdown for answer engines, at its own URL plus
@@ -44,7 +79,33 @@ Every published page and entry is also served as Markdown for answer engines, at
44
79
  - `llms.txt`, `llms-full.txt` and which pages serve a twin are switched there too. Those writes
45
80
  need the SEO permission, and no MCP tool writes them.
46
81
 
47
- ## 2. Route-level audit
82
+ ## 2. Redirects and URLs
83
+
84
+ Keep every URL that ranks or is linked to. Change a path only when BetterCMS cannot express it.
85
+
86
+ 1. Build a URL map first: each source URL (from the sitemap and a crawl) beside its new path.
87
+ 2. Every row whose path changes gets one `create_redirect` (301) aimed at the FINAL URL, never at
88
+ an intermediate one. Chains are collapsed for you; a loop answers 422 and a source path that
89
+ already has a rule answers 409.
90
+ 3. Import the source's own rules as well: a `_redirects` file, `vercel.json`, `netlify.toml`,
91
+ `.htaccess` or a redirect plugin's export.
92
+ 4. Never send a batch of removed pages to the home page. Redirect each to its closest equivalent,
93
+ or retire it with a 410.
94
+ 5. 302 or 307 is for a temporary move, 308 when the request method must survive.
95
+ 6. A page gone for good with no equivalent takes `redirectType: "410"` and no destination. List
96
+ those in the report.
97
+
98
+ | Case | Do |
99
+ |---|---|
100
+ | Slug shape | Lowercase letters, digits and hyphens. Strip `.html`, a trailing slash and capitals, each with a 301. Keep the source's words: a slug is not copy to improve |
101
+ | Collection URLs | Set `urlPattern` (`/blog/:slug`): a fixed prefix and `:slug` exactly once |
102
+ | Nested (`/:category/:slug`), dated (`/2024/05/post`) or query-string (`?p=123`) URLs | The pattern cannot say these. Map each to the flat pattern and add one 301 per old URL |
103
+ | Two sources want one slug | The one with more links or traffic keeps it. Give the other a suffix from its own words (`-guide`, the city), add its 301 and report it. A taken model slug answers 409 (code slug_taken) |
104
+ | A page or entry slug changed in BetterCMS | The 301 is written by the same save (an entry needs its collection's `urlPattern`). Do not add a second one |
105
+ | A URL folder renamed or moved | 301s are written for every page under it. Do not add them again |
106
+ | A closed job, a past event, a sold-out product | Keep the URL and mark the item closed in its content. Do not delete it |
107
+
108
+ ## 3. Route-level audit
48
109
 
49
110
  Read the route inventory, page and entry records, site defaults, the rendered `<head>`, robots,
50
111
  sitemap, structured data and redirects. For each indexable route check:
@@ -58,18 +119,81 @@ sitemap, structured data and redirects. For each indexable route check:
58
119
 
59
120
  A CMS field holding the right title does not prove the public route emits it. Fetch the HTML.
60
121
 
61
- ## 3. Structured data
122
+ ## 4. Structured data
123
+
124
+ `list_seo_issues` reports a pasted `schema` that lacks its type's required properties as
125
+ `schema.invalid`, naming the type and what is missing.
126
+
127
+ Choose in this order, and stop at the first that expresses the page:
128
+
129
+ 1. **Automatic.** With no `schemaType` and no `schema`, JSON-LD is built from the content at
130
+ render time and follows every edit: WebSite on the home page (plus Organization when the site
131
+ has a name and a logo, or `siteSchema` when one is stored), Blog on a list of posts,
132
+ CollectionPage on any other list, BlogPosting on a post entry, Product on an entry of a
133
+ collection with a price or SKU, WebPage everywhere else. FAQPage is never automatic. A hosted
134
+ page or entry also gets a BreadcrumbList beside its main type: Home, each ancestor path that
135
+ is itself a published page (a folder with no page is skipped), then the document.
136
+ 2. **`schemaType`.** One of nineteen: WebPage, AboutPage, ContactPage, CollectionPage, Blog,
137
+ BlogPosting, Article, Product, FAQPage, Event, Organization, LocalBusiness, JobPosting, Course,
138
+ Recipe, HowTo, Service, Person, VideoObject. Its properties are filled from the document's own
139
+ fields by key (a job's `company`, a recipe's `ingredients`), and a property the content lacks
140
+ is left out. A type missing what it cannot do without (a JobPosting with no company or posting
141
+ date, a Recipe with no image, a HowTo with no steps) is not emitted: the document keeps its
142
+ automatic type until the field is filled. Clear it with `"auto"`
143
+ on `update_page` or `null` in an entry's `meta`. For a whole collection, set
144
+ `seoDefaults.collectionSchemaTypes` once instead of typing every entry.
145
+ 3. **Pasted `schema`.** Only when neither of the above can express the type AND every value in it
146
+ is visible on the page. It wins over both and does not follow content edits, so it goes stale.
147
+
148
+ JSON-LD found on the source site is evidence, not content. Use it to fill fields (event dates,
149
+ opening hours, prices) and to check that the emitted type and key values match. Do not paste it
150
+ back. Never add a second copy through custom head code.
151
+
152
+ ### Content to schema.org type
153
+
154
+ | Source content | Type | How in BetterCMS |
155
+ |---|---|---|
156
+ | The whole site | Organization, WebSite | Automatic on the home page; `siteSchema` to add `sameAs` or replace it |
157
+ | Blog or journal post | BlogPosting, or Article | Automatic for a collection with a body that is named for posts or has a date and an author; else `collectionSchemaTypes` |
158
+ | List of posts, any other list | Blog, CollectionPage | Automatic |
159
+ | Product | Product with an Offer | Automatic when the model has a price or SKU; else `schemaType` |
160
+ | FAQ: visible questions with answers | FAQPage | `schemaType: "FAQPage"`. It reads the first repeater whose rows hold a question and an answer |
161
+ | Event | Event | `schemaType` (name, start, end, location read from the fields) |
162
+ | Company, store, restaurant, office | Organization, LocalBusiness | `schemaType`; a subtype such as Restaurant, a `PostalAddress`, geo or opening hours need pasted `schema` |
163
+ | About or contact page | AboutPage, ContactPage | `schemaType` |
164
+ | Breadcrumb trail | BreadcrumbList | Automatic on hosted pages (ancestors that have a page). Paste `schema` only to match a trail the page shows differently |
165
+ | News, recipe, job, video, course, podcast episode, how-to, author profile | NewsArticle, Recipe, JobPosting, VideoObject, Course, PodcastEpisode, HowTo, ProfilePage | Pasted `schema`, visible values only |
166
+ | Product with on-page reviews | AggregateRating, Review | Pasted `schema`, with the average and count the page displays |
167
+
168
+ ### Rules
169
+
170
+ - Every value must be on the page. No invented rating, review count, price, availability, salary,
171
+ author or date. What the page does not show is left out.
172
+ - One FAQPage per URL and one main entity per page. Two FAQ sections on one page still make one
173
+ FAQPage. An accordion of features is not a FAQ.
174
+ - `AggregateRating` needs a product or another reviewable subject. Never on the home page about
175
+ the business itself, and never typed by hand.
176
+ - Awards go in `award`; they are not certifications.
177
+ - Dates are ISO 8601 with a time zone offset.
178
+ - Cart, checkout, 404 and legal pages stay on Automatic (plain WebPage). There is no off switch;
179
+ never give them a richer type.
180
+ - Pasted JSON-LD is stored as given and is not checked for required properties. Check them
181
+ yourself before saving:
182
+
183
+ | Type | Required |
184
+ |---|---|
185
+ | Article, BlogPosting | `headline`, `image` |
186
+ | Product | `name` plus one of `offers`, `review`, `aggregateRating` |
187
+ | Recipe | `name`, `image` |
188
+ | FAQPage | `mainEntity`, each with `name` and `acceptedAnswer.text` |
189
+ | BreadcrumbList | `itemListElement`, each with `position` and `name` |
190
+ | Organization | `name`, `url` |
191
+ | LocalBusiness | `name`, `address` |
192
+ | Event | `name`, `startDate`, `location` |
62
193
 
63
- - With no `schemaType` or `schema`, a BetterCMS page gets automatic JSON-LD from its content
64
- (WebSite on the home page, Blog or CollectionPage on a collection list, WebPage otherwise).
65
- `schemaType` picks an explicit schema.org type; pasted `schema` wins over both.
66
- - Prefer a small, accurate graph. Add Article, Product, BreadcrumbList, FAQ or Service only when
67
- the page visibly supports it. Keep valid imported JSON-LD, and avoid duplicate nodes from
68
- native metadata plus custom head code.
69
- - Never fabricate ratings, reviews, authors, availability, prices or dates. Valid markup does not
70
- guarantee a rich result: [Google structured-data guidelines](https://developers.google.com/search/docs/appearance/structured-data/sd-policies).
194
+ Valid markup does not guarantee a rich result: [Google structured-data guidelines](https://developers.google.com/search/docs/appearance/structured-data/sd-policies).
71
195
 
72
- ## 4. Sitemap, robots, llms.txt
196
+ ## 5. Sitemap, robots, llms.txt
73
197
 
74
198
  - The hosted sitemap leaves out noindex pages, and the generated robots.txt points at the
75
199
  sitemap. Confirm drafts, private pages and noindex pages are absent from the live file.
@@ -77,7 +201,7 @@ A CMS field holding the right title does not prove the public route emits it. Fe
77
201
  sibling (its `locale`, else the project's `localeConfig.defaultLanguage`) plus `x-default` for
78
202
  the source row. A noindex or unpublished sibling is left out of the links as well.
79
203
  - `sitemapConfig` sets it `auto` or `custom`, the default changefreq and priority and `extraUrls`;
80
- `robotsConfig` is `auto` or `custom` text; `indexnowConfig` { enabled, excludePatterns } pings
204
+ `robotsConfig` is `auto` (with `disallow`: AI crawler names such as `GPTBot` or `ClaudeBot` to block) or `custom` text, which wins when set; `indexnowConfig` { enabled, excludePatterns } pings
81
205
  IndexNow on publish; `searchConfig` { enabled, placeholder, aiEnabled } is site search, which
82
206
  `search_content` queries (published content only).
83
207
  - `refresh_search_index` costs nothing: `action: 'indexnow'` submits `urls` (this site's own, up
@@ -86,9 +210,52 @@ A CMS field holding the right title does not prove the public route emits it. Fe
86
210
  - `llms.txt` lists one link per published item. Maintain it for its intended readers, but do not
87
211
  sell it as a ranking requirement: Google's [guidance for AI features](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide)
88
212
  says ordinary SEO fundamentals are the base.
213
+ - `set_site_file` stores a hand-written `llms.txt` or `llms-full.txt` (plain text, up to 5 MB).
214
+ Give `llms.txt` a one-line site summary taken from the home page's description or About copy.
89
215
  - Check each redirect's real status and target, and detect loops.
90
216
 
91
- ## 5. AEO and content quality
217
+ ### AI crawlers
218
+
219
+ `robotsConfig` is `{ mode: "auto" }` or `{ mode: "custom", custom: "<robots.txt text>" }` (up to
220
+ 10,000 characters). `robotsConfig.disallow` is the per-bot switch: a list of names from GPTBot,
221
+ OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-Web, anthropic-ai, PerplexityBot, Google-Extended,
222
+ CCBot, Bytespider, Applebot-Extended, Amazonbot and Meta-ExternalAgent. Each named bot is
223
+ disallowed from the whole site in auto mode. In custom mode the custom text is served as written
224
+ and `disallow` is not applied; any other bot or a path rule is a line of custom text.
225
+
226
+ - Carry the source's AI-bot rules over exactly. Changing the owner's crawler policy is not part of
227
+ a conversion. No rules in the source: stay on `auto` and say so in the report.
228
+ - Custom text replaces the generated file, so include the `Sitemap:` line yourself.
229
+ - Blocking `Google-Extended` or `Applebot-Extended` leaves Google Search and Siri untouched.
230
+ Blocking an answer or search bot (`OAI-SearchBot`, `ChatGPT-User`, `PerplexityBot`,
231
+ `DuckAssistBot`) removes the site from that product's answers. Say so before the owner asks
232
+ for it.
233
+ - Other agents to look for in a source file: `GPTBot`, `ClaudeBot`, `Claude-Web`, `anthropic-ai`,
234
+ `Meta-ExternalAgent`, `FacebookBot`, `Amazonbot`, `cohere-ai`, `CCBot`, `Bytespider`.
235
+
236
+ ```text
237
+ User-agent: *
238
+ Allow: /
239
+
240
+ User-agent: CCBot
241
+ Disallow: /
242
+
243
+ Sitemap: https://caldera.com/sitemap.xml
244
+ ```
245
+
246
+ ## 6. AEO and content quality
247
+
248
+ Set these up while converting, without asking:
249
+
250
+ | Do | How |
251
+ |---|---|
252
+ | Facts as text | Specs, prices, menus, opening hours and chart data become fields or a real table, never an image of them |
253
+ | Answer first | A real question on the page becomes a FAQ row: the question as asked, the answer in the first one or two sentences, detail after |
254
+ | One name per thing | A product, plan, person or brand is one entry that other content references, so a rename reaches every page and feed |
255
+ | Dated facts | A statistic or comparison keeps its date and source in fields beside the number |
256
+ | Authors as entries | An author collection with name, bio and photo, referenced from each post |
257
+ | Transcripts | Audio and video carry one, because crawlers do not watch video. It lives on the asset (`update_media` `transcript`); never invent one ([accessibility.md](accessibility.md)) |
258
+ | Twins and `llms.txt` | Leave them on unless the owner turned them off |
92
259
 
93
260
  Improve the page for people first: clear questions and answers where the topic warrants them,
94
261
  descriptive headings, first-hand evidence, dates and named authors where real, text alongside
@@ -98,7 +265,7 @@ snippets or unverifiable claims.
98
265
  Report concrete findings ("canonical points to a 404", "OG image missing", "FAQ schema does not
99
266
  match the visible answers"), never an untraceable score.
100
267
 
101
- ## 6. Publish and verify
268
+ ## 7. Publish and verify
102
269
 
103
270
  After an authorized change ([publishing.md](publishing.md)): inspect the rendered `<head>` and JSON-LD,
104
271
  route status, sitemap and robots output, and redirects. Use Search Console or similar only to
@@ -145,12 +312,45 @@ for a page), or from `generateMetadata`. Rules: 1200×630, an absolute URL, a 40
145
312
  missing slug, and strip stega from any preview string first ([bindings.md](bindings.md) §5).
146
313
  A hosted site gets its OG image from the native value; this route is for self-hosted Next only.
147
314
 
148
- ## Acceptance
315
+ ## SEO and AEO gate
316
+
317
+ Run before publishing or reporting done, then run `list_seo_issues` and `get_next_steps` and fix
318
+ what they list. The accessibility half of the gate is the check list in
319
+ [accessibility.md](accessibility.md).
320
+
321
+ **SEO**
322
+
323
+ - [ ] No model, page or component carries a meta, OG, canonical, noindex, hreflang or schema field.
324
+ - [ ] Every indexable page and entry has a `metaTitle` of at most 60 characters and a
325
+ `metaDescription` (aim 120–160, never over 160). No two titles match.
326
+ - [ ] Source canonicals that point elsewhere are kept; self canonicals are not stored.
327
+ - [ ] Source noindex pages are `noindex`, and so are search, cart, checkout and thank-you pages.
328
+ - [ ] Each language is its own row with `locale` and `translationOf`.
329
+ - [ ] Every changed URL has a 301 to its final URL; the source's redirects are imported; no chain,
330
+ no loop, no batch pointed at the home page.
331
+ - [ ] `seoDefaults`, `siteSchema` and `collectionSchemaTypes` are set.
332
+ - [ ] Sitemap and robots are `auto`, or `custom` carrying the source's rules.
333
+ - [ ] Native values and the fetched HTML agree on representative static and dynamic routes.
334
+
335
+ **Structured data**
336
+
337
+ - [ ] Each page is on Automatic unless a type is missing; then `schemaType`; pasted `schema` last.
338
+ - [ ] The emitted JSON-LD matches the source's for type and key values, and validates.
339
+ - [ ] Every value is visible on the page.
340
+ - [ ] One FAQPage per URL. No self-serving `AggregateRating`.
341
+ - [ ] Pasted JSON-LD parses and has its type's required properties.
342
+
343
+ **AEO**
344
+
345
+ - [ ] Specs, prices, hours, menus and chart data are text in fields or tables.
346
+ - [ ] Names are consistent and come from referenced entries.
347
+ - [ ] `llms.txt` has a site summary and Markdown twins are on.
348
+ - [ ] Videos and podcasts have a transcript, or the gap is reported.
349
+ - [ ] The source's AI-crawler policy is carried over unchanged.
350
+
351
+ **Report**
149
352
 
150
- - Native values and public HTML agree on representative static and dynamic routes.
151
- - Canonical, indexable routes are in the sitemap with no conflicting directives.
152
- - Structured data validates and matches visible content.
153
- - OG and Twitter previews use valid media and route-specific copy.
154
- - Internal links and redirects resolve; staging and private pages stay out of the index.
155
- - The report separates fixed technical issues, content recommendations, and unverified search
156
- outcomes.
353
+ - [ ] OG and Twitter previews use valid media and route-specific copy.
354
+ - [ ] Internal links and redirects resolve; staging and private pages stay out of the index.
355
+ - [ ] Fixed technical issues, content recommendations and unverified search outcomes are listed
356
+ separately, with every unsupported directive, missing transcript and page that should be gone.