Modeling SEO and Metadata Fields for Headless Pages

This guide, part of Content Modeling Best Practices, designs the metadata part of a content model: which fields a page needs for search engines and social sharing, where they live, how they fall back to other content, and how the frontend turns them into tags. The rendering side, generating the tags in a framework, is covered in metadata injection.

In a traditional CMS, SEO plugins add metadata fields to every page automatically. In a headless CMS, nothing happens unless the model includes them, and teams often end up with a different set of fields on each content type, named differently each time. The result is inconsistent titles, missing social images and canonical URLs that nobody can set. A single reusable SEO object, embedded in every routable type, fixes that.

Where each tag's value comes fromThe frontend resolves each metadata value from the page's SEO object if set, otherwise from the page's own fields such as title and summary, otherwise from site-wide defaults.SEO objectoptional overridesPage fieldstitle, summary, heroSite settingsname, default imageResolverfirst non-empty<title>, meta,og:*, canonical1st2nd3rd
Editors override only when they need to; everything else comes from content that already exists.

The Problem

An online magazine had three routable types, articles, category pages and landing pages, each added by a different developer. Articles had metaTitle and metaDescription; category pages had seoTitle only; landing pages had an og object with an image but no description. Social previews for category pages used the site logo, landing pages had no meta descriptions and search results showed truncated first paragraphs, and there was no way to set a canonical URL for syndicated articles, which led to the original publisher’s page being outranked by the copies.

How to Model Metadata

Use one embedded object type, seo, on every routable type, with these fields:

  • title: optional override for the <title> element and og:title. Validated to about 60 characters.
  • description: optional override for the meta description and og:description. Validated to about 160 characters.
  • image: optional social image, an asset reference with alt text. Validated for aspect ratio, 1.91:1 for most networks.
  • canonical: optional absolute URL for content that is published elsewhere first.
  • noindex: a boolean for pages that should not appear in search results, such as campaign variants.

Every field is optional, because each has a sensible fallback from the page’s own content: the page title, its summary or first paragraph, and its hero image. Editors set overrides only when the default is not good enough. Required SEO fields tend to be filled with copies of the page title, which adds nothing.

Keep structured data out of the model in most cases. The frontend can generate Article, Product or BreadcrumbList JSON-LD from existing fields more reliably than editors can write it. Model only the facts structured data needs that do not exist elsewhere, such as an FAQ list or an event’s start date.

Metadata fields and their fallbacksFor each tag, the SEO field that overrides it, the page field it falls back to, and the site-wide default used last.TagOverridePage fallbackSite defaulttitleseo.titlepage title + site namesite namemeta descriptionseo.descriptionsummary or first paragraphsite taglineog:imageseo.imagehero imagedefault share imagecanonicalseo.canonicalthe page's own URLnonerobotsseo.noindexindexindex
Only the override column needs editor attention, and usually only for important pages.

Implementation

A resolver function applies the fallbacks, so every route produces metadata the same way. In Next.js, it feeds generateMetadata; other frameworks have equivalent hooks.

TypeScript
// lib/seo.ts
import type { Metadata } from "next";

interface SeoObject { title?: string; description?: string; image?: { url: string; alt?: string }; canonical?: string; noindex?: boolean }
interface PageLike { title: string; summary?: string; heroImage?: { url: string; alt?: string }; path: string; seo?: SeoObject }
interface Site { name: string; baseUrl: string; tagline: string; shareImage: { url: string; alt: string } }

function firstSentences(text: string | undefined, max = 160): string | undefined {
  if (!text) return undefined;
  const clean = text.replace(/\s+/g, " ").trim();
  return clean.length <= max ? clean : clean.slice(0, clean.lastIndexOf(" ", max - 1)) + "…";
}

export function resolveMetadata(page: PageLike, site: Site): Metadata {
  const title = page.seo?.title ?? `${page.title} | ${site.name}`;
  const description = page.seo?.description ?? firstSentences(page.summary) ?? site.tagline;
  const image = page.seo?.image ?? page.heroImage ?? site.shareImage;
  const canonical = page.seo?.canonical ?? new URL(page.path, site.baseUrl).toString();

  return {
    title,
    description,
    alternates: { canonical },
    robots: page.seo?.noindex ? { index: false, follow: true } : undefined,
    openGraph: { title, description, url: canonical, images: [{ url: image.url, alt: image.alt ?? "" }] },
    twitter: { card: "summary_large_image", title, description, images: [image.url] },
  };
}

