Previewing SEO Metadata in the CMS
This guide belongs to Metadata Injection & SEO Automation and closes the loop between editors and the metadata pipeline. It shows how to give editors, inside the CMS, a live view of what search engines and social platforms will receive for the entry they are editing: the resolved title and description, the social card, the canonical URL, the hreflang set and any warnings, all computed by the same code that renders production pages.
Headless metadata is often correct in code and wrong in practice, because editors cannot see the result of their choices. They leave a description empty without knowing that the fallback is the first sentence of a legal disclaimer, or write a 90-character title that search results cut in half. In a traditional CMS, SEO plugins show a snippet preview next to the fields. A headless CMS needs the same, but the preview must come from the frontend’s resolver, since the CMS does not know the fallbacks, templates and URL rules that live in the frontend code.
The Problem
A publisher’s editors wrote articles in five languages and rarely filled the SEO fields, because they did not know what the defaults were. An audit found that 40 percent of descriptions in search results were the first sentence of the article, which for many articles was a standard “This article was updated on” line. Titles in German were often truncated. The metadata resolver in the frontend was fine; nobody who wrote content could see its output.
How the Preview Works
A preview endpoint on the frontend. A protected API route accepts an entry id, locale and draft flag, fetches the draft content with the preview token, runs the production metadata resolver and returns the resolved head values plus a list of warnings.
A panel in the CMS. Most headless CMSs support UI extensions: Sanity document views and plugins, Contentful app framework locations, Storyblok field plugins and tool plugins, Strapi admin extensions. The panel calls the endpoint when the entry changes and renders a search snippet, a social card and a list of warnings.
Warnings that teach. Warnings explain what the fallback will be and why it matters: “No description set; search results will show: ‘This article was updated on…’. Write a description of 70 to 160 characters.” Editors learn the rules by seeing their effect.
Same code, no drift. Because the panel uses the production resolver, changes to title templates or fallback rules appear in the panel immediately after deploy.
Implementation
The preview endpoint reuses the metadata resolver and adds checks that only make sense for editors.
// app/api/seo-preview/route.ts
import { draftMode } from "next/headers";
import { getEntryDraft } from "@/lib/cms/draft";
import { resolveMetadata } from "@/lib/seo/resolve";
import { findDuplicateDescriptions } from "@/lib/seo/duplicates";
export async function POST(req: Request) {
if (req.headers.get("authorization") !== `Bearer ${process.env.SEO_PREVIEW_SECRET}`) return new Response("unauthorized", { status: 401 });
(await draftMode()).enable();
const { entryId, locale } = (await req.json()) as { entryId: string; locale: string };
const entry = await getEntryDraft(entryId, locale);
if (!entry) return Response.json({ error: "not found" }, { status: 404 });
const meta = resolveMetadata(entry.page, locale, entry.translatedLocales, entry.servedLocale);
const warnings: { field: string; message: string }[] = [];
if (!entry.page.seo?.description) warnings.push({ field: "seo.description", message: `No description set. Search results will show: "${String(meta.description ?? "").slice(0, 120)}…"` });
const title = String(meta.title ?? "");
if (title.length > 60) warnings.push({ field: "seo.title", message: `Title is ${title.length} characters; results usually show about 60.` });
if (await findDuplicateDescriptions(String(meta.description ?? ""), locale, entryId)) warnings.push({ field: "seo.description", message: "Another page in this locale has the same description." });
if (entry.servedLocale !== locale) warnings.push({ field: "locale", message: `This page has no ${locale} content and will fall back to ${entry.servedLocale}.` });
return Response.json({ title, description: meta.description, canonical: meta.alternates?.canonical, languages: meta.alternates?.languages, image: meta.openGraph?.images, warnings });
}
The CMS extension calls this endpoint through a small server-side proxy in the CMS’s app framework, so the secret never reaches the browser. It debounces calls while the editor types, and renders the response. The snippet preview is plain HTML styled to resemble a search result; the card preview renders the image and title at the size platforms use.
Checks worth including
Good warnings are specific and actionable: empty overrides that lead to poor fallbacks, titles and descriptions outside length targets for the locale, duplicates within the locale, missing alt text on the social image, a canonical pointing to another page, a missing translation causing fallback, and noindex set on a page type that is normally indexed. Avoid vague scores; editors act on concrete messages, not on a number out of 100.
Designing the panel for editors
The panel competes for attention with the fields editors are actually working on, so it should be compact and quiet when everything is fine. Show the snippet and card side by side at small size, with warnings listed below them only when there are any. Link each warning to the field it concerns, so a click focuses that field. Offer a locale switch that previews the other locales of the same entry, because translators often work on one language while the panel should reveal problems in the others. Avoid colour-coded scores and traffic lights for the whole entry; they encourage editors to chase a green light rather than fix the specific issue. Test the panel with a few editors before rolling it out, since wording that is obvious to SEO specialists can be opaque to writers.
Configuration Reference
| Component | Recommendation | Why |
|---|---|---|
| Resolver | production code, imported by the endpoint | No drift between preview and live. |
| Endpoint auth | secret via CMS server-side proxy | Drafts are not public. |
| Data | draft content with preview token | Editors see unpublished changes. |
| Update rate | debounced, about one second | Responsive without flooding. |
| Warnings | specific, with the resulting value | Editors learn the rules. |
| Locales | preview each locale separately | Metadata differs per locale. |
Gotchas & Edge Cases
- Resolver needs full page context. Some fallbacks depend on referenced entries, such as a category’s name in the title. Fetch references in the preview too.
- Duplicate checks on drafts. Compare against published content in the same locale, excluding the current entry, or every entry duplicates itself.
- Permissions. The preview endpoint exposes draft content; protect it like any other preview route.
- Performance. Duplicate checks can be expensive; precompute a hash index of published descriptions per locale.
Worked Example
The publisher built a Sanity document view that called the preview endpoint and showed the snippet, card and warnings for the current language. The most frequent warning, the empty description with its bad fallback, prompted editors to write descriptions for new articles; the share of articles with written descriptions rose from 22 to 81 percent within two months, and search results in all five languages stopped showing the “updated on” line. Truncated German titles fell sharply once editors could see the cut-off in the preview.
Beyond Individual Entries
The same endpoint powers reports across many entries. A scheduled job calls it for published entries in each locale, collects warnings and produces a list per locale of the highest-traffic pages with the most important issues, which SEO specialists can work through systematically. It can also run as a pre-publish check: the CMS’s workflow calls the endpoint when an editor requests publication and shows blocking errors, such as a canonical pointing to another page, while leaving editorial suggestions as non-blocking warnings. Using one endpoint for the panel, the reports and the pre-publish check means the rules are defined once and applied everywhere.
Rollout Checklist
- Expose a protected preview endpoint that runs the production resolver on drafts.
- Build a CMS panel that shows snippet, card, canonical, hreflang and warnings.
- Write warnings that show the resulting value and a concrete fix.
- Preview each locale separately.
- Reuse the endpoint for reports and pre-publish checks.
- Keep blocking checks few and limited to real errors.
Frequently Asked Questions
Can we build the preview entirely inside the CMS?
Only by duplicating the resolver’s rules in the extension, which drifts from production as soon as either side changes. Calling the frontend keeps one implementation.
Should warnings block publishing?
Only for errors that harm the site, such as wrong canonicals. Length and style issues should be warnings that editors can weigh against other needs.
Does this work for static sites?
Yes. The endpoint can run as a serverless function next to the static site, importing the same resolver used at build time.
How accurate is the snippet preview?
Search engines sometimes rewrite titles and descriptions, so the preview shows what you provide, not a guarantee. It is still the best predictor editors have, and far better than guessing.