@bettercms-ai/mcp 0.63.2 → 0.64.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.
Files changed (30) hide show
  1. package/README.md +1 -0
  2. package/SKILL.md +1 -1
  3. package/dist/index.js +908 -487
  4. package/dist/index.js.map +1 -1
  5. package/dist/skills/bettercms/SKILL.md +175 -0
  6. package/dist/skills/bettercms/references/astro.md +67 -0
  7. package/dist/skills/bettercms/references/bindings.md +201 -0
  8. package/dist/skills/bettercms/references/componentize.md +106 -0
  9. package/dist/skills/bettercms/references/components.md +228 -0
  10. package/dist/skills/bettercms/references/content-experiments.md +62 -0
  11. package/dist/skills/bettercms/references/content-modeling.md +185 -0
  12. package/dist/skills/bettercms/references/editor-chrome.md +88 -0
  13. package/dist/skills/bettercms/references/external-content.md +78 -0
  14. package/dist/skills/bettercms/references/finish-site.md +123 -0
  15. package/dist/skills/bettercms/references/forms-and-interactions.md +203 -0
  16. package/dist/skills/bettercms/references/frameworks.md +149 -0
  17. package/dist/skills/bettercms/references/getting-started.md +106 -0
  18. package/dist/skills/bettercms/references/localization.md +81 -0
  19. package/dist/skills/bettercms/references/media-and-brand.md +142 -0
  20. package/dist/skills/bettercms/references/next.md +93 -0
  21. package/dist/skills/bettercms/references/other-frameworks.md +66 -0
  22. package/dist/skills/bettercms/references/portable-text.md +206 -0
  23. package/dist/skills/bettercms/references/programmatic-seo.md +98 -0
  24. package/dist/skills/bettercms/references/publishing.md +146 -0
  25. package/dist/skills/bettercms/references/seo.md +156 -0
  26. package/dist/skills/bettercms/references/shopify-headless.md +52 -0
  27. package/dist/skills/bettercms/references/site-blueprints.md +250 -0
  28. package/dist/skills/bettercms/references/site-review.md +73 -0
  29. package/dist/skills/bettercms/references/write-limits.md +84 -0
  30. package/package.json +2 -2