Each route’s generateMetadata fetches the page, which is usually already cached from the page render, and returns resolveMetadata(page, site). Because the logic lives in one function, a change such as a new title format applies to every type at once.

Validation in the CMS

Add validation and help text to the SEO object once, and every type benefits. Length limits should warn rather than block, because a slightly long title is better than an editor unable to publish. Canonical URLs should be validated as absolute URLs, and the help text should explain that they are for syndicated content only, since a wrong canonical can remove a page from search results. Image validation should check dimensions and require alt text.

Configuration Reference

Field Type Validation Notes
seo.title string warn above 60 characters Falls back to page title with site name.
seo.description text warn below 70, above 160 Falls back to summary.
seo.image asset with alt 1200 × 630 minimum, alt required Falls back to hero image.
seo.canonical URL absolute, https Syndicated content only.
seo.noindex boolean none Campaign variants and thin pages.

Gotchas & Edge Cases

  • Localized metadata. In field-level localized models, localize the SEO object’s text fields, and make sure fallbacks resolve in the same locale as the page, as discussed in the localization strategies guide.
  • Duplicate descriptions. Descriptions generated from a shared intro paragraph repeat across pages. Check for duplicates in CI and ask editors for overrides where they occur.
  • Canonical misuse. Editors sometimes set a canonical to the homepage or a category, which tells search engines to ignore the page. Restrict the field by role, or warn when the canonical points to the same site.
  • Social image caching. Networks cache previews aggressively. Changing the image does not update existing shares; use each network’s debugging tool to refresh important pages.

Worked Example

The magazine replaced its three sets of metadata fields with one SEO object on all routable types, migrated existing values into it, and routed all metadata through the resolver. Category pages gained proper social images from their hero images without any editorial work, landing pages got descriptions from their summaries, and syndicated articles got canonical URLs pointing to the original publisher. A CI check for duplicate descriptions found 140 article pairs sharing a description, which editors fixed in a week.

Metadata coverage before and after the shared SEO objectShare of routable pages with a meta description, a proper social image and a correct canonical URL, before and after introducing the shared SEO object and resolver.Description, before41 % of pagesDescription, after100 % of pagesSocial image, before38 % of pagesSocial image, after97 % of pagesCorrect canonical, before88 % of pagesCorrect canonical, after100 % of pages
Most of the gain came from fallbacks, not from new editorial work.

Testing metadata output

Metadata is easy to break silently, because nothing on the visible page changes when a title or canonical goes wrong. Add a test that renders a sample of each routable type and asserts on the head: exactly one <title>, a description within the length limits, an absolute canonical URL on the site’s own domain unless an override is set, an og:image with dimensions, and no noindex on pages that should be indexed. Run the same assertions against production with a crawler after each release, comparing counts of missing or duplicate tags with the previous run. A sudden jump in pages without descriptions usually means a content type lost its SEO object in a model change, or a query stopped requesting it.

Editors benefit from a preview as much as from validation. A small panel in the CMS, or in the preview frame, that shows the resolved search result and social card for the current entry, including fallbacks, lets them see whether an override is needed before they write one. That preview should call the same resolver as the live site, so it never shows something different from what search engines and social networks will actually receive.

Rollout Checklist

  • Define one SEO object type and embed it in every routable type.
  • Make all fields optional with fallbacks from page content and site settings.
  • Resolve metadata in one function used by every route.
  • Validate lengths with warnings and canonical URLs strictly.
  • Generate structured data from content rather than modeling it by hand.
  • Check for duplicate titles and descriptions in CI.

Frequently Asked Questions

Should meta descriptions be required?

No. Required fields get filled with placeholders or copies. Generate a sensible default from the summary and let editors override it on pages where it matters.

Where should Open Graph fields live?

In the same SEO object. Separate social titles and descriptions are rarely needed; when they are, add optional socialTitle and socialDescription fields that fall back to the SEO ones.

Should editors write JSON-LD?

Almost never. The frontend can build structured data from the page’s fields, which keeps it valid and consistent. Model only facts that do not exist anywhere else in the content.

How do we handle metadata for listing and tag pages?

Give them the same SEO object if they are CMS entries. For generated pages, such as tag archives, derive metadata in code from the tag’s name and description, and consider noindex for thin ones.

Should the site name always be appended to titles?

Usually, for recognition in search results, except on the homepage and where the page title already contains it. The resolver handles both cases, so editors never type the site name themselves.