DEV Community

Cover image for How to Use the Next.js Metadata API for SEO
Connor Fitzgerald
Connor Fitzgerald

Posted on • Originally published at autoblogwriter.app

How to Use the Next.js Metadata API for SEO

Modern React apps live or die by discoverability. If search engines and AI crawlers cannot parse titles, canonicals, or structured data, your pages will underperform regardless of content quality.

This guide explains how to use the Next.js metadata API to ship reliable, production-grade SEO in SSR and SSG apps. It is for developers and SaaS teams building with the App Router who need consistent metadata, schema, and sitemaps without a CMS. The key takeaway: model metadata as code using the metadata API, validate it at build time, and automate it across your blog and docs.

What is the Next.js metadata API?

The Next.js metadata API is a first-class way to declare page metadata in App Router projects. Instead of hand-writing tags in head elements, you export objects or a generateMetadata function that Next.js serializes into SEO-friendly tags during render.

Why it exists

  • Centralize SEO configuration in code
  • Avoid brittle, ad hoc head tags
  • Enable consistent defaults with easy overrides per route

How it works at a high level

  • In a layout or page, export a metadata object or a generateMetadata function
  • Next.js merges metadata from root to leaf layouts and the active page
  • The framework renders tags server side for search engines

For official docs and option references, see the Next.js Metadata docs: https://nextjs.org/docs/app/api-reference/functions/generate-metadata.

Core concepts and types in the metadata API

Understanding the main fields will help you enforce an SEO baseline across your app.

Top level fields you will use often

  • title: Static string or template with default and route-specific titles
  • description: One sentence that matches on-page content
  • keywords: Short list of relevant terms
  • openGraph: Title, description, URL, siteName, images, type, and locale
  • twitter: Card type, title, description, images
  • alternates: Canonicals and language alternates
  • robots: Indexing and crawling rules
  • icons: Favicons and touch icons

Where metadata is declared

  • app/layout.tsx or app/layout.ts for global defaults
  • route groups and nested layouts for section-specific defaults
  • app/[slug]/page.tsx for per-page overrides via generateMetadata

Setting up global defaults with metadata

Clean defaults save time and prevent SEO drift.

Example: app/layout.tsx

// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  title: {
    default: 'Example SaaS',
    template: '%s | Example SaaS'
  },
  description: 'Programmatic SEO and a reliable React SEO pipeline for your product.',
  keywords: ['Next.js SEO', 'programmatic SEO', 'React SEO'],
  openGraph: {
    siteName: 'Example SaaS',
    type: 'website',
    url: 'https://example.com',
    images: ['/og-default.png']
  },
  twitter: {
    card: 'summary_large_image',
    creator: '@examplesaas'
  },
  robots: {
    index: true,
    follow: true
  },
  alternates: {
    canonical: '/',
  }
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tips for reliable defaults

  • Set metadataBase to ensure absolute URLs in openGraph and alternates
  • Use title templates to keep titles consistent
  • Provide a fallback OG image so every route shares a valid card

Route level metadata with generateMetadata

For dynamic content like blog posts or docs, use generateMetadata to compute tags from data.

Example: app/blog/[slug]/page.tsx

// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next';
import { getPostBySlug } from '@/lib/data';

export async function generateMetadata(
  { params }: { params: { slug: string } },
  parent: ResolvingMetadata
): Promise<Metadata> {
  const post = await getPostBySlug(params.slug);
  if (!post) return { title: 'Not found', robots: { index: false, follow: false } };

  const url = new URL(`/blog/${post.slug}`, 'https://example.com');

  return {
    title: post.seoTitle ?? post.title,
    description: post.seoDescription ?? post.excerpt,
    keywords: post.keywords,
    alternates: { canonical: url.toString() },
    openGraph: {
      type: 'article',
      url: url.toString(),
      title: post.title,
      description: post.excerpt,
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      images: [{ url: post.ogImage ?? '/og-default.png' }]
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.ogImage ?? '/og-default.png']
    }
  };
}

