Next.js Metadata API: A Practical Guide to Technical SEO

September 20, 2026

Most Next.js SEO guides stop at "add a title and description." The Metadata API can do a lot more — dynamic per-page JSON-LD, generated OG images, proper canonical handling across dynamic routes — and getting these details right is the difference between a site that Google actually understands and one that just technically renders.

Table of Contents

Static vs Dynamic Metadata

For pages with fixed content, export a static metadata object:

export const metadata: Metadata = { title: "Services", description: "Cloud architecture, web development, and DevOps services.", };

For anything driven by dynamic data — a blog post, a product page, a service detail page — use generateMetadata, an async function that runs at build time (for static routes) or request time (for dynamic ones):

export async function generateMetadata({ params, }: { params: Promise<{ slug: string }>; }): Promise<Metadata> { const { slug } = await params; const post = await getPost(slug); if (!post) return {}; return { title: post.title, description: post.summary, }; }

The mistake I see most often: hardcoding metadata on a dynamic route and letting every generated page share the same title and description. That's a duplicate-content signal Google will notice.

generateMetadata for Dynamic Routes

A pattern worth adopting: derive Open Graph and Twitter metadata from the same source object as the page title/description, so they can never drift out of sync:

export async function generateMetadata({ params }: Props): Promise<Metadata> { const { slug } = await params; const post = await getPost(slug); if (!post) return {}; const { title, summary: description, publishedAt, image } = post; return { title, description, openGraph: { title, description, type: "article", publishedTime: publishedAt, url: `https://example.com/blog/${slug}`, ...(image && { images: [{ url: image }] }), }, twitter: { card: "summary_large_image", title, description, }, alternates: { canonical: `https://example.com/blog/${slug}`, }, }; }

JSON-LD Structured Data Done Right

Structured data is what turns a blog post into a rich result eligible page. Inject it via a <script type="application/ld+json"> tag rendered server-side:

const jsonLd = { "@context": "https://schema.org", "@type": "BlogPosting", headline: post.title, datePublished: post.publishedAt, dateModified: post.updatedAt ?? post.publishedAt, description: post.summary, author: { "@type": "Person", name: "Your Name" }, }; return ( <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c") }} /> );

That .replace(/</g, "\\u003c") matters — it's a defense against script-injection if any field in your JSON-LD ever contains user-controlled content (comments, form input, CMS fields editable by non-trusted users). Skipping it is a small but real XSS surface.

For a personal site or business, a Person or Organization schema with knowsAbout and hasCredential fields (if you hold real, verifiable certifications) can meaningfully strengthen E-E-A-T signals — Google's guidance on demonstrating expertise leans on exactly this kind of structured, verifiable claim rather than prose alone.

Dynamic OG Images with ImageResponse

Next.js's ImageResponse (via opengraph-image.tsx) generates social preview images at request or build time using JSX and CSS, rendered to PNG:

import { ImageResponse } from "next/og"; export default async function Image({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = await getPost(slug); return new ImageResponse( ( <div style={{ display: "flex", fontSize: 60, background: "white", width: "100%", height: "100%", alignItems: "center", justifyContent: "center" }}> {post?.title} </div> ), { width: 1200, height: 630 } ); }

This means every blog post or product page gets a unique, on-brand social preview without manually designing 40 individual images in Figma.

Canonical URLs and Duplicate Content Traps

Dynamic routes are a common source of accidental duplicate content: pagination (/blog?page=2), tracking parameters (?utm_source=...), and trailing-slash inconsistencies can all generate multiple indexable URLs for what's conceptually the same content. Always set an explicit canonical:

alternates: { canonical: `https://example.com/blog/${slug}`, },

For paginated list pages, canonicalize to the unparameterized first page or use rel="next"/"prev" conventions consistently — Google has deprecated formal support for rel=next/prev but a clean canonical strategy still avoids duplicate-content dilution.

Sitemap and Robots Generation

Next.js's file-based sitemap.ts and robots.ts conventions let you generate these dynamically from your actual content source instead of maintaining a static XML file by hand:

export default function sitemap(): MetadataRoute.Sitemap { const posts = getAllPosts(); return [ { url: "https://example.com", priority: 1 }, ...posts.map((post) => ({ url: `https://example.com/blog/${post.slug}`, lastModified: new Date(post.updatedAt ?? post.publishedAt), priority: 0.6, })), ]; }

This is worth automating early — a sitemap that silently falls out of sync with your actual content (because someone forgot to update a hardcoded array) is a common, avoidable crawl-coverage gap.

Common Mistakes

  • Missing metadataBase. Without it, relative Open Graph image URLs resolve incorrectly when crawled, and Twitter card previews can silently fail.
  • Static metadata on dynamic routes. Every generated page sharing the same title/description is a duplicate-content signal.
  • Forgetting canonical tags on paginated or parameterized routes. Leads to Google indexing multiple URLs for the same logical content, diluting ranking signal.
  • JSON-LD with unescaped user content. A real, if narrow, XSS vector if any field can contain user-supplied text.
  • Not regenerating OG images per page. A single static OG image across every blog post is a missed opportunity — dynamic ones directly reflect what's actually on the page and improve click-through from social shares.

Performance Considerations

generateMetadata runs before the page component and can fetch data — if that data-fetching is expensive and not cached, it doubles your data layer's load (once for metadata, once for the page body) unless you deduplicate with React's cache() or Next.js's built-in request memoization. For static content, prefer generateStaticParams plus build-time metadata generation over per-request fetching.

Security Considerations

Beyond the JSON-LD escaping noted above, avoid putting sensitive internal identifiers (internal database IDs, unpublished draft slugs) into metadata fields that get rendered into publicly crawlable HTML — metadata is just as public as page content.

FAQs

Does the Next.js Metadata API replace next-seo? For most use cases, yes — the built-in Metadata API covers what next-seo historically provided, and it's maintained in step with the framework rather than as a separate dependency.

Do I need JSON-LD if I already have good meta tags? Meta tags control how a link previews and what shows in the basic search snippet; JSON-LD is what enables rich results (FAQ accordions, breadcrumbs, article cards) in search results. They serve different purposes and both matter.

Can generateMetadata cause performance issues? Yes, if it performs uncached expensive data fetching that duplicates work already done by the page component — dedupe with request memoization.

Should every page have unique Open Graph images? For content pages (blog posts, products), yes — it materially improves social click-through. For utility pages (settings, account pages), a shared default OG image is fine.

How do I verify my structured data is valid? Use Google's Rich Results Test or the Schema.org validator before shipping — a JSON-LD syntax error silently fails rather than throwing a visible client error.

Key Takeaways

  • Use generateMetadata for anything dynamic; static metadata exports invite duplicate-content problems on templated routes.
  • JSON-LD unlocks rich results that meta tags alone can't — and always escape user-influenced fields within it.
  • Dynamic OG images via ImageResponse scale social preview quality without manual design work per page.
  • Canonical tags and dynamically generated sitemaps prevent the quiet duplicate-content issues that erode rankings over time.

Related Reading

Next Steps

Need a technical SEO audit or a Next.js site that's actually built with search visibility in mind from day one? Get in touch.

LinkedIn