Here's a pattern I've seen in nearly every codebase I've audited. There's a <Seo> component, or a generateMetadata function, or a layout that sets the <title>. It has sensible fallbacks: if a page doesn't provide a title, use the site name. If there's no description, leave it empty. If there's no canonical URL, skip it.
Those fallbacks feel helpful. They're actually how hundreds of pages end up titled "Acme Inc" with no description and no canonical, and nobody notices for months.
My opinion: SEO metadata should be required by the type system, not suggested by documentation. If a page can compile without a title, eventually a page will ship without one.
The problem is defaults, not knowledge
Developers mostly know pages need unique titles. Content teams mostly know descriptions matter. The bugs don't come from ignorance. They come from:
- A new page type copied from a template that relied on the layout's fallback title.
- A refactor that moved metadata into a parent layout, so every child inherited the same values.
- A CMS field that's optional, so half the entries leave it blank.
- A dynamic route where the data fetch failed and the page rendered with defaults.
In every case, the code did exactly what it was allowed to do. The fix is to stop allowing it.
What "required" looks like in practice
In TypeScript, it can be as simple as making your page metadata a required, typed prop rather than an optional one with fallbacks:
type PageSeo = { title: string; // required, no fallback description: string; // required canonicalPath: `/${string}`; // required, must start with / noindex?: boolean; // optional, explicit opt-out }; type PageProps<T> = { seo: PageSeo; data: T }; Your page layout then accepts seo: PageSeo, never seo?: Partial<PageSeo>. A new page that doesn't provide a title fails type-checking. A content type without a description can't be rendered by the component that expects one.
If you use a framework's metadata API (Next.js generateMetadata, Nuxt's useSeoMeta, SvelteKit's <svelte:head>, Astro layouts), wrap it in a small function that takes your strict type and returns the framework's format. The framework stays flexible; your app doesn't.
Validate at runtime too
Types catch missing fields. They don't catch bad values: an empty string, a title that's just "Untitled", a description identical across every product. Add runtime validation where data enters the page, for example with a schema library:
import { z } from "zod"; export const PageSeoSchema = z.object({ title: z.string().trim().min(10).max(70), description: z.string().trim().min(50).max(170), canonicalPath: z.string().regex(/^\/[^\s?#]*$/), noindex: z.boolean().optional(), }); The exact length limits are a judgement call. Search engines don't publish strict limits and truncate by display width rather than characters, so treat these as guard rails, not rules. The point is catching blanks and obvious junk.
When validation fails, fail loudly in development and preview builds. In production, log it and alert; don't silently render the fallback, because silent fallbacks are the original problem.
Add a test for duplicates
Required fields stop blanks; they don't stop copy-paste. A cheap build-time test that collects every page's title and description and fails on duplicates catches the other half of the problem:
const seen = new Map<string, string>(); for (const page of allPages()) { const prev = seen.get(page.seo.title); if (prev) throw new Error(`Duplicate title on ${page.path} and ${prev}`); seen.set(page.seo.title, page.path); } On sites with thousands of generated pages, run it against a sample or per section, but run it.
Make the CMS agree
If content comes from a CMS, make the corresponding fields required there as well. Most headless CMSs support required fields and length validation. A required field in code with an optional field in the CMS just moves the failure from build time to runtime.
For existing content, run a one-off script to list entries missing the fields, and fix them before you switch the requirement on.
"But this slows people down"
Slightly. A content editor now has to write a description before publishing. A developer adding a page has to think about its title. That's the point. Those few seconds of friction replace a later audit, a spreadsheet of 400 pages with duplicate titles, and a sprint spent fixing them.
And the friction is front-loaded: once page types and CMS models are set up, most new pages fill the fields naturally.
"Can't we just audit regularly?"
You should audit too. But audits find problems after they've shipped, been crawled, and possibly been shown to searchers for weeks. Types and validation prevent most of them from shipping at all. Audits are your smoke detector; required props are not leaving the stove on.
What I'd make required
For a typical content or marketing site:
- Title: unique per page, written for humans.
- Meta description: written, not auto-truncated body text.
- Canonical path: explicit, so it never depends on whatever URL the request happened to arrive at.
- An explicit indexing decision: default indexable, with
noindexas a deliberate opt-out that's visible in code review.
For specific page types, consider adding:
- Open Graph image for anything likely to be shared.
- Structured data inputs (product price and availability, article dates) where you output schema markup.
-
hreflangalternates for multilingual sites.
The broader principle
Good SEO engineering looks a lot like good engineering generally: make the right thing the easy thing, and the wrong thing a compile error. You wouldn't let an API endpoint ship without authentication because someone forgot. Don't let a page ship without a title for the same reason.
Catching what slips through anyway, on the live site after deploys, is part of what seotto.com does.
Related reading
- Should every page have a meta description? Let's argue about it
- Find duplicate and missing title tags with a Python crawl script
- Quick tip: a tiny script that prints the title, canonical and robots tags of any URL (so you stop opening view-source)
- Pagination and infinite scroll: an SEO checklist for the component everyone builds in an afternoon
Does your codebase enforce SEO metadata in types or schemas, or does it rely on fallbacks? What's stopped you from making it strict?

