Next.js · Metadata
Metadata in Next.js
App Router generateMetadata, canonicals, and OG tags so crawlers never depend on client hydration for head tags.
Problem Overview
Missing or duplicate titles and meta descriptions weaken click-through and confuse crawlers. Every primary URL needs unique, accurate document metadata.
Why It Matters
Metadata is the first pitch in search results. Weak titles bury strong pages; thin descriptions waste SERP real estate.
Framework-Specific Explanation
Next.js App Router owns document metadata through the metadata export and generateMetadata. These run on the server and serialize into the initial HTML <head>. Client-only document.title updates are invisible to many crawlers and create a false sense of SEO readiness after hydration.
Pages Router apps should use next/head consistently on the server render path — never only inside useEffect.
Step-by-Step Solution
- Set a root
metadata/title.templateinapp/layout.tsx. - Override per route with unique
titleanddescription(static export orgenerateMetadata). - Add
alternates.canonicalfor the preferred URL (matchwwwvs apex). - Map Open Graph / Twitter fields (
openGraph,twitter) to the same title/description intent. - Mark preview/staging with
robots: { index: false }so soft environments do not compete. - View Source on production — confirm tags exist before any JS runs.
- Validate with
npx moneygap-scan <url>and Search Console URL inspection.
Code Examples
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://www.example.com"),
title: {
default: "Acme Analytics",
template: "%s — Acme Analytics",
},
description: "Close growth gaps with clearer Fix Paths.",
};
// app/pricing/page.tsx
export async function generateMetadata(): Promise<Metadata> {
return {
title: "Pricing",
description:
"Simple plans for teams closing growth gaps. Start Free Trial — AI Estimates only, not guaranteed ROI.",
alternates: { canonical: "/pricing" },
openGraph: {
title: "Pricing — Acme Analytics",
description: "Simple plans for teams closing growth gaps.",
url: "/pricing",
type: "website",
},
};
}
Dynamic segments:
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const page = await loadPage(slug);
return {
title: page.title,
description: page.excerpt,
alternates: { canonical: `/blog/${slug}` },
};
}Common Mistakes
- One title template for every route
- Descriptions over ~160 characters truncated awkwardly
- Forgetting to update metadata when messaging changes
- Client-only title updates that crawlers never see
Validation Checklist
- [ ] Unique
<title>per indexable page - [ ] Unique meta description
- [ ] Charset and viewport present
- [ ] Titles reflect primary H1 intent
AI Readiness Notes
Clear titles and descriptions help AI systems label pages correctly. Keep language concrete and entity-rich (product, audience, action).
Deployment Checklist
- [ ] Every indexable route has unique title + description in View Source
- [ ]
metadataBaseset so OG images resolve to absolute URLs - [ ] Canonical host matches redirects (
wwwvs apex) - [ ] Preview deployments are
noindex - [ ]
moneygap-scanshows no missing-title findings on money pages
Browser Extension Tips
Open a key landing page and confirm the shared extension report lists metadata opportunities — then re-scan after shipping generateMetadata fixes.