Concepts
SEO
One resolved object on every page-shaped thing, with the fallback chain already applied.
Anything in ZevContent that corresponds to a page carries a complete seo
object: blog articles, blog categories, knowledge base articles, knowledge base
collections, and authors.
"seo": {
"title": "How we deploy",
"description": "A short tour of the pipeline.",
"og_image": {
"url": "https://cdn.example.com/media/deploy.jpg",
"alt": "A deployment pipeline",
"width": 1200,
"height": 630,
"content_type": "image/jpeg"
},
"canonical_url": null,
"noindex": false
}
It always arrives complete and resolved. Render what you are given.
The fallback chains
A writer fills in as much as they care to. The server applies the rest, in the same code path for every module:
title falls back to | description falls back to | og_image falls back to | |
|---|---|---|---|
| Blog article | the article title | the excerpt, then the opening of the body | the featured image |
| Blog category | the name | the description | the featured image |
| KB article | the title | the summary, then the opening of the body | the featured image |
| KB collection | the title | the description | the collection image |
| Author | the name | the opening of the bio | the avatar |
Descriptions derived from a body are cut on a word boundary, not mid-word.
canonical_url and noindex always pass through exactly as set. There is
nothing sensible to derive them from, and guessing a canonical URL is how
duplicate-content problems get created rather than solved.
Rendering it
<title>{seo.title}</title>
{seo.description && <meta name="description" content={seo.description} />}
{seo.canonical_url && <link rel="canonical" href={seo.canonical_url} />}
{seo.noindex && <meta name="robots" content="noindex" />}
<meta property="og:title" content={seo.title} />
{seo.description && <meta property="og:description" content={seo.description} />}
{seo.og_image && <meta property="og:image" content={seo.og_image.url} />}
{seo.og_image?.width && <meta property="og:image:width" content={seo.og_image.width} />}
{seo.og_image?.height && <meta property="og:image:height" content={seo.og_image.height} />}
og_image is the same media object as everywhere else, so on a private
project it is a presigned URL that expires. Social crawlers fetch when someone
shares a link, which may be months after your build. See
Media for why a static build should ask for
media_urls=permanent.
Where seo is not
Nested references do not carry it. An author inside an article’s authors
array is a byline carrying id, name, slug, bio and social, and a category
inside categories is a breadcrumb. Neither has seo or article_count,
because a card printing twenty bylines will never render twenty og images.
Fetch the author’s or category’s own endpoint for the page shape.
Custom content types
seo is a feature of the built-in modules, not of custom types. Your own type
gets the fields you define. If a case-study needs a meta description, add a
string field for it and render it yourself.
We do not synthesise an SEO object for a shape we did not design, because we would have to guess which of your fields is the title.
Updated at, Wednesday, September 2, 2026