Docs
Dashboard

Built-in modules

Blog

Articles, categories and a resolved SEO chain, in a built-in module you configure nothing for.

A built-in module: articles and categories whose schema ZevContent defines, documents and maintains. Nothing is configured first. Every project has these endpoints, and they serve an empty list until somebody writes something.

Bylines come from the shared authors module, not from here.

List articles

GET /v1/blog/articles

Published articles, newest first by published_at. Cursor-paginated. body is omitted, so fetch one article for it.

Parameter
limit1–100, default 20
cursormeta.next_cursor from the previous page
categoryA category slug. Matches an article carrying it anywhere in categories. Unknown → error.
authorAn author slug. Matches anywhere in authors, so a co-authored post appears on both pages. Unknown → error.
tagAn exact tag. No match → empty list.
qCase-insensitive substring of title or excerpt
media_urlspermanent to refuse expiring image URLs

q combines with category and author, which is how a category page or an author page offers search within its own articles without a second endpoint.

One article

GET /v1/blog/articles/{slug}

The whole article, with body as { html, json, plain, toc }, every image resolved, and the same toc beside the article so a sidebar table of contents does not have to reach inside the body.

An unknown slug teaches with did_you_mean; a slug that exists but is a draft says so (blog_article_not_published).

Categories

GET /v1/blog/categories
GET /v1/blog/categories/{slug}

Every category alphabetically, each with article_count, its featured_image and its resolved seo. That is enough to build blog navigation, section cards included, from one request.

A category page is two requests and never more: the page itself, and GET /v1/blog/articles?category={slug} for its articles, which pages and takes q.

The article shape

{
  "id": "art_…",
  "slug": "how-we-deploy",
  "title": "How we deploy",
  "excerpt": "A short tour of the pipeline.",
  "body": { "html": "…", "json": {}, "plain": "…", "toc": [] },
  "toc": [{ "level": 2, "text": "The pipeline", "id": "the-pipeline" }],
  "featured_image": { "url": "…", "alt": "…", "width": 1600, "height": 900, "content_type": "image/jpeg" },
  "authors": [{ "id": "aut_…", "name": "Ada", "slug": "ada", "bio": "…", "avatar": null, "social": {} }],
  "categories": [{ "id": "cat_…", "name": "Engineering", "slug": "engineering", "description": null }],
  "tags": ["deploys", "ci"],
  "published_at": "2026-08-14T09:00:00.000Z",
  "updated_at": "2026-08-20T11:12:00.000Z",
  "reading_time_minutes": 6,
  "word_count": 1180,
  "seo": { "title": "…", "description": "…", "og_image": {}, "canonical_url": null, "noindex": false }
}

body and toc appear on one article only, never in a listing. Twenty full rich text documents, each rendered three ways, is a response measured in megabytes for a page that shows twenty cards.

Arrays, and the first element

authors and categories are ordered arrays, and the first element of each is the primary one:

  • authors[0] is the lead byline: the name to print when there is room for one.
  • categories[0] is the primary section: the breadcrumb, the eyebrow label, the section URL.

The order is the one a writer chose. Render it as given; never re-sort.

Both may be empty, since an unattributed post and an uncategorised one are both ordinary, but neither is ever null, so map over them without a check.

References and pages

Nested inside an article, an author and a category are references: the byline and the breadcrumb. They carry no article_count and no seo, because a card printing twenty bylines will never render twenty og images.

On their own endpoints they are pages and carry those as well. The category page is /v1/blog/categories/{slug}; the author page is /v1/authors/{slug}, which is not under /v1/blog.

Categories and tags are different things

A category is a taxonomy term with a page of its own: a slug, a description, an image, an SEO object, and a URL. A tag is a free-form label with no page. Many per article, matched exactly by ?tag=.

If you find yourself wanting a page for a tag, it should have been a category.

SEO

Every article and category carries a complete, resolved seo object. Do not reimplement the fallback chain. See SEO.

Notes

  • Everything here is read-only and works with any key kind. Articles are written in the dashboard.
  • Only published articles are served. A draft is a 404 that says it is a draft.
  • A future published_at does not publish an article by itself. Scheduling is not built.
  • The body is rich text at schema version 2. An article written before schema 2 keeps "version": 1 and renders exactly as it always did. See Rich text.

Updated at, Wednesday, September 2, 2026