@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.
- package/README.md +1 -0
- package/SKILL.md +1 -1
- package/dist/index.js +908 -487
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +175 -0
- package/dist/skills/bettercms/references/astro.md +67 -0
- package/dist/skills/bettercms/references/bindings.md +201 -0
- package/dist/skills/bettercms/references/componentize.md +106 -0
- package/dist/skills/bettercms/references/components.md +228 -0
- package/dist/skills/bettercms/references/content-experiments.md +62 -0
- package/dist/skills/bettercms/references/content-modeling.md +185 -0
- package/dist/skills/bettercms/references/editor-chrome.md +88 -0
- package/dist/skills/bettercms/references/external-content.md +78 -0
- package/dist/skills/bettercms/references/finish-site.md +123 -0
- package/dist/skills/bettercms/references/forms-and-interactions.md +203 -0
- package/dist/skills/bettercms/references/frameworks.md +149 -0
- package/dist/skills/bettercms/references/getting-started.md +106 -0
- package/dist/skills/bettercms/references/localization.md +81 -0
- package/dist/skills/bettercms/references/media-and-brand.md +142 -0
- package/dist/skills/bettercms/references/next.md +93 -0
- package/dist/skills/bettercms/references/other-frameworks.md +66 -0
- package/dist/skills/bettercms/references/portable-text.md +206 -0
- package/dist/skills/bettercms/references/programmatic-seo.md +98 -0
- package/dist/skills/bettercms/references/publishing.md +146 -0
- package/dist/skills/bettercms/references/seo.md +156 -0
- package/dist/skills/bettercms/references/shopify-headless.md +52 -0
- package/dist/skills/bettercms/references/site-blueprints.md +250 -0
- package/dist/skills/bettercms/references/site-review.md +73 -0
- package/dist/skills/bettercms/references/write-limits.md +84 -0
- 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.
|