Foundgrove
← All posts

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 The Foundgrove team · Published June 22, 2026 · Updated June 29, 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.io')` — 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' }`. 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. A word of caution on expectations: Ahrefs' controlled study of 1,885 pages found that adding JSON-LD schema did not, on its own, increase AI citations on Google, ChatGPT, or Perplexity — so treat schema as a correctness and rich-result feature, not a guaranteed citation lever.

  • 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. Cap each sitemap at 50,000 URLs (Google's limit) and split into multiple sitemaps if you exceed it. For dynamic content, include lastModified from the CMS so search engines know when to recrawl.

  • 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 — OG image URLs render as relative paths, breaking social previews
  • 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 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.

About Foundgrove

The Foundgrove team

Foundgrove helps US service businesses win qualified leads from search and AI. We write about the practical, measurable side of acquisition — what works in production, not what looks good in a conference deck.

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.

Free SEO & AI visibility auditGet my free audit