@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.
- package/README.md +86 -86
- package/SKILL.md +160 -160
- package/dist/index.js +38 -9
- package/dist/index.js.map +1 -1
- package/dist/kit-catalog.json +1509 -1509
- package/dist/skills/bettercms/SKILL.md +206 -206
- package/dist/skills/bettercms/references/accessibility.md +171 -171
- package/dist/skills/bettercms/references/animation.md +296 -296
- package/dist/skills/bettercms/references/astro.md +148 -148
- package/dist/skills/bettercms/references/bindings.md +366 -366
- package/dist/skills/bettercms/references/componentize.md +185 -185
- package/dist/skills/bettercms/references/components.md +415 -415
- package/dist/skills/bettercms/references/content-experiments.md +62 -62
- package/dist/skills/bettercms/references/content-modeling.md +382 -382
- package/dist/skills/bettercms/references/detection.md +465 -465
- package/dist/skills/bettercms/references/dock-input.md +121 -121
- package/dist/skills/bettercms/references/editor-chrome.md +92 -92
- package/dist/skills/bettercms/references/external-content.md +78 -78
- package/dist/skills/bettercms/references/finish-site.md +377 -377
- package/dist/skills/bettercms/references/forms-and-interactions.md +245 -245
- package/dist/skills/bettercms/references/frameworks.md +159 -159
- package/dist/skills/bettercms/references/getting-started.md +156 -156
- package/dist/skills/bettercms/references/integrations.md +364 -364
- package/dist/skills/bettercms/references/kit-catalog.md +3259 -3259
- package/dist/skills/bettercms/references/kit-scenarios.md +712 -712
- package/dist/skills/bettercms/references/localization.md +85 -85
- package/dist/skills/bettercms/references/media-and-brand.md +181 -181
- package/dist/skills/bettercms/references/next.md +189 -189
- package/dist/skills/bettercms/references/other-frameworks.md +85 -85
- package/dist/skills/bettercms/references/portable-text.md +213 -213
- package/dist/skills/bettercms/references/programmatic-seo.md +98 -98
- package/dist/skills/bettercms/references/publishing.md +289 -289
- package/dist/skills/bettercms/references/seo.md +357 -357
- package/dist/skills/bettercms/references/shopify-headless.md +52 -52
- package/dist/skills/bettercms/references/site-blueprints.md +895 -895
- package/dist/skills/bettercms/references/site-review.md +74 -74
- package/dist/skills/bettercms/references/worked-examples-advanced.md +509 -509
- package/dist/skills/bettercms/references/worked-examples.md +885 -885
- package/dist/skills/bettercms/references/write-limits.md +217 -217
- package/package.json +1 -1
|
@@ -1,357 +1,357 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "SEO and AEO"
|
|
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
|
-
---
|
|
5
|
-
|
|
6
|
-
# SEO and AEO
|
|
7
|
-
|
|
8
|
-
**Outcome.** Every publishable route sends correct technical signals and carries useful,
|
|
9
|
-
attributable content. Keep an implemented change separate from a search-engine outcome, which
|
|
10
|
-
no one can guarantee.
|
|
11
|
-
|
|
12
|
-
Doctrine: SEO is native and never modelled (§5b of `bettercms://playbook/schema`); flow:
|
|
13
|
-
`seo_optimize` prompt.
|
|
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
|
-
|
|
26
|
-
## 1. Where each value lives
|
|
27
|
-
|
|
28
|
-
| Value | Writer | Notes |
|
|
29
|
-
|---|---|---|
|
|
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 |
|
|
31
|
-
| Entry meta (title, description, noindex, canonical, OG, Twitter, `schemaType`, `schema`) | `update_content_entry` with `meta` | Same merge rule |
|
|
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` |
|
|
33
|
-
| Suggestions | `generate_seo_meta` | Returns a suggestion only; apply it with the writers above |
|
|
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 |
|
|
36
|
-
|
|
37
|
-
Never create `seoTitle`-style fields on a model, page or component: the renderer, the sitemap,
|
|
38
|
-
`llms.txt` and `list_seo_issues` read the native values only. Self-hosted sites own their
|
|
39
|
-
`<head>`, sitemap and robots; the platform renderer emits them only for hosted sites, so confirm
|
|
40
|
-
the frontend emits the native values in its HTML ([next.md](next.md) covers `buildMetadata`).
|
|
41
|
-
There is no hreflang field: translations are native rows (`locale`, `translationOf`). The hosted
|
|
42
|
-
sitemap emits the hreflang alternates from them; a frontend emits the `<head>` links from each
|
|
43
|
-
entry's delivered `translations` ([localization.md](localization.md)).
|
|
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. A BetterCMS-built import fills it, the card and `og:type` from the home page's head where the project holds none |
|
|
57
|
-
| `keywords`, `author`, `theme-color`, a site verification tag | nothing | No native key stores them. The binding report's `elementGaps` lists each by name (`HEAD_META_UNMAPPED`); report them |
|
|
58
|
-
| A site-wide share image | `update_seo` `seoDefaults.ogImage` | Then pages inherit it and `og.image` stays empty |
|
|
59
|
-
| Canonical pointing at the page itself | nothing | A hosted page gets its own canonical |
|
|
60
|
-
| Canonical pointing at another URL | `canonical` | Keep it: someone consolidated those URLs on purpose |
|
|
61
|
-
| `<meta name="robots" content="noindex">` | `noindex: true` | Also drops the route from the sitemap, `llms.txt`, its Markdown twin and site search |
|
|
62
|
-
| `nofollow`, `noarchive`, `max-snippet` and other robots directives | nothing | No native key stores them. Report each page that used one |
|
|
63
|
-
| `<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)) |
|
|
64
|
-
| JSON-LD on a page | nothing | Read it as evidence and compare it with what BetterCMS emits (§4) |
|
|
65
|
-
| Organization JSON-LD on the home page | `update_seo` `siteSchema` | Keep name, URL, logo and `sameAs`; drop any value the site does not show |
|
|
66
|
-
| robots.txt | `update_seo` `robotsConfig` | Stay on `auto` unless the source has rules worth keeping, then `custom` with that text (§5) |
|
|
67
|
-
| sitemap.xml | `update_seo` `sitemapConfig` | Stay on `auto`. URLs the CMS does not own (a docs path, a static app) go in `extraUrls` |
|
|
68
|
-
| RSS or Atom feed | `update_seo` `rssConfig` `{ enabled: true }` | Item links come from the collection's `urlPattern` |
|
|
69
|
-
|
|
70
|
-
### Markdown twins
|
|
71
|
-
|
|
72
|
-
Every published page and entry is also served as Markdown for answer engines, at its own URL plus
|
|
73
|
-
`.md` (the home page at `/index.md`; an entry at its parent page's path, then `/<slug>.md`).
|
|
74
|
-
|
|
75
|
-
- The twin is generated from the content, per request. Nothing to model or keep in sync.
|
|
76
|
-
- A noindex page or entry has no twin, and is left out of `llms.txt`.
|
|
77
|
-
- To replace one twin, store a Markdown file at the same path in the dashboard (Pages →
|
|
78
|
-
Markdown). Only a published file serves; deleting it restores the generated twin. A second file
|
|
79
|
-
at the same path is refused (409, code markdown_path_taken).
|
|
80
|
-
- `llms.txt`, `llms-full.txt` and which pages serve a twin are switched there too. Those writes
|
|
81
|
-
need the SEO permission, and no MCP tool writes them.
|
|
82
|
-
|
|
83
|
-
## 2. Redirects and URLs
|
|
84
|
-
|
|
85
|
-
Keep every URL that ranks or is linked to. Change a path only when BetterCMS cannot express it.
|
|
86
|
-
|
|
87
|
-
1. Build a URL map first: each source URL (from the sitemap and a crawl) beside its new path.
|
|
88
|
-
2. Every row whose path changes gets one `create_redirect` (301) aimed at the FINAL URL, never at
|
|
89
|
-
an intermediate one. Chains are collapsed for you; a loop answers 422 and a source path that
|
|
90
|
-
already has a rule answers 409.
|
|
91
|
-
3. Import the source's own rules as well: a `_redirects` file, `vercel.json`, `netlify.toml`,
|
|
92
|
-
`.htaccess` or a redirect plugin's export.
|
|
93
|
-
4. Never send a batch of removed pages to the home page. Redirect each to its closest equivalent,
|
|
94
|
-
or retire it with a 410.
|
|
95
|
-
5. 302 or 307 is for a temporary move, 308 when the request method must survive.
|
|
96
|
-
6. A page gone for good with no equivalent takes `redirectType: "410"` and no destination. List
|
|
97
|
-
those in the report.
|
|
98
|
-
|
|
99
|
-
| Case | Do |
|
|
100
|
-
|---|---|
|
|
101
|
-
| 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 |
|
|
102
|
-
| Collection URLs | Set `urlPattern` (`/blog/:slug`): a fixed prefix and `:slug` exactly once |
|
|
103
|
-
| 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 |
|
|
104
|
-
| 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) |
|
|
105
|
-
| 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 |
|
|
106
|
-
| A URL folder renamed or moved | 301s are written for every page under it. Do not add them again |
|
|
107
|
-
| 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 |
|
|
108
|
-
|
|
109
|
-
## 3. Route-level audit
|
|
110
|
-
|
|
111
|
-
Read the route inventory, page and entry records, site defaults, the rendered `<head>`, robots,
|
|
112
|
-
sitemap, structured data and redirects. For each indexable route check:
|
|
113
|
-
|
|
114
|
-
- HTTP status, canonical, title, description, the main heading, language, breadcrumbs,
|
|
115
|
-
internal links, image alt text and the social preview.
|
|
116
|
-
- Page type and structured-data eligibility; schema values must match visible content.
|
|
117
|
-
- Duplicate or near-duplicate routes, pagination, query parameters, redirects, trailing slash.
|
|
118
|
-
- Robots and noindex conflicts, staging or private pages that got indexed, sitemap inclusion.
|
|
119
|
-
- Client-rendered content: does the initial HTML carry the key content?
|
|
120
|
-
|
|
121
|
-
A CMS field holding the right title does not prove the public route emits it. Fetch the HTML.
|
|
122
|
-
|
|
123
|
-
## 4. Structured data
|
|
124
|
-
|
|
125
|
-
`list_seo_issues` reports a pasted `schema` that lacks its type's required properties as
|
|
126
|
-
`schema.invalid`, naming the type and what is missing.
|
|
127
|
-
|
|
128
|
-
Choose in this order, and stop at the first that expresses the page:
|
|
129
|
-
|
|
130
|
-
1. **Automatic.** With no `schemaType` and no `schema`, JSON-LD is built from the content at
|
|
131
|
-
render time and follows every edit: WebSite on the home page (plus Organization when the site
|
|
132
|
-
has a name and a logo, or `siteSchema` when one is stored), Blog on a list of posts,
|
|
133
|
-
CollectionPage on any other list, BlogPosting on a post entry, Product on an entry of a
|
|
134
|
-
collection with a price or SKU, WebPage everywhere else. FAQPage is never automatic. A hosted
|
|
135
|
-
page or entry also gets a BreadcrumbList beside its main type: Home, each ancestor path that
|
|
136
|
-
is itself a published page (a folder with no page is skipped), then the document.
|
|
137
|
-
2. **`schemaType`.** One of nineteen: WebPage, AboutPage, ContactPage, CollectionPage, Blog,
|
|
138
|
-
BlogPosting, Article, Product, FAQPage, Event, Organization, LocalBusiness, JobPosting, Course,
|
|
139
|
-
Recipe, HowTo, Service, Person, VideoObject. Its properties are filled from the document's own
|
|
140
|
-
fields by key (a job's `company`, a recipe's `ingredients`), and a property the content lacks
|
|
141
|
-
is left out. A type missing what it cannot do without (a JobPosting with no company or posting
|
|
142
|
-
date, a Recipe with no image, a HowTo with no steps) is not emitted: the document keeps its
|
|
143
|
-
automatic type until the field is filled. Clear it with `"auto"`
|
|
144
|
-
on `update_page` or `null` in an entry's `meta`. For a whole collection, set
|
|
145
|
-
`seoDefaults.collectionSchemaTypes` once instead of typing every entry.
|
|
146
|
-
3. **Pasted `schema`.** Only when neither of the above can express the type AND every value in it
|
|
147
|
-
is visible on the page. It wins over both and does not follow content edits, so it goes stale.
|
|
148
|
-
|
|
149
|
-
JSON-LD found on the source site is evidence, not content. Use it to fill fields (event dates,
|
|
150
|
-
opening hours, prices) and to check that the emitted type and key values match. Do not paste it
|
|
151
|
-
back. Never add a second copy through custom head code.
|
|
152
|
-
|
|
153
|
-
### Content to schema.org type
|
|
154
|
-
|
|
155
|
-
| Source content | Type | How in BetterCMS |
|
|
156
|
-
|---|---|---|
|
|
157
|
-
| The whole site | Organization, WebSite | Automatic on the home page; `siteSchema` to add `sameAs` or replace it |
|
|
158
|
-
| 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` |
|
|
159
|
-
| List of posts, any other list | Blog, CollectionPage | Automatic |
|
|
160
|
-
| Product | Product with an Offer | Automatic when the model has a price or SKU; else `schemaType` |
|
|
161
|
-
| FAQ: visible questions with answers | FAQPage | `schemaType: "FAQPage"`. It reads the first repeater whose rows hold a question and an answer |
|
|
162
|
-
| Event | Event | `schemaType` (name, start, end, location read from the fields) |
|
|
163
|
-
| Company, store, restaurant, office | Organization, LocalBusiness | `schemaType`; a subtype such as Restaurant, a `PostalAddress`, geo or opening hours need pasted `schema` |
|
|
164
|
-
| About or contact page | AboutPage, ContactPage | `schemaType` |
|
|
165
|
-
| Breadcrumb trail | BreadcrumbList | Automatic on hosted pages (ancestors that have a page). Paste `schema` only to match a trail the page shows differently |
|
|
166
|
-
| News, recipe, job, video, course, podcast episode, how-to, author profile | NewsArticle, Recipe, JobPosting, VideoObject, Course, PodcastEpisode, HowTo, ProfilePage | Pasted `schema`, visible values only |
|
|
167
|
-
| Product with on-page reviews | AggregateRating, Review | Pasted `schema`, with the average and count the page displays |
|
|
168
|
-
|
|
169
|
-
### Rules
|
|
170
|
-
|
|
171
|
-
- Every value must be on the page. No invented rating, review count, price, availability, salary,
|
|
172
|
-
author or date. What the page does not show is left out.
|
|
173
|
-
- One FAQPage per URL and one main entity per page. Two FAQ sections on one page still make one
|
|
174
|
-
FAQPage. An accordion of features is not a FAQ.
|
|
175
|
-
- `AggregateRating` needs a product or another reviewable subject. Never on the home page about
|
|
176
|
-
the business itself, and never typed by hand.
|
|
177
|
-
- Awards go in `award`; they are not certifications.
|
|
178
|
-
- Dates are ISO 8601 with a time zone offset.
|
|
179
|
-
- Cart, checkout, 404 and legal pages stay on Automatic (plain WebPage). There is no off switch;
|
|
180
|
-
never give them a richer type.
|
|
181
|
-
- Pasted JSON-LD is stored as given and is not checked for required properties. Check them
|
|
182
|
-
yourself before saving:
|
|
183
|
-
|
|
184
|
-
| Type | Required |
|
|
185
|
-
|---|---|
|
|
186
|
-
| Article, BlogPosting | `headline`, `image` |
|
|
187
|
-
| Product | `name` plus one of `offers`, `review`, `aggregateRating` |
|
|
188
|
-
| Recipe | `name`, `image` |
|
|
189
|
-
| FAQPage | `mainEntity`, each with `name` and `acceptedAnswer.text` |
|
|
190
|
-
| BreadcrumbList | `itemListElement`, each with `position` and `name` |
|
|
191
|
-
| Organization | `name`, `url` |
|
|
192
|
-
| LocalBusiness | `name`, `address` |
|
|
193
|
-
| Event | `name`, `startDate`, `location` |
|
|
194
|
-
|
|
195
|
-
Valid markup does not guarantee a rich result: [Google structured-data guidelines](https://developers.google.com/search/docs/appearance/structured-data/sd-policies).
|
|
196
|
-
|
|
197
|
-
## 5. Sitemap, robots, llms.txt
|
|
198
|
-
|
|
199
|
-
- The hosted sitemap leaves out noindex pages, and the generated robots.txt points at the
|
|
200
|
-
sitemap. Confirm drafts, private pages and noindex pages are absent from the live file.
|
|
201
|
-
- A translated page or entry carries `<xhtml:link rel="alternate" hreflang>` for each listed
|
|
202
|
-
sibling (its `locale`, else the project's `localeConfig.defaultLanguage`) plus `x-default` for
|
|
203
|
-
the source row. A noindex or unpublished sibling is left out of the links as well.
|
|
204
|
-
- `sitemapConfig` sets it `auto` or `custom`, the default changefreq and priority and `extraUrls`;
|
|
205
|
-
`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
|
|
206
|
-
IndexNow on publish; `searchConfig` { enabled, placeholder, aiEnabled } is site search, which
|
|
207
|
-
`search_content` queries (published content only).
|
|
208
|
-
- `refresh_search_index` costs nothing: `action: 'indexnow'` submits `urls` (this site's own, up
|
|
209
|
-
to 1000) or, without them, every change since the last submission; `action: 'reindex'` rebuilds
|
|
210
|
-
site search after bulk changes. A 503 means the index is unreachable: retry later.
|
|
211
|
-
- `llms.txt` lists one link per published item. Maintain it for its intended readers, but do not
|
|
212
|
-
sell it as a ranking requirement: Google's [guidance for AI features](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide)
|
|
213
|
-
says ordinary SEO fundamentals are the base.
|
|
214
|
-
- `set_site_file` stores a hand-written `llms.txt` or `llms-full.txt` (plain text, up to 5 MB).
|
|
215
|
-
Give `llms.txt` a one-line site summary taken from the home page's description or About copy.
|
|
216
|
-
- Check each redirect's real status and target, and detect loops.
|
|
217
|
-
|
|
218
|
-
### AI crawlers
|
|
219
|
-
|
|
220
|
-
`robotsConfig` is `{ mode: "auto" }` or `{ mode: "custom", custom: "<robots.txt text>" }` (up to
|
|
221
|
-
10,000 characters). `robotsConfig.disallow` is the per-bot switch: a list of names from GPTBot,
|
|
222
|
-
OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-Web, anthropic-ai, PerplexityBot, Google-Extended,
|
|
223
|
-
CCBot, Bytespider, Applebot-Extended, Amazonbot and Meta-ExternalAgent. Each named bot is
|
|
224
|
-
disallowed from the whole site in auto mode. In custom mode the custom text is served as written
|
|
225
|
-
and `disallow` is not applied; any other bot or a path rule is a line of custom text.
|
|
226
|
-
|
|
227
|
-
- Carry the source's AI-bot rules over exactly. Changing the owner's crawler policy is not part of
|
|
228
|
-
a conversion. No rules in the source: stay on `auto` and say so in the report.
|
|
229
|
-
- Custom text replaces the generated file, so include the `Sitemap:` line yourself.
|
|
230
|
-
- Blocking `Google-Extended` or `Applebot-Extended` leaves Google Search and Siri untouched.
|
|
231
|
-
Blocking an answer or search bot (`OAI-SearchBot`, `ChatGPT-User`, `PerplexityBot`,
|
|
232
|
-
`DuckAssistBot`) removes the site from that product's answers. Say so before the owner asks
|
|
233
|
-
for it.
|
|
234
|
-
- Other agents to look for in a source file: `GPTBot`, `ClaudeBot`, `Claude-Web`, `anthropic-ai`,
|
|
235
|
-
`Meta-ExternalAgent`, `FacebookBot`, `Amazonbot`, `cohere-ai`, `CCBot`, `Bytespider`.
|
|
236
|
-
|
|
237
|
-
```text
|
|
238
|
-
User-agent: *
|
|
239
|
-
Allow: /
|
|
240
|
-
|
|
241
|
-
User-agent: CCBot
|
|
242
|
-
Disallow: /
|
|
243
|
-
|
|
244
|
-
Sitemap: https://caldera.com/sitemap.xml
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
## 6. AEO and content quality
|
|
248
|
-
|
|
249
|
-
Set these up while converting, without asking:
|
|
250
|
-
|
|
251
|
-
| Do | How |
|
|
252
|
-
|---|---|
|
|
253
|
-
| Facts as text | Specs, prices, menus, opening hours and chart data become fields or a real table, never an image of them |
|
|
254
|
-
| 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 |
|
|
255
|
-
| 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 |
|
|
256
|
-
| Dated facts | A statistic or comparison keeps its date and source in fields beside the number |
|
|
257
|
-
| Authors as entries | An author collection with name, bio and photo, referenced from each post |
|
|
258
|
-
| 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)) |
|
|
259
|
-
| Twins and `llms.txt` | Leave them on unless the owner turned them off |
|
|
260
|
-
|
|
261
|
-
Improve the page for people first: clear questions and answers where the topic warrants them,
|
|
262
|
-
descriptive headings, first-hand evidence, dates and named authors where real, text alongside
|
|
263
|
-
media, consistent internal links and accurate structured data. No "AI-only" pages, artificial
|
|
264
|
-
snippets or unverifiable claims.
|
|
265
|
-
|
|
266
|
-
Report concrete findings ("canonical points to a 404", "OG image missing", "FAQ schema does not
|
|
267
|
-
match the visible answers"), never an untraceable score.
|
|
268
|
-
|
|
269
|
-
## 7. Publish and verify
|
|
270
|
-
|
|
271
|
-
After an authorized change ([publishing.md](publishing.md)): inspect the rendered `<head>` and JSON-LD,
|
|
272
|
-
route status, sitemap and robots output, and redirects. Use Search Console or similar only to
|
|
273
|
-
report observed behaviour after launch, never to predict indexing.
|
|
274
|
-
|
|
275
|
-
Validators, run against the live URL (they read the public HTML, never the CMS):
|
|
276
|
-
|
|
277
|
-
| Checks | Tool |
|
|
278
|
-
|---|---|
|
|
279
|
-
| Open Graph tags as Facebook reads them; refreshes its cache | [Sharing Debugger](https://developers.facebook.com/tools/debug/) |
|
|
280
|
-
| LinkedIn card | [Post Inspector](https://www.linkedin.com/post-inspector/) |
|
|
281
|
-
| Any OG card | [opengraph.ing](https://opengraph.ing/) |
|
|
282
|
-
| JSON-LD eligibility | [Rich Results Test](https://search.google.com/test/rich-results) |
|
|
283
|
-
| JSON-LD syntax against schema.org | [Schema Markup Validator](https://validator.schema.org/) |
|
|
284
|
-
| Sitemap XML | [XML Sitemaps validator](https://www.xml-sitemaps.com/validate-xml-sitemap.html) |
|
|
285
|
-
|
|
286
|
-
X (Twitter) has no working public card preview: confirm the `twitter:*` tags in the fetched HTML.
|
|
287
|
-
|
|
288
|
-
### A generated OG image on a self-hosted Next site
|
|
289
|
-
|
|
290
|
-
When pages have no designed share image, render one per route instead of modelling an image
|
|
291
|
-
field for it. The image reads the entry's own title, so it follows every content edit:
|
|
292
|
-
|
|
293
|
-
```tsx
|
|
294
|
-
// app/og/[slug]/route.tsx
|
|
295
|
-
import { ImageResponse } from "next/og";
|
|
296
|
-
import { cms } from "@/lib/cms"; // createBetterCMS(...)
|
|
297
|
-
|
|
298
|
-
export async function GET(_req: Request, { params }: { params: Promise<{ slug: string }> }) {
|
|
299
|
-
const { slug } = await params;
|
|
300
|
-
const entry = await cms.getEntry<{ title: string }>(slug);
|
|
301
|
-
if (!entry) return new Response("Not found", { status: 404 });
|
|
302
|
-
return new ImageResponse(
|
|
303
|
-
<div style={{ display: "flex", width: "100%", height: "100%", padding: 64, fontSize: 64 }}>
|
|
304
|
-
{entry.fields.title}
|
|
305
|
-
</div>,
|
|
306
|
-
{ width: 1200, height: 630 },
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
Point the route's OG image at it with `update_content_entry` `meta.og.image` (or `update_page`
|
|
312
|
-
for a page), or from `generateMetadata`. Rules: 1200×630, an absolute URL, a 404 (never a blank image) for a
|
|
313
|
-
missing slug, and strip stega from any preview string first ([bindings.md](bindings.md) §5).
|
|
314
|
-
A hosted site gets its OG image from the native value; this route is for self-hosted Next only.
|
|
315
|
-
|
|
316
|
-
## SEO and AEO gate
|
|
317
|
-
|
|
318
|
-
Run before publishing or reporting done, then run `list_seo_issues` and `get_next_steps` and fix
|
|
319
|
-
what they list. The accessibility half of the gate is the check list in
|
|
320
|
-
[accessibility.md](accessibility.md).
|
|
321
|
-
|
|
322
|
-
**SEO**
|
|
323
|
-
|
|
324
|
-
- [ ] No model, page or component carries a meta, OG, canonical, noindex, hreflang or schema field.
|
|
325
|
-
- [ ] Every indexable page and entry has a `metaTitle` of at most 60 characters and a
|
|
326
|
-
`metaDescription` (aim 120–160, never over 160). No two titles match.
|
|
327
|
-
- [ ] Source canonicals that point elsewhere are kept; self canonicals are not stored.
|
|
328
|
-
- [ ] Source noindex pages are `noindex`, and so are search, cart, checkout and thank-you pages.
|
|
329
|
-
- [ ] Each language is its own row with `locale` and `translationOf`.
|
|
330
|
-
- [ ] Every changed URL has a 301 to its final URL; the source's redirects are imported; no chain,
|
|
331
|
-
no loop, no batch pointed at the home page.
|
|
332
|
-
- [ ] `seoDefaults`, `siteSchema` and `collectionSchemaTypes` are set.
|
|
333
|
-
- [ ] Sitemap and robots are `auto`, or `custom` carrying the source's rules.
|
|
334
|
-
- [ ] Native values and the fetched HTML agree on representative static and dynamic routes.
|
|
335
|
-
|
|
336
|
-
**Structured data**
|
|
337
|
-
|
|
338
|
-
- [ ] Each page is on Automatic unless a type is missing; then `schemaType`; pasted `schema` last.
|
|
339
|
-
- [ ] The emitted JSON-LD matches the source's for type and key values, and validates.
|
|
340
|
-
- [ ] Every value is visible on the page.
|
|
341
|
-
- [ ] One FAQPage per URL. No self-serving `AggregateRating`.
|
|
342
|
-
- [ ] Pasted JSON-LD parses and has its type's required properties.
|
|
343
|
-
|
|
344
|
-
**AEO**
|
|
345
|
-
|
|
346
|
-
- [ ] Specs, prices, hours, menus and chart data are text in fields or tables.
|
|
347
|
-
- [ ] Names are consistent and come from referenced entries.
|
|
348
|
-
- [ ] `llms.txt` has a site summary and Markdown twins are on.
|
|
349
|
-
- [ ] Videos and podcasts have a transcript, or the gap is reported.
|
|
350
|
-
- [ ] The source's AI-crawler policy is carried over unchanged.
|
|
351
|
-
|
|
352
|
-
**Report**
|
|
353
|
-
|
|
354
|
-
- [ ] OG and Twitter previews use valid media and route-specific copy.
|
|
355
|
-
- [ ] Internal links and redirects resolve; staging and private pages stay out of the index.
|
|
356
|
-
- [ ] Fixed technical issues, content recommendations and unverified search outcomes are listed
|
|
357
|
-
separately, with every unsupported directive, missing transcript and page that should be gone.
|
|
1
|
+
---
|
|
2
|
+
title: "SEO and AEO"
|
|
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
|
+
---
|
|
5
|
+
|
|
6
|
+
# SEO and AEO
|
|
7
|
+
|
|
8
|
+
**Outcome.** Every publishable route sends correct technical signals and carries useful,
|
|
9
|
+
attributable content. Keep an implemented change separate from a search-engine outcome, which
|
|
10
|
+
no one can guarantee.
|
|
11
|
+
|
|
12
|
+
Doctrine: SEO is native and never modelled (§5b of `bettercms://playbook/schema`); flow:
|
|
13
|
+
`seo_optimize` prompt.
|
|
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
|
+
|
|
26
|
+
## 1. Where each value lives
|
|
27
|
+
|
|
28
|
+
| Value | Writer | Notes |
|
|
29
|
+
|---|---|---|
|
|
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 |
|
|
31
|
+
| Entry meta (title, description, noindex, canonical, OG, Twitter, `schemaType`, `schema`) | `update_content_entry` with `meta` | Same merge rule |
|
|
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` |
|
|
33
|
+
| Suggestions | `generate_seo_meta` | Returns a suggestion only; apply it with the writers above |
|
|
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 |
|
|
36
|
+
|
|
37
|
+
Never create `seoTitle`-style fields on a model, page or component: the renderer, the sitemap,
|
|
38
|
+
`llms.txt` and `list_seo_issues` read the native values only. Self-hosted sites own their
|
|
39
|
+
`<head>`, sitemap and robots; the platform renderer emits them only for hosted sites, so confirm
|
|
40
|
+
the frontend emits the native values in its HTML ([next.md](next.md) covers `buildMetadata`).
|
|
41
|
+
There is no hreflang field: translations are native rows (`locale`, `translationOf`). The hosted
|
|
42
|
+
sitemap emits the hreflang alternates from them; a frontend emits the `<head>` links from each
|
|
43
|
+
entry's delivered `translations` ([localization.md](localization.md)).
|
|
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. A BetterCMS-built import fills it, the card and `og:type` from the home page's head where the project holds none |
|
|
57
|
+
| `keywords`, `author`, `theme-color`, a site verification tag | nothing | No native key stores them. The binding report's `elementGaps` lists each by name (`HEAD_META_UNMAPPED`); report them |
|
|
58
|
+
| A site-wide share image | `update_seo` `seoDefaults.ogImage` | Then pages inherit it and `og.image` stays empty |
|
|
59
|
+
| Canonical pointing at the page itself | nothing | A hosted page gets its own canonical |
|
|
60
|
+
| Canonical pointing at another URL | `canonical` | Keep it: someone consolidated those URLs on purpose |
|
|
61
|
+
| `<meta name="robots" content="noindex">` | `noindex: true` | Also drops the route from the sitemap, `llms.txt`, its Markdown twin and site search |
|
|
62
|
+
| `nofollow`, `noarchive`, `max-snippet` and other robots directives | nothing | No native key stores them. Report each page that used one |
|
|
63
|
+
| `<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)) |
|
|
64
|
+
| JSON-LD on a page | nothing | Read it as evidence and compare it with what BetterCMS emits (§4) |
|
|
65
|
+
| Organization JSON-LD on the home page | `update_seo` `siteSchema` | Keep name, URL, logo and `sameAs`; drop any value the site does not show |
|
|
66
|
+
| robots.txt | `update_seo` `robotsConfig` | Stay on `auto` unless the source has rules worth keeping, then `custom` with that text (§5) |
|
|
67
|
+
| sitemap.xml | `update_seo` `sitemapConfig` | Stay on `auto`. URLs the CMS does not own (a docs path, a static app) go in `extraUrls` |
|
|
68
|
+
| RSS or Atom feed | `update_seo` `rssConfig` `{ enabled: true }` | Item links come from the collection's `urlPattern` |
|
|
69
|
+
|
|
70
|
+
### Markdown twins
|
|
71
|
+
|
|
72
|
+
Every published page and entry is also served as Markdown for answer engines, at its own URL plus
|
|
73
|
+
`.md` (the home page at `/index.md`; an entry at its parent page's path, then `/<slug>.md`).
|
|
74
|
+
|
|
75
|
+
- The twin is generated from the content, per request. Nothing to model or keep in sync.
|
|
76
|
+
- A noindex page or entry has no twin, and is left out of `llms.txt`.
|
|
77
|
+
- To replace one twin, store a Markdown file at the same path in the dashboard (Pages →
|
|
78
|
+
Markdown). Only a published file serves; deleting it restores the generated twin. A second file
|
|
79
|
+
at the same path is refused (409, code markdown_path_taken).
|
|
80
|
+
- `llms.txt`, `llms-full.txt` and which pages serve a twin are switched there too. Those writes
|
|
81
|
+
need the SEO permission, and no MCP tool writes them.
|
|
82
|
+
|
|
83
|
+
## 2. Redirects and URLs
|
|
84
|
+
|
|
85
|
+
Keep every URL that ranks or is linked to. Change a path only when BetterCMS cannot express it.
|
|
86
|
+
|
|
87
|
+
1. Build a URL map first: each source URL (from the sitemap and a crawl) beside its new path.
|
|
88
|
+
2. Every row whose path changes gets one `create_redirect` (301) aimed at the FINAL URL, never at
|
|
89
|
+
an intermediate one. Chains are collapsed for you; a loop answers 422 and a source path that
|
|
90
|
+
already has a rule answers 409.
|
|
91
|
+
3. Import the source's own rules as well: a `_redirects` file, `vercel.json`, `netlify.toml`,
|
|
92
|
+
`.htaccess` or a redirect plugin's export.
|
|
93
|
+
4. Never send a batch of removed pages to the home page. Redirect each to its closest equivalent,
|
|
94
|
+
or retire it with a 410.
|
|
95
|
+
5. 302 or 307 is for a temporary move, 308 when the request method must survive.
|
|
96
|
+
6. A page gone for good with no equivalent takes `redirectType: "410"` and no destination. List
|
|
97
|
+
those in the report.
|
|
98
|
+
|
|
99
|
+
| Case | Do |
|
|
100
|
+
|---|---|
|
|
101
|
+
| 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 |
|
|
102
|
+
| Collection URLs | Set `urlPattern` (`/blog/:slug`): a fixed prefix and `:slug` exactly once |
|
|
103
|
+
| 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 |
|
|
104
|
+
| 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) |
|
|
105
|
+
| 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 |
|
|
106
|
+
| A URL folder renamed or moved | 301s are written for every page under it. Do not add them again |
|
|
107
|
+
| 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 |
|
|
108
|
+
|
|
109
|
+
## 3. Route-level audit
|
|
110
|
+
|
|
111
|
+
Read the route inventory, page and entry records, site defaults, the rendered `<head>`, robots,
|
|
112
|
+
sitemap, structured data and redirects. For each indexable route check:
|
|
113
|
+
|
|
114
|
+
- HTTP status, canonical, title, description, the main heading, language, breadcrumbs,
|
|
115
|
+
internal links, image alt text and the social preview.
|
|
116
|
+
- Page type and structured-data eligibility; schema values must match visible content.
|
|
117
|
+
- Duplicate or near-duplicate routes, pagination, query parameters, redirects, trailing slash.
|
|
118
|
+
- Robots and noindex conflicts, staging or private pages that got indexed, sitemap inclusion.
|
|
119
|
+
- Client-rendered content: does the initial HTML carry the key content?
|
|
120
|
+
|
|
121
|
+
A CMS field holding the right title does not prove the public route emits it. Fetch the HTML.
|
|
122
|
+
|
|
123
|
+
## 4. Structured data
|
|
124
|
+
|
|
125
|
+
`list_seo_issues` reports a pasted `schema` that lacks its type's required properties as
|
|
126
|
+
`schema.invalid`, naming the type and what is missing.
|
|
127
|
+
|
|
128
|
+
Choose in this order, and stop at the first that expresses the page:
|
|
129
|
+
|
|
130
|
+
1. **Automatic.** With no `schemaType` and no `schema`, JSON-LD is built from the content at
|
|
131
|
+
render time and follows every edit: WebSite on the home page (plus Organization when the site
|
|
132
|
+
has a name and a logo, or `siteSchema` when one is stored), Blog on a list of posts,
|
|
133
|
+
CollectionPage on any other list, BlogPosting on a post entry, Product on an entry of a
|
|
134
|
+
collection with a price or SKU, WebPage everywhere else. FAQPage is never automatic. A hosted
|
|
135
|
+
page or entry also gets a BreadcrumbList beside its main type: Home, each ancestor path that
|
|
136
|
+
is itself a published page (a folder with no page is skipped), then the document.
|
|
137
|
+
2. **`schemaType`.** One of nineteen: WebPage, AboutPage, ContactPage, CollectionPage, Blog,
|
|
138
|
+
BlogPosting, Article, Product, FAQPage, Event, Organization, LocalBusiness, JobPosting, Course,
|
|
139
|
+
Recipe, HowTo, Service, Person, VideoObject. Its properties are filled from the document's own
|
|
140
|
+
fields by key (a job's `company`, a recipe's `ingredients`), and a property the content lacks
|
|
141
|
+
is left out. A type missing what it cannot do without (a JobPosting with no company or posting
|
|
142
|
+
date, a Recipe with no image, a HowTo with no steps) is not emitted: the document keeps its
|
|
143
|
+
automatic type until the field is filled. Clear it with `"auto"`
|
|
144
|
+
on `update_page` or `null` in an entry's `meta`. For a whole collection, set
|
|
145
|
+
`seoDefaults.collectionSchemaTypes` once instead of typing every entry.
|
|
146
|
+
3. **Pasted `schema`.** Only when neither of the above can express the type AND every value in it
|
|
147
|
+
is visible on the page. It wins over both and does not follow content edits, so it goes stale.
|
|
148
|
+
|
|
149
|
+
JSON-LD found on the source site is evidence, not content. Use it to fill fields (event dates,
|
|
150
|
+
opening hours, prices) and to check that the emitted type and key values match. Do not paste it
|
|
151
|
+
back. Never add a second copy through custom head code.
|
|
152
|
+
|
|
153
|
+
### Content to schema.org type
|
|
154
|
+
|
|
155
|
+
| Source content | Type | How in BetterCMS |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| The whole site | Organization, WebSite | Automatic on the home page; `siteSchema` to add `sameAs` or replace it |
|
|
158
|
+
| 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` |
|
|
159
|
+
| List of posts, any other list | Blog, CollectionPage | Automatic |
|
|
160
|
+
| Product | Product with an Offer | Automatic when the model has a price or SKU; else `schemaType` |
|
|
161
|
+
| FAQ: visible questions with answers | FAQPage | `schemaType: "FAQPage"`. It reads the first repeater whose rows hold a question and an answer |
|
|
162
|
+
| Event | Event | `schemaType` (name, start, end, location read from the fields) |
|
|
163
|
+
| Company, store, restaurant, office | Organization, LocalBusiness | `schemaType`; a subtype such as Restaurant, a `PostalAddress`, geo or opening hours need pasted `schema` |
|
|
164
|
+
| About or contact page | AboutPage, ContactPage | `schemaType` |
|
|
165
|
+
| Breadcrumb trail | BreadcrumbList | Automatic on hosted pages (ancestors that have a page). Paste `schema` only to match a trail the page shows differently |
|
|
166
|
+
| News, recipe, job, video, course, podcast episode, how-to, author profile | NewsArticle, Recipe, JobPosting, VideoObject, Course, PodcastEpisode, HowTo, ProfilePage | Pasted `schema`, visible values only |
|
|
167
|
+
| Product with on-page reviews | AggregateRating, Review | Pasted `schema`, with the average and count the page displays |
|
|
168
|
+
|
|
169
|
+
### Rules
|
|
170
|
+
|
|
171
|
+
- Every value must be on the page. No invented rating, review count, price, availability, salary,
|
|
172
|
+
author or date. What the page does not show is left out.
|
|
173
|
+
- One FAQPage per URL and one main entity per page. Two FAQ sections on one page still make one
|
|
174
|
+
FAQPage. An accordion of features is not a FAQ.
|
|
175
|
+
- `AggregateRating` needs a product or another reviewable subject. Never on the home page about
|
|
176
|
+
the business itself, and never typed by hand.
|
|
177
|
+
- Awards go in `award`; they are not certifications.
|
|
178
|
+
- Dates are ISO 8601 with a time zone offset.
|
|
179
|
+
- Cart, checkout, 404 and legal pages stay on Automatic (plain WebPage). There is no off switch;
|
|
180
|
+
never give them a richer type.
|
|
181
|
+
- Pasted JSON-LD is stored as given and is not checked for required properties. Check them
|
|
182
|
+
yourself before saving:
|
|
183
|
+
|
|
184
|
+
| Type | Required |
|
|
185
|
+
|---|---|
|
|
186
|
+
| Article, BlogPosting | `headline`, `image` |
|
|
187
|
+
| Product | `name` plus one of `offers`, `review`, `aggregateRating` |
|
|
188
|
+
| Recipe | `name`, `image` |
|
|
189
|
+
| FAQPage | `mainEntity`, each with `name` and `acceptedAnswer.text` |
|
|
190
|
+
| BreadcrumbList | `itemListElement`, each with `position` and `name` |
|
|
191
|
+
| Organization | `name`, `url` |
|
|
192
|
+
| LocalBusiness | `name`, `address` |
|
|
193
|
+
| Event | `name`, `startDate`, `location` |
|
|
194
|
+
|
|
195
|
+
Valid markup does not guarantee a rich result: [Google structured-data guidelines](https://developers.google.com/search/docs/appearance/structured-data/sd-policies).
|
|
196
|
+
|
|
197
|
+
## 5. Sitemap, robots, llms.txt
|
|
198
|
+
|
|
199
|
+
- The hosted sitemap leaves out noindex pages, and the generated robots.txt points at the
|
|
200
|
+
sitemap. Confirm drafts, private pages and noindex pages are absent from the live file.
|
|
201
|
+
- A translated page or entry carries `<xhtml:link rel="alternate" hreflang>` for each listed
|
|
202
|
+
sibling (its `locale`, else the project's `localeConfig.defaultLanguage`) plus `x-default` for
|
|
203
|
+
the source row. A noindex or unpublished sibling is left out of the links as well.
|
|
204
|
+
- `sitemapConfig` sets it `auto` or `custom`, the default changefreq and priority and `extraUrls`;
|
|
205
|
+
`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
|
|
206
|
+
IndexNow on publish; `searchConfig` { enabled, placeholder, aiEnabled } is site search, which
|
|
207
|
+
`search_content` queries (published content only).
|
|
208
|
+
- `refresh_search_index` costs nothing: `action: 'indexnow'` submits `urls` (this site's own, up
|
|
209
|
+
to 1000) or, without them, every change since the last submission; `action: 'reindex'` rebuilds
|
|
210
|
+
site search after bulk changes. A 503 means the index is unreachable: retry later.
|
|
211
|
+
- `llms.txt` lists one link per published item. Maintain it for its intended readers, but do not
|
|
212
|
+
sell it as a ranking requirement: Google's [guidance for AI features](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide)
|
|
213
|
+
says ordinary SEO fundamentals are the base.
|
|
214
|
+
- `set_site_file` stores a hand-written `llms.txt` or `llms-full.txt` (plain text, up to 5 MB).
|
|
215
|
+
Give `llms.txt` a one-line site summary taken from the home page's description or About copy.
|
|
216
|
+
- Check each redirect's real status and target, and detect loops.
|
|
217
|
+
|
|
218
|
+
### AI crawlers
|
|
219
|
+
|
|
220
|
+
`robotsConfig` is `{ mode: "auto" }` or `{ mode: "custom", custom: "<robots.txt text>" }` (up to
|
|
221
|
+
10,000 characters). `robotsConfig.disallow` is the per-bot switch: a list of names from GPTBot,
|
|
222
|
+
OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-Web, anthropic-ai, PerplexityBot, Google-Extended,
|
|
223
|
+
CCBot, Bytespider, Applebot-Extended, Amazonbot and Meta-ExternalAgent. Each named bot is
|
|
224
|
+
disallowed from the whole site in auto mode. In custom mode the custom text is served as written
|
|
225
|
+
and `disallow` is not applied; any other bot or a path rule is a line of custom text.
|
|
226
|
+
|
|
227
|
+
- Carry the source's AI-bot rules over exactly. Changing the owner's crawler policy is not part of
|
|
228
|
+
a conversion. No rules in the source: stay on `auto` and say so in the report.
|
|
229
|
+
- Custom text replaces the generated file, so include the `Sitemap:` line yourself.
|
|
230
|
+
- Blocking `Google-Extended` or `Applebot-Extended` leaves Google Search and Siri untouched.
|
|
231
|
+
Blocking an answer or search bot (`OAI-SearchBot`, `ChatGPT-User`, `PerplexityBot`,
|
|
232
|
+
`DuckAssistBot`) removes the site from that product's answers. Say so before the owner asks
|
|
233
|
+
for it.
|
|
234
|
+
- Other agents to look for in a source file: `GPTBot`, `ClaudeBot`, `Claude-Web`, `anthropic-ai`,
|
|
235
|
+
`Meta-ExternalAgent`, `FacebookBot`, `Amazonbot`, `cohere-ai`, `CCBot`, `Bytespider`.
|
|
236
|
+
|
|
237
|
+
```text
|
|
238
|
+
User-agent: *
|
|
239
|
+
Allow: /
|
|
240
|
+
|
|
241
|
+
User-agent: CCBot
|
|
242
|
+
Disallow: /
|
|
243
|
+
|
|
244
|
+
Sitemap: https://caldera.com/sitemap.xml
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## 6. AEO and content quality
|
|
248
|
+
|
|
249
|
+
Set these up while converting, without asking:
|
|
250
|
+
|
|
251
|
+
| Do | How |
|
|
252
|
+
|---|---|
|
|
253
|
+
| Facts as text | Specs, prices, menus, opening hours and chart data become fields or a real table, never an image of them |
|
|
254
|
+
| 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 |
|
|
255
|
+
| 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 |
|
|
256
|
+
| Dated facts | A statistic or comparison keeps its date and source in fields beside the number |
|
|
257
|
+
| Authors as entries | An author collection with name, bio and photo, referenced from each post |
|
|
258
|
+
| 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)) |
|
|
259
|
+
| Twins and `llms.txt` | Leave them on unless the owner turned them off |
|
|
260
|
+
|
|
261
|
+
Improve the page for people first: clear questions and answers where the topic warrants them,
|
|
262
|
+
descriptive headings, first-hand evidence, dates and named authors where real, text alongside
|
|
263
|
+
media, consistent internal links and accurate structured data. No "AI-only" pages, artificial
|
|
264
|
+
snippets or unverifiable claims.
|
|
265
|
+
|
|
266
|
+
Report concrete findings ("canonical points to a 404", "OG image missing", "FAQ schema does not
|
|
267
|
+
match the visible answers"), never an untraceable score.
|
|
268
|
+
|
|
269
|
+
## 7. Publish and verify
|
|
270
|
+
|
|
271
|
+
After an authorized change ([publishing.md](publishing.md)): inspect the rendered `<head>` and JSON-LD,
|
|
272
|
+
route status, sitemap and robots output, and redirects. Use Search Console or similar only to
|
|
273
|
+
report observed behaviour after launch, never to predict indexing.
|
|
274
|
+
|
|
275
|
+
Validators, run against the live URL (they read the public HTML, never the CMS):
|
|
276
|
+
|
|
277
|
+
| Checks | Tool |
|
|
278
|
+
|---|---|
|
|
279
|
+
| Open Graph tags as Facebook reads them; refreshes its cache | [Sharing Debugger](https://developers.facebook.com/tools/debug/) |
|
|
280
|
+
| LinkedIn card | [Post Inspector](https://www.linkedin.com/post-inspector/) |
|
|
281
|
+
| Any OG card | [opengraph.ing](https://opengraph.ing/) |
|
|
282
|
+
| JSON-LD eligibility | [Rich Results Test](https://search.google.com/test/rich-results) |
|
|
283
|
+
| JSON-LD syntax against schema.org | [Schema Markup Validator](https://validator.schema.org/) |
|
|
284
|
+
| Sitemap XML | [XML Sitemaps validator](https://www.xml-sitemaps.com/validate-xml-sitemap.html) |
|
|
285
|
+
|
|
286
|
+
X (Twitter) has no working public card preview: confirm the `twitter:*` tags in the fetched HTML.
|
|
287
|
+
|
|
288
|
+
### A generated OG image on a self-hosted Next site
|
|
289
|
+
|
|
290
|
+
When pages have no designed share image, render one per route instead of modelling an image
|
|
291
|
+
field for it. The image reads the entry's own title, so it follows every content edit:
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
// app/og/[slug]/route.tsx
|
|
295
|
+
import { ImageResponse } from "next/og";
|
|
296
|
+
import { cms } from "@/lib/cms"; // createBetterCMS(...)
|
|
297
|
+
|
|
298
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ slug: string }> }) {
|
|
299
|
+
const { slug } = await params;
|
|
300
|
+
const entry = await cms.getEntry<{ title: string }>(slug);
|
|
301
|
+
if (!entry) return new Response("Not found", { status: 404 });
|
|
302
|
+
return new ImageResponse(
|
|
303
|
+
<div style={{ display: "flex", width: "100%", height: "100%", padding: 64, fontSize: 64 }}>
|
|
304
|
+
{entry.fields.title}
|
|
305
|
+
</div>,
|
|
306
|
+
{ width: 1200, height: 630 },
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Point the route's OG image at it with `update_content_entry` `meta.og.image` (or `update_page`
|
|
312
|
+
for a page), or from `generateMetadata`. Rules: 1200×630, an absolute URL, a 404 (never a blank image) for a
|
|
313
|
+
missing slug, and strip stega from any preview string first ([bindings.md](bindings.md) §5).
|
|
314
|
+
A hosted site gets its OG image from the native value; this route is for self-hosted Next only.
|
|
315
|
+
|
|
316
|
+
## SEO and AEO gate
|
|
317
|
+
|
|
318
|
+
Run before publishing or reporting done, then run `list_seo_issues` and `get_next_steps` and fix
|
|
319
|
+
what they list. The accessibility half of the gate is the check list in
|
|
320
|
+
[accessibility.md](accessibility.md).
|
|
321
|
+
|
|
322
|
+
**SEO**
|
|
323
|
+
|
|
324
|
+
- [ ] No model, page or component carries a meta, OG, canonical, noindex, hreflang or schema field.
|
|
325
|
+
- [ ] Every indexable page and entry has a `metaTitle` of at most 60 characters and a
|
|
326
|
+
`metaDescription` (aim 120–160, never over 160). No two titles match.
|
|
327
|
+
- [ ] Source canonicals that point elsewhere are kept; self canonicals are not stored.
|
|
328
|
+
- [ ] Source noindex pages are `noindex`, and so are search, cart, checkout and thank-you pages.
|
|
329
|
+
- [ ] Each language is its own row with `locale` and `translationOf`.
|
|
330
|
+
- [ ] Every changed URL has a 301 to its final URL; the source's redirects are imported; no chain,
|
|
331
|
+
no loop, no batch pointed at the home page.
|
|
332
|
+
- [ ] `seoDefaults`, `siteSchema` and `collectionSchemaTypes` are set.
|
|
333
|
+
- [ ] Sitemap and robots are `auto`, or `custom` carrying the source's rules.
|
|
334
|
+
- [ ] Native values and the fetched HTML agree on representative static and dynamic routes.
|
|
335
|
+
|
|
336
|
+
**Structured data**
|
|
337
|
+
|
|
338
|
+
- [ ] Each page is on Automatic unless a type is missing; then `schemaType`; pasted `schema` last.
|
|
339
|
+
- [ ] The emitted JSON-LD matches the source's for type and key values, and validates.
|
|
340
|
+
- [ ] Every value is visible on the page.
|
|
341
|
+
- [ ] One FAQPage per URL. No self-serving `AggregateRating`.
|
|
342
|
+
- [ ] Pasted JSON-LD parses and has its type's required properties.
|
|
343
|
+
|
|
344
|
+
**AEO**
|
|
345
|
+
|
|
346
|
+
- [ ] Specs, prices, hours, menus and chart data are text in fields or tables.
|
|
347
|
+
- [ ] Names are consistent and come from referenced entries.
|
|
348
|
+
- [ ] `llms.txt` has a site summary and Markdown twins are on.
|
|
349
|
+
- [ ] Videos and podcasts have a transcript, or the gap is reported.
|
|
350
|
+
- [ ] The source's AI-crawler policy is carried over unchanged.
|
|
351
|
+
|
|
352
|
+
**Report**
|
|
353
|
+
|
|
354
|
+
- [ ] OG and Twitter previews use valid media and route-specific copy.
|
|
355
|
+
- [ ] Internal links and redirects resolve; staging and private pages stay out of the index.
|
|
356
|
+
- [ ] Fixed technical issues, content recommendations and unverified search outcomes are listed
|
|
357
|
+
separately, with every unsupported directive, missing transcript and page that should be gone.
|