SEO checks
Deterministic SEO checks and an optional AI review before you publish.
Every post is checked for the things search engines and AI answer engines look for: titles and descriptions, heading structure, links, duplicates, and more. Findings have a stable code, a severity of error, warn, or info, the field they attach to, a plain-language message, and a fix hint. They come back sorted by severity.
Where they show
posts.previewreturns the findings in itsseofield next towarnings, so the existing preview flow sees them for free.posts.seo_check({postId, focusKeyword?, ai?})checks a saved post’s current tip and returns{versionNumber, findings, ai?, warnings?}. REST mirrors it atGET /api/v1/posts/{postId}/seo?focusKeyword=&ai=1.- The editor’s SEO section lists findings grouped by severity, with a focus keyword field saved per browser and one-click jumps to the SEO fields a finding attaches to.
- The publish dialog lists open
errorfindings. It informs, it does not block.
Previewing unsaved Markdown (contentMarkdown instead of postId) skips the checks that attach to saved metadata — title, description, slug, duplicates, canonical — but still runs site context like site-not-indexable alongside the body checks.
Finding codes
Title, description, slug
| Code | Severity | Meaning and fix |
|---|---|---|
title-length |
warn | Effective SEO title is outside 15-60 characters — write a more descriptive title or shorten it. |
description-missing |
warn | No SEO description or excerpt — write a 50-160 character description. |
description-length |
warn | Description is outside 50-160 characters — expand or shorten it. |
slug-long |
warn | Slug is over 75 characters — keep the essential words. |
slug-format |
warn | Slug has uppercase letters, underscores, or a date — use lowercase hyphenated words. |
Headings
| Code | Severity | Meaning and fix |
|---|---|---|
h1-in-body |
warn | Body contains an H1 other than a leading # <post title> — that one is removed before rendering, so demote any other body # headings to ##. |
heading-skip |
warn | A heading level jumps (H2 to H4) — use the next level so the outline stays intact. |
heading-empty |
warn | A heading has no text — fill it or remove it. |
heading-duplicate |
warn | The same heading text appears twice — rename it so each section is unique. |
Links
| Code | Severity | Meaning and fix |
|---|---|---|
link-internal-broken |
error | A root-relative or same-host link points to a slug with no published post — publish or fix the target. |
link-insecure |
warn | A link uses http:// — switch it to https://. |
link-anchor-generic |
warn | Link text like “click here” doesn’t describe the destination — use descriptive text. |
link-none-internal |
info | The post links to no other published post — link at least one related post. |
Content
| Code | Severity | Meaning and fix |
|---|---|---|
content-thin |
warn | Body is under 300 words — add substance or consolidate with a related post. |
paragraph-long |
info | A paragraph runs over 150 words — split it for readability. |
Images and sharing
| Code | Severity | Meaning and fix |
|---|---|---|
image-alt-missing |
warn | An image has no alt text — describe it for accessibility and image search. |
social-image-default |
info | No cover image — pick one, or the generated social card is used. |
Site and duplicates
| Code | Severity | Meaning and fix |
|---|---|---|
duplicate-title |
warn | Another published post uses this title — differentiate it. |
duplicate-description |
warn | Another published post uses this description — write a distinct one. |
canonical-external |
info | The canonical URL points off-site — remove it unless the post is syndicated from there. |
site-not-indexable |
info | The site isn’t indexable (search engines are told to skip it) — upgrade or enable indexing. |
Focus keyword
Pass focusKeyword to posts.seo_check (or fill the editor field) to add five warn findings, one for each place the keyword should appear: keyword-title, keyword-slug, keyword-intro (first 100 words), keyword-heading (a subheading; the title does not count), and keyword-description. Hyphens and underscores in a keyword match like spaces.
AI review
posts.seo_check with ai: true (or the editor’s Ask AI review button) runs one opt-in Workers AI pass over the post. It returns:
intent— the searcher intent the post best servestitleIdeas— two alternative SEO titlesdescriptionIdea— one meta descriptionmissingSubtopics— up to five subtopics a searcher would expectunclearPassages— up to three verbatim quotes and why they confuse or overreachclaimsNeedingSources— up to three verbatim quotes for claims that need a citation
In the editor, Use this puts a title or description idea into the SEO field and saves through the normal autosave — it may fold into the current unpublished tip rather than cut a new version, and a suggestion equal to the current value saves nothing. Nothing goes live until you publish.
The post is sent to the model as untrusted data — instructions inside the post are ignored. Results are cached per post version and model, so checking the same version again is free; changing the focus keyword does not regenerate a cached review.
Limits: hosted free sites get 3 reviews a day, paid sites 100 a day, and self-hosted installs have no cap — they spend their own Workers AI allocation (about 140 reviews a day on the free 10,000-neuron tier). Over the limit returns RATE_LIMIT with retryAfterSeconds. If the deployment has no Workers AI binding, ai: true returns AI_UNAVAILABLE; a failed or unparseable model response returns ai: null with a warning — the deterministic findings still apply.
Agent guidance
Fix error and warn findings before asking for publish approval. error findings are broken promises — a link to a post that doesn’t exist yet; warn findings are things readers and crawlers will notice. info findings are optional improvements.