Feature: SEO output backend, resolved SEO object, non-persisting check, list status and sitemap As a frontend developer or the RenewCMS back-office editor I want the resolved SEO data of an article, a non-persisting check, an SEO status on the article list and a sitemap per site So that a frontend can render meta tags and a sitemap without reimplementing the fallback chain Background: Given the spec of record is docs/design/seo-slices.md (slice 2), docs/design/change-list.md ("SEO round"), docs/design/screens/editor.md, docs/design/screens/articles.md and docs/design/screens/integration.md And this slice is backend only: no SPA screen and no change to docs/design And it reuses the article and site SEO fields of features/seo_data_backend.feature and adds no column and no migration And RenewCMS renders no page and injects no meta tag: it only returns data And one rule set (a single use-case-level function over the article, its site, its authors and its body) feeds the list seo_status and seo_suggestions, the check endpoint and the suggestion-related parts of the resolver And the suggestion codes are title_long, title_empty, desc_short, desc_long, cover_alt, cover_none, slug_long, no_au, no_h, body_alt and short And the soft ranges are: meta title 30 to 60 characters, description 70 to 160, slug "very long" above 75; they only produce suggestions and never block saving or publishing And codes listed in the article's seo_dismissed are never reported and never counted And the status of an article is "hidden" when noindex is true, otherwise "suggestions" when at least one non-dismissed suggestion remains, otherwise "ready" And a caller is "verified" when the jwt cookie (or an API key) resolves to a user with is_verified true, and "public" otherwise, exactly as for GET /v1/article/{id} And the fallback chain is: title = seo_title, then the article title, with the site title_template applied last; description = seo_description, then excerpt, then the first sentence of the body, then the site default_description; image = social_image_id, then cover_image_id, then the site default_social_image_id; canonical = canonical_url, then site base_url + "/" + slug; robots = "noindex, nofollow" when noindex is true, otherwise from the site default_robots; og and twitter title and description = social_title and social_description, then the resolved values And the sample site 1 is key "renew", base_url "https://www.renew.example", site_name "Renew", title_template "%title% | %site%", default_description "Renovation notes from the Renew team.", default_robots "index", locale "en" and default_social_image_id 7 (path /static/uploadedImages/7.png) And the sample article 12 is online on the site 1, titled "How we price a bathroom renovation", slug "how-we-price-a-bathroom-renovation", created 2026-09-01T08:00:00Z, updated 2026-09-10T09:30:00Z, with no SEO field set and the body "

We price by the square metre. Tiles cost extra.

" And error responses are plain text from http.Error: 404 "Article not found" (apperr.ArticleNotFoundErrorMessage), 404 "site_not_found" (apperr.SiteNotFoundErrorMessage), 400 apperr.IdUint32ErrorMessage for a malformed id, 400 apperr.BodyErrorMessage for a malformed body or paging parameter And request validation goes through validate tags and validator.Validate.Struct And handlers reach articles, sites, authors and images only through use cases and domain interfaces, and no comment is added anywhere Rule: GET /v1/article/{id}/seo returns the resolved object Scenario: The object has the documented shape When an anonymous caller sends GET /v1/article/12/seo Then the status is 200 And the body has exactly the keys title, description, canonical_url, robots, og, twitter, image, authors, published_at, modified_at, fallbacks and json_ld And og has exactly the keys type, title, description, image, image_alt, url, site_name and locale And twitter has exactly the keys card, title, description, image and image_alt And image has exactly the keys url, alt and source And fallbacks has exactly the keys title, description, image and canonical And the body has no body, no id and no seo_dismissed key Scenario: A bare article resolves every output through the fallbacks When an anonymous caller sends GET /v1/article/12/seo Then title is "How we price a bathroom renovation | Renew" And description is "We price by the square metre." And canonical_url is "https://www.renew.example/how-we-price-a-bathroom-renovation" And robots is "index, follow" And image is {"url": "https://www.renew.example/static/uploadedImages/7.png", "alt": "", "source": "site"} And og is {"type": "article", "title": "How we price a bathroom renovation | Renew", "description": "We price by the square metre.", "image": "https://www.renew.example/static/uploadedImages/7.png", "image_alt": "", "url": "https://www.renew.example/how-we-price-a-bathroom-renovation", "site_name": "Renew", "locale": "en"} And twitter is {"card": "summary_large_image", "title": "How we price a bathroom renovation | Renew", "description": "We price by the square metre.", "image": "https://www.renew.example/static/uploadedImages/7.png", "image_alt": ""} And fallbacks is {"title": "title", "description": "body", "image": "site", "canonical": "site"} And published_at is "2026-09-01T08:00:00Z" and modified_at is "2026-09-10T09:30:00Z" Scenario Outline: The title chain and the template Given the article 12 has seo_title "" and the site title_template is "