Skip to content

Public v2 API

The v2 surface (/public/v2) is the worker-optimized read API consumed by the cms-cf-worker (Cloudflare Worker). It was designed in AISD-115 and built in AISD-116. It is mounted alongside v1 (/public/v1), which stays in place during rollout and is retired once v2 is live in production.

  • Routes: src/cms_api/public/routes/v2/ -- one file per endpoint, aggregated onto public_v2_app in routes/v2/__init__.py and mounted in main.py behind validate_worker_api_key.
  • Schemas: src/cms_api/public/schemas/v2.py -- every response model sets extra="forbid", so any drift between the store output and the declared schema fails fast in tests rather than silently shipping an unexpected field.
  • Auth: the whole block requires the worker API key (see Auth flow).

Endpoints

Every DB-backed endpoint emits ETag, Last-Modified, and Cache-Control, and honors If-None-Match / If-Modified-Since with a 304 (see Request lifecycle).

# Method + path Purpose s-maxage / swr
1 GET /public/v2/ping Liveness; no DB, no caching. Returns {"status": "ok", "time": <iso>}. --
2 GET /public/v2/sites/{site}/settings Site identity + merged settings + light categories/tags + resolved templates + active_template + ads_txt/robots_txt. 300 / 86400
3 GET /public/v2/sites/{site_id}/pages/{slug} One article + flat SEO + author + breadcrumbs + related. ?version=N reads content_history (preview-gated). 300 / 600
4 GET /public/v2/sites/{site_id}/authors/{slug} Author profile + computed SEO + article stats. 300 / 600
5 GET /public/v2/sites/{site_id}/lists?kind=... Metadata-only content cards, cursor-paginated. 120 / 600
6 GET /public/v2/sites/{site_id}/sitemap Flagged content URLs + static-page URLs, cursor-paginated. 3600 / 3600

Parameters by endpoint

/settings

Param In Notes
site path Site id, acronym, or domain (see the resolver).
language query Optional language filter.

Returns a SettingsBundle: site (SiteIdentity), site_settings (SiteSettingsBlob -- per-column merge of site over global defaults, with light categories/tags, templates resolved by precedence override > block > element default, and active_template), and meta.

/pages/{slug}

Param In Notes
site_id path Resolver input.
slug path Article slug.
language query Optional; a slug published in another language is skipped, not 404'd.
version query, >=1 Reads a content_history snapshot; preview-gated.
related_limit query, 0..50 Related-article count (default 6).
X-CMS-Preview header Shared secret required when version is set.

Only content_type in ARTICLE_CONTENT_TYPES (currently ("homepage_article",)) is served as an article. A versioned read keeps the live row's identity columns and overlays only the snapshot's mutable business columns (the HISTORY_BUSINESS_FIELDS merge policy in v2_common). Returns an ArticlePage.

/authors/{slug}

Param In Notes
site_id path Resolver input.
slug path Author slug (scoped to the site, active only).

Returns an AuthorProfile: the author slice, a computed Person SEO block, and stats (article_count, latest_published_at).

/lists

Param In Notes
site_id path Resolver input.
kind query, required One of homepage, category, author, featured, trending, related, recent, search.
category query Required when kind=category.
author_slug query Required when kind=author.
related_to query Required when kind=related.
q query Required when kind=search (LIKE substring on title + description).
language query Optional language filter.
limit query, 1..100 Page size (default 20).
cursor query Opaque cursor; response carries next_cursor.

A missing required parameter for the chosen kind, or an unknown kind, raises a 400 validation_error. Returns a ListResponse of metadata-only ListItem cards.

/sitemap

Param In Notes
site_id path Resolver input.
since query RFC1123 or ISO date; only content published at/after it.
limit query, 1..50000 Page size (default 5000).
cursor query Opaque cursor. Static-page entries are emitted only on the first page (cursor unset).

Includes content flagged is_featured OR show_in_homepage, plus static-page URLs derived from site_settings.navigation whose path maps to a type='page' template element (the homepage / is always included). Returns a SitemapResponse.

The site resolver

All site-keyed v2 endpoints resolve the {site} / {site_id} path parameter through resolve_site_any: it tries int(site) against sites.id first, then falls back to the v1 acronym OR domain predicate. An inactive or unknown site yields a 404 resource_not_found.

Schema convention

Response shapes are declared in cms_api.public.schemas.v2 with model_config = ConfigDict(extra="forbid") on every model. Because the store populates these models directly, an extra or renamed field surfaces as a validation error in tests rather than silently shipping. See API reference / Public v2 schemas.

Recorded deviations

These were recorded against the AISD-115 design at implementation time:

  • Sync, not async. The design's asyncio.gather / async-SQLAlchemy language does not apply: the store is synchronous (session_scope() + session.query), so the v2 methods are synchronous def, mirroring v1.
  • feature_flags is an empty object. The current site_settings schema has no feature_flags column; the field is returned as {} until one exists.
  • meta.etag in the body is null. The authoritative ETag is the HTTP ETag header; computing it into the body would be self-referential.
  • search is a LIKE substring match, not MySQL MATCH ... AGAINST -- no fulltext index is declared in hq.sql_models.