@@ -0,0 +1,98 @@
1
+ ---
2
+ title: "Programmatic SEO"
3
+ description: "Page families from data: deciding a family should exist, generating from a dataset through a reviewed job, and the indexability gate."
4
+ ---
5
+
6
+ # Programmatic SEO: page families from data
7
+
8
+ **Outcome.** A maintainable family of genuinely useful pages, each with a clear search intent
9
+ and verifiable content, without flooding the index with near-duplicates.
10
+
11
+ This supplements [site-blueprints.md](site-blueprints.md) (route and model shape) and
12
+ [seo.md](seo.md) (native meta, structured data, sitemap). It never grants permission to publish a large batch.
13
+
14
+ ## 1. Decide whether a family should exist
15
+
16
+ Build an intent matrix before a model. For each family record audience, intent, the unique
17
+ answer, data source, owner, update cadence, expected URL count and one real example.
18
+
19
+ | Family | A good reason for a distinct page | Often a poor reason |
20
+ |---|---|---|
21
+ | Glossary term | Precise definition, examples, related concepts, source and review owner | A reworded dictionary sentence per keyword |
22
+ | Solution × industry | Different workflow, constraints, capability and proof | The same template with the industry name swapped |
23
+ | Integration | A shipped connector, setup, limitations, status | An unshipped logo or speculative partnership |
24
+ | Comparison | Fair, current criteria and evidence | Generated competitor assertions |
25
+ | Location | Real local presence, service terms, team or availability | A city name substituted into generic copy |
26
+
27
+ If a candidate has no unique value, keep it unpublished or fold it into a stronger parent.
28
+ Google's [spam policies](https://developers.google.com/search/docs/essentials/spam-policies)
29
+ name large volumes of unoriginal pages made for rankings as scaled content abuse, and its
30
+ [AI-search guidance](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide)
31
+ warns against a page for every phrasing of a query.
32
+
33
+ ## 2. Model once, generate selectively
34
+
35
+ 1. **One entity collection per durable subject** (`GlossaryTerm`, `Integration`, `Industry`)
36
+ with one dynamic detail template. The collection's `urlPattern` (for example
37
+ `/glossary/:slug`) is its entry URL, set with `create_content_model` or
38
+ `update_content_model`. It must contain `:slug` exactly once, and when an entry's slug
39
+ changes the platform adds the 301 from the old URL. Connect subjects with references so each
40
+ fact lives once.
41
+ 2. **Cross-product pages need an approved pair record.** Never generate the Cartesian product of
42
+ two collections. The pair record holds the pair-specific insight, evidence, related entities,
43
+ owner, last review date and status.
44
+ 3. **Fields by editorial responsibility:** title and slug, a concise answer, original body or
45
+ sections, facts and sources, related links, imagery with alt text, and review state. Meta is
46
+ native ([seo.md](seo.md)), never a model field. Shape rules: [content-modeling.md](content-modeling.md).
47
+ 4. **Routes from stable ids and slugs.** Check case, punctuation, locale, reserved names,
48
+ collisions with static routes and previously published slugs. Any other moved URL gets a
49
+ redirect with `create_redirect`. A slug is an address, not a keyword field.
50
+ 5. **Everything stays draft until reviewed.** Record provenance for data and AI-assisted copy.
51
+ Never invent sources, testimonials, integrations or capabilities.
52
+
53
+ ### Generating from a dataset
54
+
55
+ The `generate_landing_pages` prompt is the guided flow for this step (one template plus a
56
+ dataset; for per-account pages it hands over to the `bettercms-abm` skill's order).
57
+ `generate_pages_from_dataset` turns rows into pages from a template page, a mapping (model, a
58
+ slug template such as `for-{{term}}`, and per-field values that are a column or an AI prompt)
59
+ and the rows. Always run it with `dryRun: true` first and show the user the sample: the dry run
60
+ writes and charges nothing. A real run waits for approval, and only the user's yes releases it
61
+ through `approve_ai_job`; never approve your own plan.
62
+
63
+ ## 3. Indexability gate
64
+
65
+ Decide `publish`, `publish-noindex`, `merge/redirect` or `omit` for every candidate. An
66
+ indexable page needs:
67
+
68
+ - a working 200 route with substantial distinct value for its intent;
69
+ - a stable canonical that matches the internal-link and sitemap form;
70
+ - visible facts consistent with the title, description and any structured data;
71
+ - no conflict with a stronger parent, a duplicate, a locale variant or a filtered URL;
72
+ - a path from relevant hubs and child pages, not only the XML sitemap;
73
+ - an accountable owner and review date when facts can change.
74
+
75
+ A noindex page is left out of the hosted sitemap; keep drafts, empty records, duplicate
76
+ combinations, internal search and facet URLs out too. `robots.txt` does not remove a page from
77
+ the index, and a blocked URL does not belong in the sitemap.
78
+
79
+ ## 4. Navigation, sitemaps and scale
80
+
81
+ - Give each family a readable hub (`/glossary/`, `/integrations/`) with crawlable `<a href>`
82
+ links to its children. A sitemap is an inventory aid, not navigation.
83
+ - Split sitemap files before Google's 50,000 URL / 50 MB limit: [sitemap guide](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap).
84
+ - Keep one path pattern per family, and do not move an established path to fit it. Treat
85
+ locales as explicit equivalents: [URL guidance](https://developers.google.com/search/docs/crawling-indexing/url-structure).
86
+ - At volume, generate inventory and validation reports with deterministic code. Use the MCP for
87
+ project state, model setup, samples and targeted repairs. Dry-run creates, updates and
88
+ deletes, confirm exact counts, batch with resumable ids, and reconcile later changes without
89
+ overwriting editor edits ([publishing.md](publishing.md) §5).
90
+
91
+ ## Acceptance and reporting
92
+
93
+ Sample one page per family plus edge cases: shortest and longest record, no image, no evidence,
94
+ duplicate slug, archived entry, locale variant, empty hub. Verify edit → preview → public HTML,
95
+ head and JSON-LD, indexability, internal links, sitemap inclusion and redirects.
96
+
97
+ Report candidates, published and indexable, published noindex, merged, omitted, broken, and
98
+ pending review. A successful bulk write alone never completes the family.
@@ -0,0 +1,146 @@
1
+ ---
2
+ title: "Publishing and sync"
3
+ description: "Publish order, delivery lanes, execute and verify, rollback, reconciling a re-import or continuous sync, and the automation surfaces that exist."
4
+ ---
5
+
6
+ # Publishing, release, rollback and sync reconciliation
7
+
8
+ **Outcome.** The intended revision reaches the right public site with its dependencies, routes,
9
+ forms and cache state checked, and a new source revision lands without overwriting editorial
10
+ work. A dashboard success is not evidence that a public route works.
11
+
12
+ Order of operations and why a draft renders nothing: §7 of `bettercms://playbook/schema`.
13
+ Component refusal codes: [components.md](components.md) §4. Hosting modes, the SSR switch, env
14
+ names and revalidation: [frameworks.md](frameworks.md).
15
+
16
+ ## 1. Plan the dependency graph
17
+
18
+ Confirm the project and its hosting mode from `get_project`, never from the repo name (`hybrid`
19
+ is handled exactly like `managed`). Record the source commit, content revision, live baseline,
20
+ release target and rollback point.
21
+
22
+ Publish in dependency order: component definitions (and nested components), Form documents, the
23
+ global navigation and footer, the Global Layout, pages and entries, SEO, then the frontend code
24
+ that reads them.
25
+
26
+ - **Chrome**: components first, then `publish_layout`; the order and the logo refusals are in
27
+ [components.md](components.md) §4.
28
+ - **Forms**: test the destination and consent behaviour, then `publish_form`; a draft form answers
29
+ 403 to every public submission.
30
+ - **Pages and entries are two rows.** Publishing a singleton page publishes its entry too, as the
31
+ dashboard's publish button does, unless the entry's own gate refuses it (its workflow stage, or
32
+ the publish permission). `ENTRY_STILL_DRAFT` means the page went live over content that did
33
+ not, and names the entry and why; `ENTRY_SEED_FAILED` on `create_page` means none of its seed
34
+ data was saved.
35
+ - Publishing copies the stored draft to live, so what you checked in the draft is what ships.
36
+ - **Many at once**: `publish_entries` { action: 'publish' | 'unpublish', entryIds, pageIds } takes
37
+ up to 100 of each in one call (pages publish only). Each item still passes its own gates, and
38
+ nothing stops the run: read `failed` for every refusal and its reason.
39
+ - **Throw away a draft**: `discard_draft` with `entryId` or `pageId` returns it to exactly what is
40
+ live; a page also drops its unpublished Layout overrides (`no_published_version` if it never
41
+ went live: delete it instead).
42
+ - **Edit a page safely**: read `get_page` and send its `optimisticVersion` as `ifMatch` on
43
+ `update_page`; a 409 means someone changed it since, so read again. `blockJson` replaces the
44
+ whole composition, so an unguarded write can erase a concurrent edit.
45
+
46
+ ## 2. Choose the delivery lane
47
+
48
+ | Hosting mode | Release path | Public proof |
49
+ |---|---|---|
50
+ | `managed` or `hybrid` | `deploy_project` or `create_deploy_upload`, then `get_deploy_status` | Cache-busted BetterCMS URL answers 200 with the intended revision |
51
+ | `headless` | Publish content in BetterCMS; the frontend deploys through its own host and rebuild or revalidation | The external host's URL answers 200 with the intended content and metadata |
52
+
53
+ A managed site renders static unless the owner switches Hosting to **Run as a server app**, which
54
+ must happen before an SSR build ships ([frameworks.md](frameworks.md)).
55
+
56
+ ### Deploy to Vercel or Netlify
57
+
58
+ To serve a `managed` or `hybrid` site from the owner's own Vercel or Netlify account, call
59
+ `list_hosting_connections` first; with no working connection for that host, hand the user
60
+ `connectUrl` to connect it in the browser and wait. Then `deploy_to_host` with the `provider`:
61
+ BetterCMS builds, the host serves, and calling it again redeploys the same site. Relay any
62
+ refusal's `fixUrl` to the user. Poll `get_host_deploy_status` until the newest deployment is live
63
+ and verify its `providerUrl`. Forms post to the BetterCMS API, so they need no rewrite, and no host
64
+ CLI or pasted host token is ever needed while a connection exists.
65
+
66
+ ## 3. Execute and verify
67
+
68
+ 1. After an authorized release, record the operation ids and revisions from each call.
69
+ 2. On a refusal, stop: resolve the cause its code names, then retry only with changed input.
70
+ 3. Fetch each touched route directly, cache-busted: status, the copy you published, component
71
+ overrides, media URLs, metadata and forms. Include one collection detail route, the global
72
+ navigation and footer, and mobile interaction when affected.
73
+ 4. Run the verify loop in SKILL.md, noting each report's freshness.
74
+ 5. On a cached site, confirm the publish webhook or rebuild actually refreshed the public page.
75
+
76
+ ## 4. Roll back
77
+
78
+ Use the known prior content revision and source commit. For an entry, `list_entry_versions` then
79
+ `restore_entry_version`: it writes that version back as the current draft and adds a new version,
80
+ so publish it afterwards. Unpublished edits you do not want are `discard_draft`, not a restore.
81
+ Restore only the affected lane, recheck the live URLs, and never overwrite newer unrelated edits.
82
+
83
+ ## 4b. Stage a change on a branch
84
+
85
+ A content branch is a copy of the project's content to edit without touching the live site.
86
+ `create_branch` { name } returns it; pass its id as `branchId` on every call made inside it
87
+ (`list_branches` names the one you are on). To merge, `apply_branch` { branchId, dryRun: true }
88
+ returns the diff and its conflicts: show it to the user, then apply, with `include` to take only
89
+ some changes and `fields` to settle each conflicting field (`keep_branch` / `keep_parent`).
90
+
91
+ ## 5. Reconcile a re-import, a push or a continuous sync
92
+
93
+ Identify the previous source snapshot or commit, the new one, the import manifest, the
94
+ source-to-target id map and the current BetterCMS state. `list_activity` with `since` reads the
95
+ project's durable change feed from an ISO timestamp (up to 100 events per call): call it with the
96
+ time of the previous sync to see every dashboard or agent edit made since. Compare by stable identity, never
97
+ by display name or row order, and confirm it is the same project and source branch. Without a
98
+ manifest or id map this is no longer an automatic sync; treat it as a reviewed migration (the
99
+ `bettercms-migrate` skill).
100
+
101
+ Classify each difference and keep a three-way view where you can (the value last imported, the
102
+ current source value, the current CMS value). Never flatten a person's CMS edit back to imported
103
+ code because the file was touched.
104
+
105
+ | Difference | Resolution |
106
+ |---|---|
107
+ | New in source | Import with the normal model and component rules; file into the existing Content sidebar folders |
108
+ | Changed only in source | Update fields that are still code-owned or explicitly synced; CMS-owned values stay |
109
+ | Changed only in CMS | Keep |
110
+ | Changed on both sides | Report both values with provenance; apply a winner only after the user chooses |
111
+ | Missing in source | An orphan, never an automatic delete. Tell removed from moved or renamed; offer archive / keep / relink with the inbound references, routes and redirects each affects |
112
+ | Route or slug change | A URL and redirect plan (`create_redirect`); check collisions, canonicals and internal links first |
113
+ | Assets | Match by source id or path plus checksum; a changed binary is a new revision; a missing source asset never breaks a published reference until the user approves its replacement or removal |
114
+
115
+ Dry-run the proposed operations, then run the same snapshot again to prove it is idempotent (no
116
+ new changes the second time). Apply only approved, supported changes, then compare counts,
117
+ references, component usage, field bindings, preview and the affected public routes. The rollback
118
+ for a content value the sync got wrong is §4.
119
+
120
+ ## 6. Automation surfaces
121
+
122
+ "Run X when content changes" maps onto what exists; there is no user-deployed function runtime.
123
+
124
+ | Need | Surface | Owner |
125
+ |---|---|---|
126
+ | Rebuild or revalidate a site on publish | The publish webhook, signed `X-BetterCMS-Signature` (HMAC-SHA256 of the raw body) with the project's webhook secret ([frameworks.md](frameworks.md) Revalidation) | Secret set in the dashboard; your route verifies it |
127
+ | Notify another system of an event | Event webhooks, `list_webhooks` / `create_webhook`: `content.publish`, `content.unpublish`, `content.stage_changed` (workflow stage), `content.created`, `content.updated`, `content.deleted` and `deployment.triggered`; HMAC-SHA256 signed in `X-BetterCMS-Signature`; payloads carry ids, never content. The signing `secret` is returned ONCE, on create. A receiver that writes back on `content.updated` loops unless it skips its own writes. Per-subscription payload filters: dashboard | You |
128
+ | Send form submissions somewhere | Per-form integrations: Google Sheets, Slack, a webhook, email | Dashboard only |
129
+ | Go live or retire at a time | `scheduledAt` (publish a draft at a time; needs publish permission) and `unpublishAt` (return a published row to draft) on `update_content_entry` and `update_page`; `null` cancels either. Campaigns: `publish_campaign` takes both | You. An edit that omits `scheduledAt` CANCELS the pending publish, so resend it with every edit before it fires; omitting `unpublishAt` never cancels it |
130
+ | Load many entries | `create_content_entries`: up to 200 drafts of one collection per call, each row `created` or `failed`; then `publish_entries` | You |
131
+ | React to edits made elsewhere | Poll `list_activity` with `since` set to the time of your last read (1–100 events per call) | You |
132
+ | Generate or rewrite in bulk with a human gate | AI jobs: `list_ai_jobs`, `get_ai_job`, then `approve_ai_job` or `reject_ai_job` on the user's word | User approves |
133
+
134
+ Not available, so say so instead of building around it: code that runs inside BetterCMS on a
135
+ content event, and a change subscription over the MCP (poll `list_activity` `since`). Anything
136
+ beyond the table runs in the user's own infrastructure, triggered by a webhook. Why: a timer on the
137
+ delivery API looks like a trigger, but it serves published content only and never sees an edit
138
+ before it ships.
139
+
140
+ ## Report
141
+
142
+ State what is live, what remains draft, whether a rollback ran, and any owner action still
143
+ required (Visual Approval, a render-mode change, a secret). For a sync, list exact created,
144
+ updated, unchanged, orphaned and conflicted ids, the choices still open, and the rollback point;
145
+ an open conflict is never a successful sync. Do not say "launched" until the public response is
146
+ verified.
@@ -0,0 +1,156 @@
1
+ ---
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."
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
+ ## 1. Where each value lives
16
+
17
+ | Value | Writer | Notes |
18
+ |---|---|---|
19
+ | 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 |
21
+ | 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
+ | 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 |
25
+
26
+ Never create `seoTitle`-style fields on a model, page or component: the renderer, the sitemap,
27
+ `llms.txt` and `list_seo_issues` read the native values only. Self-hosted sites own their
28
+ `<head>`, sitemap and robots; the platform renderer emits them only for hosted sites, so confirm
29
+ the frontend emits the native values in its HTML ([next.md](next.md) covers `buildMetadata`).
30
+ There is no hreflang field: translations are native rows (`locale`, `translationOf`). The hosted
31
+ sitemap emits the hreflang alternates from them; a frontend emits the `<head>` links from each
32
+ entry's delivered `translations` ([localization.md](localization.md)).
33
+
34
+ ### Markdown twins
35
+
36
+ Every published page and entry is also served as Markdown for answer engines, at its own URL plus
37
+ `.md` (the home page at `/index.md`; an entry at its parent page's path, then `/<slug>.md`).
38
+
39
+ - The twin is generated from the content, per request. Nothing to model or keep in sync.
40
+ - A noindex page or entry has no twin, and is left out of `llms.txt`.
41
+ - To replace one twin, store a Markdown file at the same path in the dashboard (Pages →
42
+ Markdown). Only a published file serves; deleting it restores the generated twin. A second file
43
+ at the same path is refused (409, code markdown_path_taken).
44
+ - `llms.txt`, `llms-full.txt` and which pages serve a twin are switched there too. Those writes
45
+ need the SEO permission, and no MCP tool writes them.
46
+
47
+ ## 2. Route-level audit
48
+
49
+ Read the route inventory, page and entry records, site defaults, the rendered `<head>`, robots,
50
+ sitemap, structured data and redirects. For each indexable route check:
51
+
52
+ - HTTP status, canonical, title, description, the main heading, language, breadcrumbs,
53
+ internal links, image alt text and the social preview.
54
+ - Page type and structured-data eligibility; schema values must match visible content.
55
+ - Duplicate or near-duplicate routes, pagination, query parameters, redirects, trailing slash.
56
+ - Robots and noindex conflicts, staging or private pages that got indexed, sitemap inclusion.
57
+ - Client-rendered content: does the initial HTML carry the key content?
58
+
59
+ A CMS field holding the right title does not prove the public route emits it. Fetch the HTML.
60
+
61
+ ## 3. Structured data
62
+
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).
71
+
72
+ ## 4. Sitemap, robots, llms.txt
73
+
74
+ - The hosted sitemap leaves out noindex pages, and the generated robots.txt points at the
75
+ sitemap. Confirm drafts, private pages and noindex pages are absent from the live file.
76
+ - A translated page or entry carries `<xhtml:link rel="alternate" hreflang>` for each listed
77
+ sibling (its `locale`, else the project's `localeConfig.defaultLanguage`) plus `x-default` for
78
+ the source row. A noindex or unpublished sibling is left out of the links as well.
79
+ - `sitemapConfig` sets it `auto` or `custom`, the default changefreq and priority and `extraUrls`;
80
+ `robotsConfig` is `auto` or `custom` text; `indexnowConfig` { enabled, excludePatterns } pings
81
+ IndexNow on publish; `searchConfig` { enabled, placeholder, aiEnabled } is site search, which
82
+ `search_content` queries (published content only).
83
+ - `refresh_search_index` costs nothing: `action: 'indexnow'` submits `urls` (this site's own, up
84
+ to 1000) or, without them, every change since the last submission; `action: 'reindex'` rebuilds
85
+ site search after bulk changes. A 503 means the index is unreachable: retry later.
86
+ - `llms.txt` lists one link per published item. Maintain it for its intended readers, but do not
87
+ sell it as a ranking requirement: Google's [guidance for AI features](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide)
88
+ says ordinary SEO fundamentals are the base.
89
+ - Check each redirect's real status and target, and detect loops.
90
+
91
+ ## 5. AEO and content quality
92
+
93
+ Improve the page for people first: clear questions and answers where the topic warrants them,
94
+ descriptive headings, first-hand evidence, dates and named authors where real, text alongside
95
+ media, consistent internal links and accurate structured data. No "AI-only" pages, artificial
96
+ snippets or unverifiable claims.
97
+
98
+ Report concrete findings ("canonical points to a 404", "OG image missing", "FAQ schema does not
99
+ match the visible answers"), never an untraceable score.
100
+
101
+ ## 6. Publish and verify
102
+
103
+ After an authorized change ([publishing.md](publishing.md)): inspect the rendered `<head>` and JSON-LD,
104
+ route status, sitemap and robots output, and redirects. Use Search Console or similar only to
105
+ report observed behaviour after launch, never to predict indexing.
106
+
107
+ Validators, run against the live URL (they read the public HTML, never the CMS):
108
+
109
+ | Checks | Tool |
110
+ |---|---|
111
+ | Open Graph tags as Facebook reads them; refreshes its cache | [Sharing Debugger](https://developers.facebook.com/tools/debug/) |
112
+ | LinkedIn card | [Post Inspector](https://www.linkedin.com/post-inspector/) |
113
+ | Any OG card | [opengraph.ing](https://opengraph.ing/) |
114
+ | JSON-LD eligibility | [Rich Results Test](https://search.google.com/test/rich-results) |
115
+ | JSON-LD syntax against schema.org | [Schema Markup Validator](https://validator.schema.org/) |
116
+ | Sitemap XML | [XML Sitemaps validator](https://www.xml-sitemaps.com/validate-xml-sitemap.html) |
117
+
118
+ X (Twitter) has no working public card preview: confirm the `twitter:*` tags in the fetched HTML.
119
+
120
+ ### A generated OG image on a self-hosted Next site
121
+
122
+ When pages have no designed share image, render one per route instead of modelling an image
123
+ field for it. The image reads the entry's own title, so it follows every content edit:
124
+
125
+ ```tsx
126
+ // app/og/[slug]/route.tsx
127
+ import { ImageResponse } from "next/og";
128
+ import { cms } from "@/lib/cms"; // createBetterCMS(...)
129
+
130
+ export async function GET(_req: Request, { params }: { params: Promise<{ slug: string }> }) {
131
+ const { slug } = await params;
132
+ const entry = await cms.getEntry<{ title: string }>(slug);
133
+ if (!entry) return new Response("Not found", { status: 404 });
134
+ return new ImageResponse(
135
+ <div style={{ display: "flex", width: "100%", height: "100%", padding: 64, fontSize: 64 }}>
136
+ {entry.fields.title}
137
+ </div>,
138
+ { width: 1200, height: 630 },
139
+ );
140
+ }
141
+ ```
142
+
143
+ Point the route's OG image at it with `update_content_entry` `meta.og.image` (or `update_page`
144
+ for a page), or from `generateMetadata`. Rules: 1200×630, an absolute URL, a 404 (never a blank image) for a
145
+ missing slug, and strip stega from any preview string first ([bindings.md](bindings.md) §5).
146
+ A hosted site gets its OG image from the native value; this route is for self-hosted Next only.
147
+
148
+ ## Acceptance
149
+
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.
@@ -0,0 +1,52 @@
1
+ ---
2
+ title: "Headless Shopify"
3
+ description: "Shopify owns commerce, BetterCMS the editorial: ownership split, product references, cart, freshness and SEO."
4
+ ---
5
+
6
+ # Headless Shopify with BetterCMS
7
+
8
+ **Outcome.** Let editors shape the storefront in BetterCMS while Shopify remains authoritative for saleable products and the buyer transaction. The integration may use Hydrogen/Oxygen or another frontend; inspect the existing stack before choosing code paths.
9
+
10
+ This complements the [large ecommerce blueprint](site-blueprints.md#large-ecommerce-website), [bindings](bindings.md) and [publishing](publishing.md). Do not assume a BetterCMS Shopify connector or native product picker exists; unless the connected project's tools show one, model product links as validated external ids (below).
11
+
12
+ ## Ownership contract
13
+
14
+ | Concern | Default owner | BetterCMS treatment |
15
+ |---|---|---|
16
+ | Products, variants, SKUs, price, availability, collections, markets | Shopify | Stable references and optional editorial overlays |
17
+ | Cart, discounts, checkout, orders, customer identity | Shopify | Functional UI and handoff; no copy of private transaction records |
18
+ | Campaign pages, category stories, buying guides, banners, content sections | BetterCMS | Pages, collections, global components, instance overrides |
19
+ | Brand tokens, page composition, nav/footer copy, SEO editorial fields | BetterCMS or frontend design system as established | Bind to rendered leaves; verify publication |
20
+ | Product SEO facts and structured data | Rendered commerce data plus editorial fields | Merge with an explicit field precedence and current price/availability |
21
+
22
+ Before writing, identify Shopify store and sales channel, Storefront API version, authentication approach, existing frontend, checkout/account flow, market and currency behavior, product handle policy, BetterCMS project/hosting mode, and where current canonical/SEO metadata comes from. Shopify's [Storefront API](https://shopify.dev/docs/api/storefront/latest) provides public catalog, cart, and checkout operations; it does not turn BetterCMS into an order platform.
23
+
24
+ ## Reference and rendering model
25
+
26
+ Use a stable Shopify product or collection ID as the key in BetterCMS references; store the handle only as a display/routing snapshot. Handles can change. A BetterCMS `ProductEditorial` or `CollectionEditorial` entry can hold hero copy, story sections, buying advice, FAQs, related guides, or campaign placement. A missing editorial overlay must fall back to a valid Shopify-only product or collection page if the merchant intends it to be public.
27
+
28
+ Use one product template and one collection template unless genuine layout families differ. Render price, compare-at price, availability, variant options, and cart eligibility from a current Shopify response. Do not allow an editor field to override these transactional values. Use a product reference picker only if its existence and ID contract are confirmed; otherwise a validated external-ID field plus preview lookup is safer than a fake internal collection.
29
+
30
+ Model global promotion bars, nav/footer, category hero, product detail editorial slots, recommendations, and buying guides as BetterCMS components where the editor can see their effects. Define fallback behavior when an overlay is unpublished, a product is not published to the headless channel, a Shopify query fails, or a referenced product was deleted.
31
+
32
+ ## Buyer flow and privacy
33
+
34
+ The variant selected in the UI must be the exact merchandise ID added to the Shopify cart. Check invalid/expired variants, sold-out changes, quantity limits, discounts, market/currency context, cart persistence, checkout return URLs, and error recovery. Customer accounts and order history use Shopify's current account APIs and authentication, not public BetterCMS content fields. [Shopify cart guidance](https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api/cart) · [Customer Account API](https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api)
35
+
36
+ Keep private Shopify tokens and customer data server-side. Verify the configured token type and exposure boundary rather than treating every Storefront token as a secret. Never put Admin API credentials in browser code.
37
+
38
+ ## Freshness and publication
39
+
40
+ Content and commerce can publish independently. Define which events revalidate a product or collection route: BetterCMS editorial publish, Shopify product/collection change, handle change, inventory/price change, or market change. Decide cache TTL by data volatility and verify the host's invalidation mechanism. Never put a personalized cart, account page, or customer response in a shared cache. Shopify's [Hydrogen caching guide](https://shopify.dev/docs/storefronts/headless/hydrogen/caching) documents the risk for customer-scoped data.
41
+
42
+ Deploy a headless frontend through its actual host (SKILL.md invariant 6). For the on-publish refresh, BetterCMS sends a signed revalidation webhook (header `X-BetterCMS-Signature`) to the URL configured on the project; the Next adapter's revalidate route verifies it ([frameworks.md](frameworks.md) Revalidation). A Shopify webhook or polling job may be needed for catalog freshness; verify subscriptions, idempotent processing, and deleted-product handling before relying on it.
43
+
44
+ ## SEO and sitemap contract
45
+
46
+ Give product and collection routes a single public canonical form. Resolve Shopify handles and BetterCMS editorial slugs without collisions. If variants have selectable URLs, choose one-page versus multi-page variant policy and validate rendered product/variant metadata accordingly. Product JSON-LD must reflect the current purchasable variant, price/currency, and availability; don't publish stale CMS copies. [Google product variant guide](https://developers.google.com/search/docs/appearance/structured-data/product-variants)
47
+
48
+ Generate sitemap entries for eligible, live product and collection routes plus BetterCMS editorial pages, with no duplicate URLs across providers. Exclude cart, checkout, account, internal search, unpublished overlay-only landing pages, and unwanted filter combinations; a live Shopify product can remain eligible without an editorial overlay. Hydrogen has its own [SEO/sitemap/robots routes](https://shopify.dev/docs/storefronts/headless/hydrogen/seo); if using Hydrogen, reconcile them with BetterCMS page URLs rather than exposing two competing sitemap inventories. Handle `/robots.txt` and sitemap at the actual public host.
49
+
50
+ ## Verification matrix
51
+
52
+ Test: product with and without editorial overlay; multiple variants; sold-out and unpublished products; handle rename; missing Shopify ID; empty collection; discount and currency/market change; add-to-cart and checkout; account privacy; CMS preview; CMS publish; Shopify update; stale cache; product removal; canonical, schema, sitemap, and redirects. Verify public HTML and a full purchase flow in a safe test environment. Report which systems were live-tested and which assumptions remain unverified.