Docs
Dashboard

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 todescription falls back toog_image falls back to
Blog articlethe article titlethe excerpt, then the opening of the bodythe featured image
Blog categorythe namethe descriptionthe featured image
KB articlethe titlethe summary, then the opening of the bodythe featured image
KB collectionthe titlethe descriptionthe collection image
Authorthe namethe opening of the biothe 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} />}

The share version

og_image is not the original file. It is a copy made for link previews: 1200 by 630, cropped to the centre, JPEG, under 300 KB, with its exact width and height. A full-size PNG exported from a design tool can run to several megabytes, and at that size WhatsApp shows no preview at all and LinkedIn gives up fetching. The share version is what those crawlers want, so put it straight into og:image and twitter:image.

The original is untouched. featured_image, image and avatar still point at the full-resolution file, and its URL never changes, so anything you have already published in RSS or JSON-LD keeps working. Use the original for the hero on the page and og_image for the card.

The copy is made a few seconds after an upload. Until it exists, og_image serves the original, so a page never loses its picture. Images uploaded before this existed get theirs the first time a page using them is read, and a background pass covers the rest.

Every image object also carries the same copy under an optional og key, so a custom content type’s image can be shared the same way:

"image": {
  "url": "https://cdn.example.com/media/hero.png",
  "width": 2400,
  "height": 1350,
  "content_type": "image/png",
  "og": {
    "url": "https://cdn.example.com/media/og-1200x630.jpg",
    "width": 1200,
    "height": 630,
    "content_type": "image/jpeg"
  }
}

og_image follows the project’s access setting like every other image, 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, Sunday, September 20, 2026