SEO · 11 min read
Next.js Metadata API: The Complete SEO Setup Guide
Summary
Title templates, canonicals, OG, JSON-LD, sitemap.ts — Next.js Metadata API replaces Yoast. The production setup, plus the doubled-suffix bug.
By Hyder Shah, Founder & CEO · Published June 22, 2026 · Updated July 26, 2026
A large share of SEO complaints about Next.js sites trace back to teams that have not configured the Metadata API correctly. The API itself is a clean replacement for Yoast — typed, colocated with pages, version-controlled — but it has sharp edges, and one common bug doubles your title suffix on dynamic routes for weeks before anyone notices.
This is a production-grade setup you can apply on any Next.js project. Copy the patterns; they follow the conventions the Next.js team recommends for marketing sites. For the broader rendering context, see the Next.js for marketing sites pillar.
What does the Metadata API replace?
The Next.js Metadata API replaces seven things you would otherwise install or hand-roll: a SEO plugin (Yoast, RankMath, AIOSEO), a structured-data plugin, a sitemap plugin, a robots.txt editor, an OpenGraph image generator, a canonical-URL plugin, and a 'set per-page meta tags' workflow. It does all seven through typed metadata objects exported from page and layout files.
- Static metadata — `export const metadata = { title, description, ... }`
- Dynamic metadata — `export async function generateMetadata({ params })` for routes with params
- Title templates — `template: '%s | Brand'` applied to child pages from a layout
- Open Graph — `openGraph: { ... }` with auto-handled defaults
- Twitter Cards — `twitter: { ... }` (Twitter inherits OG by default if omitted)
- Canonicals — `alternates: { canonical: '/path' }`
- Robots — `robots: { index, follow }` per route
- Sitemap — `sitemap.ts` in app root returning a sitemap object
- Robots.txt — `robots.ts` in app root returning robots rules
What does the root layout metadata look like?
The root layout sets defaults that every page inherits. The five fields you must set are title (with template), description (default fallback), metadataBase (used for absolute URL resolution), openGraph defaults, and robots defaults. Get these right once and most child pages need only a title and description.
- metadataBase — `new URL('https://foundgrove.com')` — required for absolute OG image URLs
- title — `{ template: '%s | Foundgrove', default: 'Foundgrove — Service-Business Growth Agency' }`
- description — site-wide default, overridden per page
- openGraph — default siteName, locale, type: 'website'
- twitter — card: 'summary_large_image', site: '@foundgrove'
- robots — `{ index: true, follow: true, googleBot: { 'max-image-preview': 'large' } }`
What is the title-template doubled-suffix bug?
This is one of the most common bugs in Next.js metadata. The title template is `'%s | Foundgrove'`. A child page exports `title: 'Pricing | Foundgrove'` because the developer copy-pasted from another tool. The result rendered in the browser tab: `Pricing | Foundgrove | Foundgrove`. Search engines see the duplicated suffix and your CTR on SERPs drops.
The fix is a rule, enforced by code review: never include your brand name in a child page's title. The template adds it automatically. To suppress the template entirely on a specific page (rare — used for the home page only), use the absolute form: `title: { absolute: 'Foundgrove — Service-Business Growth Agency' }`. Per the Next.js generateMetadata docs, `title.absolute` 'can be used to provide a title that ignores `title.template` set in parent segments' — and a `title.default` is required whenever you define a template. A useful guardrail is an ESLint rule that flags page metadata titles containing the brand name.
How should you handle dynamic-route metadata?
Dynamic routes — `app/blog/[slug]/page.tsx`, `app/services/[slug]/page.tsx` — need generateMetadata instead of static metadata. The function receives the route params, fetches the relevant content, and returns the metadata object. This is where the buildMetadata helper pattern earns its keep.
The pattern is to define a single buildMetadata utility that takes a small input (title, description, path, optional OG image override) and returns a fully-formed Metadata object including canonical, openGraph, twitter, and JSON-LD-friendly hints. Every generateMetadata function calls buildMetadata. This eliminates copy-paste bugs and means a site-wide metadata change is one file edit.
- buildMetadata input — title, description, path, ogImage?, noindex?
- buildMetadata output — full Metadata object with canonical, OG, Twitter, robots
- Path resolution — `alternates: { canonical: new URL(path, metadataBase).toString() }`
- OG image fallback — if no ogImage provided, use the default site OG image
- Caching — `generateMetadata` runs in parallel with the page render; do not duplicate fetches; use React's cache() to dedupe
How do you handle structured data (JSON-LD)?
The Metadata API does not handle JSON-LD directly. The Next.js team's recommended pattern is a Schema component that renders a script tag with the structured data. It is type-safe and colocated with the page that needs it.
Define schema components per type: Organization, LocalBusiness, Article, FAQPage, BreadcrumbList, Product, Service. Each takes typed props and renders the JSON-LD as a `<script type='application/ld+json'>` tag. Pages import the relevant schema and render it inside the page component.
Now the expectation-setting, because schema is oversold constantly. Ahrefs tracked 1,885 web pages that added JSON-LD schema between August 2025 and March 2026 and concluded that 'adding schema produced no major uplift in citations on any platform' — Google AI Overviews moved −4.6%, Google AI Mode +2.4%, and ChatGPT +2.2%, the last two statistically indistinguishable from zero. So ship schema for what it actually does: rich-result eligibility, machine-readable correctness, and unambiguous entity definitions. If someone is pitching you structured data as an AI-citation growth lever, that is the study to hand them.
- Organization — site-wide, render in root layout
- LocalBusiness — only on location pages with NAP info
- Article — blog posts and pillar pages, with author, datePublished, dateModified
- FAQPage — any page with a Q&A section (eligible for FAQ rich results where supported)
- BreadcrumbList — every internal page with a breadcrumb trail
- Service / Product — services pages, with offers and aggregateRating where defensible
How do you set up robots.ts and sitemap.ts?
Next.js 13+ supports file-based robots.txt and sitemap.xml. Drop a robots.ts file in the app/ root and export a default function returning a robots object. Same for sitemap.ts. Both run at build time (or on every request if dynamic), generate the correct HTTP response, and serve at the standard URLs.
The sitemap pattern that scales: define a typed array of static routes, then dynamically append entries for blog posts, services, industries, and locations by reading from your data sources. Google's sitemap documentation states that 'all formats limit a single sitemap to 50MB (uncompressed) or 50,000 URLs' — whichever you hit first — so split into multiple sitemaps and a sitemap index above either ceiling. For dynamic content, include lastModified from the CMS so search engines know when to recrawl. Most marketing sites will never come close to 50,000 URLs; programmatic builds hit the 50MB size cap sooner than you would expect.
- robots.ts — rules: [{ userAgent: '*', allow: '/', disallow: ['/api/', '/admin/'] }] plus sitemap URL
- sitemap.ts — array of { url, lastModified, changeFrequency, priority }
- Multiple sitemaps — sitemap-0.xml, sitemap-1.xml split by content type
- Per-route lastModified — pull from CMS updatedAt or git commit timestamp
- Dynamic routes — call your data source inside the sitemap function and map to entries
How do you generate Open Graph images?
Next.js ships @vercel/og for runtime OG image generation. Drop an opengraph-image.tsx file in any route and Next.js will generate a 1200x630 PNG using the React component you write. The component runs on the edge runtime and renders to a PNG in 50-200ms.
The trap: dynamic OG images for every blog post are expensive in cold-start time. A sensible default is a single high-quality static OG image for the site, generating per-post images only for the top 20% of trafficked content. For pillar pages and case studies, dynamic OG images are worth it; for the long tail, static is fine.
What are the common Metadata API mistakes?
Six bugs that show up on almost any Next.js site audit. Each one is a 5-minute fix once you know to look for it.
- Title template doubled — child titles include the brand name, producing 'Page | Brand | Brand'
- metadataBase missing — per the Next.js docs, a relative URL in a metadata field without a configured metadataBase causes a build error, so set it once in the root layout
- Canonical mismatch — alternates.canonical points to a different host or trailing slash inconsistency
- OG image too small — must be at least 1200x630 for Twitter and LinkedIn to render large preview
- robots.ts missing sitemap — search engines do not auto-discover sitemap.xml without an explicit link
- JSON-LD inside <head> — modern Next.js renders JSON-LD inside the body, which is valid; older tooling expects head
How do you handle Client Components, streaming metadata, and migration from next/head?
Three gotchas catch teams once the basics work. First, the Metadata API only runs in Server Components. The Next.js docs state it flatly: 'the metadata object and generateMetadata function exports are only supported in Server Components.' Mark a route or layout `'use client'` and its metadata export is ignored entirely — no title, no description, nothing, and no error shouting at you about it. Keep metadata on the Server Component page or layout and push the interactive UI down into a nested Client Component.
Second — and this one gets misreported as a bug constantly — streaming metadata can put your meta tags in the `<body>` instead of the `<head>`. That is documented, intentional behavior, not a defect. Next.js streams the initial UI without waiting for generateMetadata to resolve, then appends the resulting tags to the body. The docs say Next.js has 'verified that metadata is interpreted correctly by bots that execute JavaScript and inspect the full DOM (e.g. Googlebot).' For HTML-limited bots that cannot run JavaScript — facebookexternalhit is the one named — Next.js detects them by User-Agent and blocks rendering so the metadata lands in the `<head>` as normal.
So the practical rule is: leave it alone. If you have a specific scraper that is genuinely missing your tags, add its User-Agent to the `htmlLimitedBots` config rather than panicking about the body placement; setting `htmlLimitedBots: /.*/` disables streaming metadata entirely, at the cost of slower responses. Do not chase this before you have evidence a real crawler is failing. Keeping metadata on Server Components is the same discipline that keeps client JavaScript low, which dovetails with Core Web Vitals incl. INP on Next.js — less client JS protects both your metadata and your INP.
Third, if you are moving from the Pages Router, you are also migrating away from next/head and libraries like next-seo. The pattern: replace per-page `<Head>` tags and next-seo config objects with the exported metadata object (static routes) or generateMetadata (dynamic routes), move your default title template and site-wide Open Graph into the root layout, and delete next-seo once every route is converted. Do it route by route and verify each with view-source before removing the old approach.
How do you verify everything works?
Three tools, every deploy. Run them as part of CI and you catch metadata regressions before they hit production.
- Google Rich Results Test — validates JSON-LD on a deployed URL
- Meta Sharing Debugger — confirms Open Graph renders correctly on Facebook/LinkedIn
- Twitter Card Validator — confirms Twitter card renders (use the OG fallback if needed)
- Manual sitemap.xml fetch — verify all routes appear with correct lastModified
- View-source on every key page — confirm canonical, OG, JSON-LD, and robots are present
When the Metadata API is configured correctly, it is the single highest-leverage SEO work on a Next.js site. Book a strategy call if you want us to audit your existing metadata setup, or see our website design service for full-stack engagements.
Where does this fit in your stack?
If you're running a US service business, the playbook in this post pairs with our full services lineup and applies cleanly across our supported industries and US locations. If you want help implementing it, book a free strategy call — we'll review your current setup and prioritize the next three moves.
For the deeper engagement details, see our SEO service. New to the terminology here? Our SEO & marketing glossary defines every acronym in this post.
What are the most common questions about this topic?
Common questions readers send us about this topic.
Does the Next.js Metadata API replace Yoast or RankMath?
Yes, entirely. The Metadata API gives you typed per-route titles, descriptions, canonicals, Open Graph, Twitter Cards, robots directives, and JSON-LD structured data — every feature Yoast or RankMath provides on WordPress. Because metadata lives in code, it is also version-controlled and reviewable in pull requests, which Yoast is not.
Should JSON-LD go in the head or body?
Either is valid per Google's documentation. Next.js renders JSON-LD via a Script component or inline script tag inside the body, which is the recommended pattern in the App Router. Older tooling and some validators prefer it in the head, but Google's crawler parses JSON-LD from anywhere in the document. Body placement is fine and simpler in the App Router.
How do I set different metadata per environment (staging vs production)?
Use environment variables for metadataBase and the OG image URL. In staging, set metadataBase to your staging URL and add `robots: { index: false, follow: false }` at the root layout to prevent indexing. Production builds use the real URL and allow indexing. Both should be guarded by NEXT_PUBLIC_VERCEL_ENV or a similar environment check.
What is the buildMetadata helper pattern?
A single TypeScript function — usually in lib/seo.ts — that takes a small input (title, description, path, optional OG image, optional noindex flag) and returns a fully-formed Metadata object including canonical, openGraph, twitter, and robots. Every generateMetadata function in dynamic routes calls buildMetadata, which guarantees consistency and eliminates the title-template doubled-suffix bug.
Do I need separate sitemaps for blog, services, and locations?
Only if you exceed 50,000 URLs per sitemap (Google's hard limit) or 50MB uncompressed. Under that, a single sitemap.ts is fine. Above it, split by content type and reference each from a sitemap index file. For most marketing sites, a single sitemap with all routes is correct.
Can I generate Open Graph images dynamically for every blog post?
Yes, with @vercel/og, but it costs cold-start time on first request per route. The pragmatic pattern is dynamic OG images for high-value pages (pillar posts, case studies, landing pages) and a single high-quality static OG image for the long tail. Dynamic images use the edge runtime and render in 50-200ms once warm.
Why is my page showing the default site title in Google instead of my custom one?
Three possible causes: 1) The title template syntax is wrong (use `%s` exactly, not `{title}`). 2) Your page exports the metadata object incorrectly (not exported, exported as default, or named wrong). 3) generateMetadata is throwing an error and falling back to defaults — check your build logs. The Next.js build output prints the resolved metadata for each route, which makes debugging fast.
How do I handle canonical URLs for paginated routes?
Each paginated route should self-canonical (page 2 canonicals to /blog?page=2, not to /blog). This was changed from Google's old guidance. Use generateMetadata on the paginated route to set the canonical to the current page's URL. Use rel=prev/rel=next link tags as a backup signal, even though Google has officially deprecated them.
Why isn't my Next.js metadata working in a Client Component?
The Metadata API (the exported metadata object and generateMetadata) only runs in Server Components. If a route or layout is marked 'use client', its metadata export is ignored. Move metadata to a Server Component parent — the layout or page — and keep interactive UI in a nested Client Component, or split the file so the metadata lives on the server side.
Why is my SEO meta showing up in the <body> instead of the <head>?
That is streaming metadata, and it is intentional rather than a bug. Next.js sends the initial UI without waiting for generateMetadata, then appends the resolved tags to the body. The docs state Next.js has verified Googlebot interprets this correctly because it executes JavaScript and inspects the full DOM. For HTML-limited bots that cannot run JavaScript, Next.js detects them by User-Agent and blocks rendering so tags land in the head. Only override it via htmlLimitedBots if a real scraper is provably failing.
About the author
Hyder Shah
Founder & CEO, Foundgrove
Hyder Shah is the founder of Foundgrove, an SEO and GEO agency for US service businesses. See our editorial policy for how these guides are researched and reviewed.
Related reading
Other tactical pieces from the Foundgrove blog.
- Web Design · 19 min read
Next.js for Marketing Sites: The Complete 2026 Guide
WordPress marketing sites are losing to Next.js on Core Web Vitals, security, and dev velocity. The full 2026 playbook for choosing and shipping.
Read the web design playbook → - Web Design · 11 min read
Core Web Vitals on Next.js: How to Hit Perfect Scores
Next.js can hit 100/100 on Lighthouse, but not by accident. The specific knobs: next/image, next/font, RSC, third-party deferral, bundle audits.
Read the web design playbook → - Web Design · 12 min read
Next.js vs WordPress for Marketing Sites in 2026
WordPress runs 41.5% of the web, but service businesses are leaving for Next.js. Speed, security, dev velocity — the honest head-to-head.
Read the web design playbook → - Web Design · 13 min read
Why Marketing Sites Are Migrating From WordPress to Next.js
Plugin sprawl, Core Web Vitals failures, and security fatigue are pushing marketing teams off WordPress. The 2026 Next.js migration playbook.
Read the web design playbook →
Want help applying this to your business?
Book a free 30-minute call. We'll review your current acquisition stack and show you the three highest-leverage moves for your industry and state. Or read how our SEO service works.