Skip to content
June 17, 2025·12 min read

Next.js SEO: Metadata API, Sitemaps, and Structured Data

Implement SEO correctly in Next.js 14+ — generateMetadata, opengraph-image, next-sitemap configuration, JSON-LD structured data, and canonical URLs.

Back

Next.js App Router gives you powerful built-in tools for SEO. But most tutorials only scratch the surface. Here's a complete implementation covering metadata, Open Graph images, structured data, and sitemaps.


Prerequisites

  • Next.js 14+ with App Router
  • Basic understanding of metadata and SEO concepts

generateMetadata — Dynamic SEO Per Page

The generateMetadata function replaces the old Head component. It runs server-side and supports async data fetching:

// app/[locale]/blog/[slug]/page.tsx
import type { Metadata } from "next";
 
type Props = {
  params: Promise<{ locale: string; slug: string }>;
};
 
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale, slug } = await params;
  const post = await getPostBySlug(slug, locale);
 
  if (!post) return { title: "Post not found" };
 
  const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "https://yoursite.com";
  const canonical = `${siteUrl}/${locale}/blog/${slug}`;
 
  return {
    title: post.title,
    description: post.excerpt,
    alternates: {
      canonical,
      languages: {
        en: `${siteUrl}/en/blog/${slug}`,
        fr: `${siteUrl}/fr/blog/${slug}`,
      },
    },
    openGraph: {
      type: "article",
      title: post.title,
      description: post.excerpt,
      url: canonical,
      publishedTime: post.date,
      authors: ["Aïcha Imène DAHOUMANE"],
      tags: post.tags,
    },
    twitter: {
      card: "summary_large_image",
      title: post.title,
      description: post.excerpt,
    },
  };
}

opengraph-image.tsx — Programmatic OG Images

Instead of static images, generate dynamic Open Graph images with the ImageResponse API:

// app/[locale]/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";
 
export const runtime = "edge";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
 
type Props = { params: Promise<{ slug: string; locale: string }> };
 
export default async function Image({ params }: Props) {
  const { slug, locale } = await params;
  const post = await getPostBySlug(slug, locale);
 
  return new ImageResponse(
    (
      <div
        style={{
          background: "linear-gradient(135deg, #0f172a 0%, #1e293b 100%)",
          width: "100%",
          height: "100%",
          display: "flex",
          flexDirection: "column",
          padding: "60px",
          justifyContent: "space-between",
        }}
      >
        <div style={{ color: "#22d3ee", fontSize: 18, fontWeight: 600 }}>
          yoursite.com
        </div>
        <div>
          <div style={{ color: "#f1f5f9", fontSize: 52, fontWeight: 700, lineHeight: 1.2 }}>
            {post?.title ?? "Article"}
          </div>
          <div style={{ color: "#94a3b8", fontSize: 24, marginTop: 20 }}>
            {post?.excerpt}
          </div>
        </div>
      </div>
    ),
    { ...size }
  );
}

JSON-LD Structured Data

Add structured data for rich results in Google Search:

// components/JsonLd.tsx
export function PersonSchema() {
  const schema = {
    "@context": "https://schema.org",
    "@type": "Person",
    name: "Aïcha Imène DAHOUMANE",
    url: "https://yoursite.com",
    jobTitle: "Salesforce Developer & IT Consultant",
    sameAs: [
      "https://github.com/Aiyeesha",
      "https://www.linkedin.com/in/aïcha-imène-dahoumane",
    ],
  };
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}
 
export function ArticleSchema({ post, url }: { post: Post; url: string }) {
  const schema = {
    "@context": "https://schema.org",
    "@type": "TechArticle",
    headline: post.title,
    description: post.excerpt,
    datePublished: post.date,
    author: {
      "@type": "Person",
      name: "Aïcha Imène DAHOUMANE",
    },
    url,
  };
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

Use them in your layout and page respectively:

// app/layout.tsx
import { PersonSchema } from "@/components/JsonLd";
// ...
<body>
  <PersonSchema />
  {children}
</body>

next-sitemap Configuration

npm install next-sitemap
// next-sitemap.config.js
/** @type {import('next-sitemap').IConfig} */
module.exports = {
  siteUrl: process.env.NEXT_PUBLIC_SITE_URL || "https://yoursite.com",
  generateRobotsTxt: true,
  sitemapSize: 5000,
  changefreq: "weekly",
  priority: 0.7,
  exclude: ["/admin/*", "/api/*"],
  robotsTxtOptions: {
    policies: [
      { userAgent: "*", allow: "/" },
      { userAgent: "*", disallow: ["/admin/", "/api/"] },
    ],
  },
  // Multilingual support
  alternateRefs: [
    { href: "https://yoursite.com/en", hreflang: "en" },
    { href: "https://yoursite.com/fr", hreflang: "fr" },
  ],
};
// package.json
{
  "scripts": {
    "postbuild": "next-sitemap"
  }
}

Canonical URLs and hreflang

Always set canonical URLs to prevent duplicate content penalties:

// In generateMetadata
alternates: {
  canonical: `https://yoursite.com/${locale}/blog/${slug}`,
  languages: {
    "en": `https://yoursite.com/en/blog/${slug}`,
    "fr": `https://yoursite.com/fr/blog/${slug}`,
    "x-default": `https://yoursite.com/en/blog/${slug}`,
  },
},

Common Pitfalls

  • Missing x-default: Always include x-default hreflang pointing to your primary language
  • Duplicate OG images: If you have opengraph-image.tsx, don't also set openGraph.images in metadata — they'll conflict
  • robots.txt blocking CSS/JS: Some generated robots.txt accidentally block assets — verify at yoursite.com/robots.txt
  • Dynamic routes not in sitemap: Make sure generateStaticParams is implemented so next-sitemap crawls all slugs

Resources

Aïcha Imène DAHOUMANE
About the author
Aïcha Imène DAHOUMANE
Salesforce Administrator & Developer · Consultant

Salesforce developer with a background in IT infrastructure — building reliable, maintainable solutions with Apex, Flows, LWC, and CI/CD.

ShareShare on LinkedIn