export default async function Page({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);
  return <article dangerouslySetInnerHTML={{ __html: post.html }} />;
}
Enter fullscreen mode Exit fullscreen mode

Good practices for dynamic routes

  • Use content data as the single source of truth
  • Always compute canonical URLs for dynamic slugs
  • Mark 404 like states as noindex to avoid thin content indexing

Canonicals, language alternates, and robots

The alternates and robots fields help control indexing and duplicate risk.

Canonical URLs

  • Use alternates.canonical at global and page levels
  • For cross-posted content, set the canonical to the primary source

Language alternates

  • Provide alternates.languages for locales, like en, fr, es
  • Ensure localized pages have self-referencing canonicals and hreflang
alternates: {
  canonical: 'https://example.com/blog/post',
  languages: {
    'en-US': 'https://example.com/en/blog/post',
    'fr-FR': 'https://example.com/fr/blog/post'
  }
}
Enter fullscreen mode Exit fullscreen mode

Robots

  • For private previews or staging, set robots to noindex and add an x-robots-tag header at the edge
  • For paginated lists, decide on indexation strategy and include link rel prev and next in markup if relevant

Open Graph and Twitter cards that always render

Social cards drive click-through and help AI crawlers form page summaries.

Recommended OG structure

  • Always include a 1200x630 image
  • Use absolute URLs if you do not set metadataBase
  • Match title and description to the HTML content
openGraph: {
  type: 'article',
  url: 'https://example.com/blog/nextjs-metadata-api',
  title: 'How to Use the Next.js Metadata API for SEO',
  description: 'Practical patterns for SSR apps.',
  images: [{ url: 'https://example.com/og/nextjs-metadata.png', width: 1200, height: 630 }]
}
Enter fullscreen mode Exit fullscreen mode

Twitter card basics

  • Use summary_large_image for most articles
  • Keep descriptions under ~200 characters

Structured data with JSON-LD in App Router

The metadata API focuses on head tags, but you also need structured data for articles, products, and org profiles. In the App Router, emit JSON-LD within your page or layout.

Simple Article schema helper

// app/components/JsonLd.tsx
export function JsonLd({ data }: { data: Record<string, unknown> }) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode
// app/blog/[slug]/page.tsx
import { JsonLd } from '@/app/components/JsonLd';

export default async function Page({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: post.title,
    description: post.excerpt,
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    author: [{ '@type': 'Person', name: post.author }],
    image: post.ogImage
  };

  return (
    <article>
      <JsonLd data={schema} />
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  );
}
Enter fullscreen mode Exit fullscreen mode

Validation tools

Sitemaps and robots.txt in Next.js

Next.js provides first-class functions to generate sitemaps and robots.txt alongside metadata.

Dynamic sitemap generation

// app/sitemap.ts
import type { MetadataRoute } from 'next';
import { listPublishedPosts } from '@/lib/data';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await listPublishedPosts();
  const base = 'https://example.com';

  return [
    { url: `${base}/`, changefreq: 'weekly', priority: 1 },
    ...posts.map((p) => ({
      url: `${base}/blog/${p.slug}`,
      lastModified: p.updatedAt,
      changefreq: 'monthly',
      priority: 0.7
    }))
  ];
}
Enter fullscreen mode Exit fullscreen mode

robots.txt with crawl rules

// app/robots.ts
import type { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  const base = 'https://example.com';
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: ['/drafts', '/api']
    },
    sitemap: [`${base}/sitemap.xml`]
  };
}
Enter fullscreen mode Exit fullscreen mode

For more on these routes, see the Next.js Metadata and SEO routes docs: https://nextjs.org/docs/app/building-your-application/optimizing/metadata.

Production checklist for the Next.js metadata API

Use this quick list to prevent missed tags and indexing issues at launch.

Global and layout level

  • metadataBase set to your canonical origin
  • Default title template and OG image
  • robots index and follow set as intended

