Where it runs
The marketing editor
A Pre-publish check section sits above Status in the item’s sidebar, so its findings are above every publish control: a score chip out of 100, errors under Fix before publishing, warnings under Worth a look. Each row names the field and clicking it jumps there (title, slug, meta title, meta description, target keyword, alt text, body). The report is fetched when the item opens and re-run locally on every edit, so it tracks what is being typed.
The publish gate
Every publish entry point in the editor (the header action, Publish changes in the sidebar, the Publish to dialog, the status flip on an unlinked item) goes through one gate: unsaved edits are saved, the server check runs again, and errors open Publish anyway? listing them, with Go back and Publish anyway. Warnings never interrupt, and a check that cannot run never blocks.
The APIs
GET /api/olympus/marketing/content/:contentId/seo-check under an Olympus API key, and its dashboard twin GET /api/marketing/content/{id}/seo-check for a signed-in user with marketing.content read. Same loader, same response; a row outside the account is a 404. From an MCP client the content_seo_check tool calls the Olympus route.The publish route
POST /api/olympus/marketing/content/:contentId/publish-to-site (content_publish_to_site) runs the check on the row as it is about to go out and returns warnings: string[] (one line each, Error (field): ... or Warning (field): ...) plus the structured seo_check. The note tells the department to state each one before treating the publish as done. Unpublish is not checked.What it checks
Two levels. An error is something the site will visibly get wrong: a missing meta description, a body with two H1s, an image with no alt text, a banned phrase, an empty body. A warning is a ranking or editorial miss the writer should look at: keyword placement, heading order, few internal links, a figure with no source.
Four rules behind the table are worth knowing:
- One H1 is the page title. Site templates render the item’s title as the page H1, so a body with no H1 is the normal, correct shape. A body with exactly one H1 is accepted (some imports carry the title inside the body); two or more is an error.
- Markdown and HTML are both read. Rows hold either, and the stored format hint is not reliable (the editor converts markdown to HTML on load and saves the HTML back), so the format is detected from the content and the hint only breaks ties. Code blocks are ignored; image syntax and links are reduced to prose before words are counted.
- Internal is relative to the site. A site-relative path counts as internal, and so does an absolute URL on the linked project’s production host, so a link back to
https://www.example.com/blog/...counts onwww.example.com. Anchors,mailto:andtel:links count as neither. - Proof follows the stage. The item’s
journey_stage(free text, spelled the way the journey map spells it) is classified by keyword into awareness, consideration, decision or retention, andstats.journey_stage_kindsays which. An awareness or retention piece, or one with no stage, gets the ordinary rules; a consideration or decision piece must carry proof under every H2; at the decision stage a figure without a source is an error, because that is the page the reader is deciding on. A source is a link, a bare URL or the[source: <label>:<id>]citation the proof pack hands writers; the marker is stripped from the prose before words are counted and phrases matched, so it never counts as copy.
- The author and the FAQ come from the row.
author_missinglooks atauthor_idand the account’s default author (see Authors);faq_schema_mismatchcompares thefaqfield the FAQPage markup is generated from with the FAQ section the body carries, so the markup never promises questions the page does not answer.answer_block_missingchecks the 40 to 60 word band only, on theanswer_blockfield or the lead paragraph; whether the paragraph restates the query is the writer’s job. - Roles are read, never guessed.
money_link_missingreads the pages markedmoneyandpillar_link_missingthe cluster’s pillar URL, both stored by the site architecture. An account that has marked nothing gets the rule skipped and a note, not a false error. Aurl=on a::ctashortcode counts toward both. - A rival claim needs a source and a date.
competitor_claim_unsourcedreadssettings.comparison_table.rows[].sourceandchecked_at(source_urlandverified_atare accepted spellings), or, for a table in the body, a link, a bare URL or a[source: ...]marker in the row plus a date in the row, the header, a caption or the line right under the table (Prices checked 2026-09-01). - The call to action is placed on a stage table. The verbs and destinations of the built-in five-rung table (unaware, problem aware, solution aware, product aware, most aware, plus retention) are merged with
settings.cta_stage_table([{ stage, verbs?, destinations?, journey_stages? }], wherejourney_stagesnames the journey map’s own stage names for a rung), and the piece’s funnel position allows a set of rungs: awareness allows unaware and problem aware; consideration problem, solution and product aware; decision product and most aware; retention retention and most aware. A link to the offer’scta_urlfits by definition when the offer’sstages[]names the piece’s stage. A form is placed by itsaction(a/newsletterform is an unaware ask, a/bookform most aware); a form with no classifiable action counts forcta_missingand never raisescta_stage_mismatch. See the five-stage CTA table. - Shortcodes are not prose. A
::cta{...}or::upgradeline is taken out of the body before words, claims and answer blocks are counted, so a label such as Save 20% today never raisesclaims_without_source. The check accepts two or three colons anywhere a space precedes them; the site renders only::ctaand::upgradewith two colons on a line of their own, so that is the grammar to write.
The score and the response
ok is true when there are no errors; warnings never flip it. score is 100 minus 15 per error and 5 per warning, floored at 0. checks lists errors first, then warnings, each in the order above.
stats.h2_count and stats.sections_without_proof are counted at every stage, and stats.journey_stage_kind says how the row’s stage was read (null when it has none); the proof_per_section finding itself is raised only for consideration and decision. context is what the check was given, so a client can re-run it locally on edits without another request: the target keyword from the item’s target_keyword column (or settings.target_keyword on a row written before 2026-09-12, which readers fall back to), the forbidden phrases from the account’s default active brand guide (empty when there is none), the linked project’s production host (its active primary production domain, else its custom domain, else its hiveku.com host), the stored format hint, and linked, which is true when the item is bound to a website project and a CMS collection. Since 2026-09-12 it also echoes what the elite rules were given: the row’s journey_stage, content_type, author (null when the account has none), answer_block and faq, the money_page_urls the account has marked, the item’s cluster (id, name, role and pillar URL, or null), the keyword_collision the loader found, the comparison_table rows as { subject, source, checked_at }, the offer named by the item and the account’s cta_stage_table. field can now also be author_id, answer_block or faq. The slug checked is the site entry’s slug once the entry exists, otherwise the library slug.
What it does not do
- It never blocks. The editor asks Publish anyway? on errors, a decision-stage figure without a source included; the publish route publishes and reports. A check that fails to run yields an empty list, not a failed publish.
- It checks the item, not the collection. A
meta_titlethat has no home in the collection (noseoTitleormetaTitlefield) is dropped at publish time and named in the publish response’sunmappedlist. The check cannot see that from the row, so readunmappedas well. - It does not read the live page. Word counts, links and images are those of the stored body. The rendered template, the blog index and the deploy are outside its view.
- Unpublish is not checked. Taking a page down never waits on a meta description.
Related pages
- The Content Department — the writer that states this list before it publishes, and links to real URLs so the internal-link check passes on merit
- Scheduling and the Content Calendar — a scheduled item is checked when it is published, and shows on the calendar
- Use the Marketing Content Review Workflow — the editor the check panel lives in
- SEO Delivery Tools — the page audit and rank tracker that measure the published result
- Authors, the SERP Brief, Positioning and Titles — the author, answer block, FAQ and title the first four elite rules read
- Site Architecture, Bottom-Funnel Pages, the Decision Loop and Conversion — the page roles, clusters, comparison table and stage table the other seven read