One File Controls Every Meta Tag on Our Site
When we rebuilt the Twenty1 Media site, we needed SEO sorted across twelve pages: titles, descriptions, OpenGraph tags, and canonical URLs. The wrong way to handle this is scattering export const metadata blocks across every page file and hoping they stay consistent. The pattern we reached for instead is one file: src/content/seo.ts.
The Structure
The file holds a typed record of every page path and its two key fields:
const pages: Record<string, { title: string; description: string }> = {
"/": {
title: "Twenty One Media — Building what others talk about.",
description:
"Custom AI workflows, agentic systems, and AI-powered portals shipped in 7 days. Free 30-min audit. Built by a cop-turned-engineer in Indianapolis.",
},
"/contact": {
title: "Book a free 30-minute AI audit",
description:
"Tell us what's eating your week. We'll tell you in plain English what AI can fix, what it costs, and what to ignore. No pitch. No upsell.",
},
// ... every other route
};
Then a single helper converts any path into a full Metadata object:
export function createPageMetadata(path: string): Metadata {
const page = pages[path];
if (!page) return {};
return {
title: page.title,
description: page.description,
openGraph: {
title: page.title,
description: page.description,
url: `${brand.siteUrl}${path}`,
siteName: brand.name,
type: "website",
},
};
}
Every page file becomes one line:
export const metadata: Metadata = createPageMetadata("/contact");
No duplication. No drift between the <title> tag and the OG title. The canonical URL is derived from brand.siteUrl plus the path, so it's always correct without typing it twelve times.
Title vs. Description: Two Different Jobs
This is the part that trips people up. The title field and the description field are read by completely different audiences.
The title shows up large in social link previews when someone pastes a URL into Slack, LinkedIn, or iMessage. It also appears in the browser tab and in Google's title display. That's a brand impression slot, not a keyword slot. Our homepage title leads with "Building what others talk about." because that's what we want someone to read when a URL gets shared.
The description is what search engines show below the title in SERPs. It has more room, and it's where keyword intent belongs: "custom AI workflows," "agentic systems," "shipped in 7 days," "Indianapolis." Searchers read this to decide if the result matches what they searched. It's a different decision than recognizing a brand from a Slack preview.
We learned this the hard way. The original homepage title was "AI tools for businesses that don't have time to figure out AI." That's okay search copy. But when a lead shared our URL internally, that's what their VP saw as the bold headline in the preview. We changed it to the brand line, kept the keywords in the description, and the two fields now do their distinct jobs.
Why TypeScript for Content
Keeping metadata in .ts instead of a CMS or JSON has three real benefits here.
First, it's co-located with the code. When we add a new route, we add a record to seo.ts in the same PR. There's no separate system to update. Missing records return an empty object from createPageMetadata, which is visible in review rather than silently deploying a pageless title.
Second, the brand.siteUrl import ties everything to the canonical domain. If the domain changes, we change one line in site.ts and every OG URL updates.
Third, TypeScript gives us a consistent shape. title and description are required strings per record. An undefined or wrong-type field is a type error at build time, not a missing meta tag discovered in a link preview after the fact.
What We'd Add Next
Dynamic pages like /blog/[slug] and /work/[slug] use generateMetadata instead, pulling title and description from the MDX frontmatter. The same brand.siteUrl import anchors those canonical URLs too.
The pattern scales cleanly: static routes in seo.ts, dynamic routes in their own generateMetadata functions, one shared source of truth for the brand constants. For a twelve-page marketing site, that's all the SEO infrastructure you need.