Page level

  • generateMetadata uses real content fields
  • Canonical URLs computed per slug
  • openGraph and twitter images present

Structured data

  • Article or Product JSON-LD emitted where relevant
  • Validation passing in Rich Results Test

Crawling and sitemaps

  • robots.ts lists the correct sitemap
  • app/sitemap.ts includes canonical URLs and lastModified

Automating blog SEO with a React SEO pipeline

Once your app-level patterns are stable, wire a pipeline that enforces them for every new post. This is where programmatic SEO and AI blog automation can help your team scale without regressions.

From content to publish

  • Store post data with SEO fields: title, excerpt, slug, publishedAt, ogImage
  • Generate HTML and images in CI or via a content service
  • Publish content and let your Next.js app render with generateMetadata

Benefits of treating SEO as code

  • Consistency across hundreds of posts
  • Easy refactors when requirements change
  • Simple testing of metadata outputs per route

For a developer-first tool that automates metadata, schema, sitemaps, and internal linking while generating production-ready posts, see AutoBlogWriter: https://autoblogwriter.app/.

Example: end to end blog route with enforced SEO

Below is a compact example that pulls together data, metadata, JSON-LD, and a render path.

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getPostBySlug } from '@/lib/data';
import { JsonLd } from '@/app/components/JsonLd';

export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
  const post = await getPostBySlug(params.slug);
  if (!post) return { title: 'Not found', robots: { index: false, follow: false } };
  const url = `https://example.com/blog/${post.slug}`;
  return {
    title: post.seoTitle ?? post.title,
    description: post.seoDescription ?? post.excerpt,
    alternates: { canonical: url },
    openGraph: { type: 'article', url, title: post.title, description: post.excerpt, images: [{ url: post.ogImage ?? '/og-default.png' }] },
    twitter: { card: 'summary_large_image', title: post.title, description: post.excerpt, images: [post.ogImage ?? '/og-default.png'] }
  };
}

export default async function Page({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);
  if (!post) return notFound();
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: post.title,
    description: post.excerpt,
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    author: [{ '@type': 'Person', name: post.author }],
    image: post.ogImage
  };
  return (
    <article>
      <JsonLd data={schema} />
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  );
}
Enter fullscreen mode Exit fullscreen mode

Common pitfalls and how to avoid them

These are the issues that most often block indexing or break social cards.

Relative URLs in OG images

  • Use metadataBase or absolute URLs for images and canonical links

Missing canonicals on dynamic routes

  • Always compute alternates.canonical based on the slug

Inconsistent titles across cards and HTML

  • Align title and description in metadata with your rendered H1 and intro

No JSON-LD when rich results are expected

  • Emit JSON-LD for articles, products, FAQs, and breadcrumbs where applicable
  • Validate after deployment, not just locally

Tooling and resources

A small set of links to keep handy when working with the Next.js metadata API and broader React SEO pipeline.

Docs and references

Validators and debuggers

Automation and pipelines

Comparison: metadata API vs manual head tags

Here is a quick comparison to help teams decide whether to migrate.

The table below contrasts the metadata API with manual head tags across core concerns.

Concern Next.js metadata API Manual head tags
Consistency Enforced via types and defaults Prone to drift across pages
DX Central, typed, composable Scattered and brittle
Dynamic data First class with generateMetadata Requires custom logic
Social cards Structured objects with images Manual link and meta tags
Sitemaps/robots First class routes Custom scripts needed
Validation Easier to test per route Ad hoc and error prone

Key Takeaways

  • Use the Next.js metadata API as the single source of truth for titles, canonicals, and cards
  • Compute per-route metadata with generateMetadata for dynamic content
  • Emit JSON-LD where rich results matter and validate after deploy
  • Generate sitemaps and robots via first class Next.js routes
  • Treat SEO as code and automate with a React SEO pipeline for scale

Ship metadata once, enforce it everywhere, and let your app publish with confidence.

Top comments (0)