GuideIntermediate

Next.js SEO Setup Template

A complete Next.js SEO configuration template covering metadata, sitemaps, robots.txt, structured data, Open Graph, and Core Web Vitals monitoring.

Time to Complete
2-3 hours
Word Count
2,000-3,500 words
Sections
8
Difficulty
Intermediate

Best Used For

New Next.js Project SEO

Configure SEO foundations from the start on a new Next.js App Router project

SEO Retrofit for Existing Apps

Add proper meta tags, sitemaps, and structured data to an existing Next.js application

Next.js Migration SEO

Ensure SEO parity when migrating to Next.js from another framework

Multi-Language Next.js SEO

Set up hreflang, locale routing, and internationalized metadata

Template Structure

1

Metadata Configuration

Set up dynamic metadata generation using Next.js App Router's generateMetadata function

Example: Title templates, descriptions, canonical URLs, and Open Graph per page
2

Sitemap Generation

Create a dynamic XML sitemap that automatically includes all routes

Example: sitemap.ts file with static and dynamic page entries
3

Robots.txt Configuration

Configure crawl directives for search engines and AI crawlers

Example: robots.ts with Googlebot, GPTBot, and ClaudeBot rules
4

Structured Data Templates

JSON-LD schema markup patterns for common page types

Example: Article, Product, FAQ, BreadcrumbList, Organization schemas
5

Open Graph and Twitter Cards

Social sharing metadata with dynamic OG images

Example: og:image generation using Next.js OG image API
6

Performance Optimization

Next.js-specific settings for optimal Core Web Vitals

Example: Image optimization, font loading, script deferral patterns
7

Internationalization SEO

Hreflang tags, locale routing, and translated metadata

Example: alternates configuration with locale-specific canonical URLs
8

Monitoring and Validation

Tools and CI checks to verify SEO configuration is working

Example: Lighthouse CI, structured data validation, meta tag checks

Example Outputs

Metadata Configuration

Title templates, descriptions, canonical URLs, and Open Graph per page

Sitemap Generation

sitemap.ts file with static and dynamic page entries

Robots.txt Configuration

robots.ts with Googlebot, GPTBot, and ClaudeBot rules

Structured Data Templates

Article, Product, FAQ, BreadcrumbList, Organization schemas

Open Graph and Twitter Cards

og:image generation using Next.js OG image API

Common Pitfalls

  • Use generateMetadata for dynamic per-page titles and descriptions
  • Set a title template at the layout level for consistent branding
  • Always include canonical URLs to prevent duplicate content
  • Use next/image for automatic image optimization and WebP conversion
  • Allow GPTBot and ClaudeBot in robots.txt for AI search visibility
  • Add Organization schema for AI knowledge graph recognition

Optimization Tips

SEO Tips

  • Use generateMetadata for dynamic per-page titles and descriptions
  • Set a title template at the layout level for consistent branding
  • Always include canonical URLs to prevent duplicate content
  • Use next/image for automatic image optimization and WebP conversion

GEO Tips

  • Allow GPTBot and ClaudeBot in robots.txt for AI search visibility
  • Add Organization schema for AI knowledge graph recognition
  • Structure FAQ sections with FAQ schema for AI citation

Example Keywords

next.js seo setupnext.js app router seonext.js metadata configurationnext.js sitemap generation

What This Template Covers

This template provides a complete SEO configuration for Next.js App Router projects. Each section includes working code you can copy directly into your project and adapt to your URL structure and content types.

Next.js App Router has first-class SEO support through its metadata API, file-based routing, and built-in optimizations. This template shows you how to use every relevant feature.


Section 1: Metadata Configuration

Root layout metadata

Set default metadata and a title template in your root layout:

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

export const metadata: Metadata = {
  title: {
    default: "YourSite — Tagline",
    template: "%s | YourSite"
  },
  description: "Your site's default description (120-155 characters).",
  metadataBase: new URL("https://yoursite.com"),
  alternates: {
    canonical: "/"
  }
}

The template pattern means child pages only need to set title: "Page Title" and it automatically becomes "Page Title | YourSite".

Dynamic page metadata

// app/blog/[slug]/page.tsx
import type { Metadata } from "next"

export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    title: post.title,
    description: post.metaDescription,
    alternates: {
      canonical: `/blog/${post.slug}`
    },
    openGraph: {
      title: post.title,
      description: post.metaDescription,
      type: "article",
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      authors: [post.author]
    }
  }
}

Keywords and robots directives

export const metadata: Metadata = {
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      "max-snippet": -1,
      "max-image-preview": "large",
      "max-video-preview": -1
    }
  }
}

Section 2: Sitemap Generation

Dynamic sitemap

// app/sitemap.ts
import type { MetadataRoute } from "next"

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = "https://yoursite.com"

  // Static pages
  const staticPages = [
    { url: baseUrl, lastModified: new Date(), priority: 1.0 },
    { url: `${baseUrl}/about`, lastModified: new Date(), priority: 0.5 },
    { url: `${baseUrl}/pricing`, lastModified: new Date(), priority: 0.8 }
  ]

  // Dynamic pages from CMS/database
  const posts = await getAllPosts()
  const postPages = posts.map(post => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: "weekly" as const,
    priority: 0.7
  }))

  return [...staticPages, ...postPages]
}

Sitemap index for large sites

For sites with 10,000+ pages, split into multiple sitemaps:

// app/sitemap.ts
export async function generateSitemaps() {
  const totalPosts = await getPostCount()
  const sitemapCount = Math.ceil(totalPosts / 5000)

  return Array.from({ length: sitemapCount }, (_, i) => ({ id: i }))
}

export default async function sitemap({ id }): Promise<MetadataRoute.Sitemap> {
  const start = id * 5000
  const posts = await getPosts({ offset: start, limit: 5000 })
  // ... return formatted entries
}

Section 3: Robots.txt

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

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: "*",
        allow: "/",
        disallow: ["/api/", "/dashboard/", "/admin/"]
      },
      {
        userAgent: "GPTBot",
        allow: "/"
      },
      {
        userAgent: "ClaudeBot",
        allow: "/"
      }
    ],
    sitemap: "https://yoursite.com/sitemap.xml"
  }
}

Allow AI crawlers explicitly if you want AI search visibility. Block authenticated routes that shouldn't be indexed.


Section 4: Structured Data

Reusable JSON-LD component

// components/json-ld.tsx
export function JsonLd({ data }: { data: Record<string, unknown> }) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  )
}

Article schema

// In your blog post page
<JsonLd
  data={{
    "@context": "https://schema.org",
    "@type": "Article",
    headline: post.title,
    description: post.description,
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    author: {
      "@type": "Person",
      name: post.author
    },
    publisher: {
      "@type": "Organization",
      name: "YourSite",
      logo: {
        "@type": "ImageObject",
        url: "https://yoursite.com/logo.png"
      }
    }
  }}
/>

FAQ schema

<JsonLd
  data={{
    "@context": "https://schema.org",
    "@type": "FAQPage",
    mainEntity: faqs.map(faq => ({
      "@type": "Question",
      name: faq.question,
      acceptedAnswer: {
        "@type": "Answer",
        text: faq.answer
      }
    }))
  }}
/>
<JsonLd
  data={{
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    itemListElement: [
      {
        "@type": "ListItem",
        position: 1,
        name: "Home",
        item: "https://yoursite.com"
      },
      {
        "@type": "ListItem",
        position: 2,
        name: "Blog",
        item: "https://yoursite.com/blog"
      },
      { "@type": "ListItem", position: 3, name: post.title }
    ]
  }}
/>

Section 5: Open Graph and Twitter Cards

Default OG configuration

// app/layout.tsx metadata
openGraph: {
  type: "website",
  locale: "en_US",
  url: "https://yoursite.com",
  siteName: "YourSite",
  images: [
    {
      url: "https://yoursite.com/og-default.jpg",
      width: 1200,
      height: 630,
      alt: "YourSite",
    },
  ],
},
twitter: {
  card: "summary_large_image",
  site: "@yoursite",
},

Dynamic OG images with Next.js

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og"

export const size = { width: 1200, height: 630 }
export const contentType = "image/png"

export default async function OGImage({ params }) {
  const post = await getPost(params.slug)

  return new ImageResponse(
    (
      <div style={{ display: "flex", flexDirection: "column", padding: 60 }}>
        <h1 style={{ fontSize: 48 }}>{post.title}</h1>
        <p style={{ fontSize: 24, color: "#666" }}>yoursite.com</p>
      </div>
    ),
    size
  )
}

Section 6: Performance Optimization

Image optimization

import Image from "next/image"

// Always use next/image for automatic optimization
;<Image
  src="/hero.jpg"
  alt="Hero description"
  width={1200}
  height={630}
  priority // Preload above-fold images
/>

next/image automatically:

  • Converts to WebP/AVIF
  • Generates responsive srcset
  • Lazy loads below-fold images
  • Sets width/height to prevent CLS

Font optimization

// app/layout.tsx
import { Inter } from "next/font/google"

const inter = Inter({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-inter",
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.variable}>
      <body>{children}</body>
    </html>
  )
}

next/font automatically:

  • Self-hosts font files (no external requests)
  • Calculates size-adjust for zero CLS
  • Subsets to reduce file size

Script optimization

import Script from "next/script"

// Defer non-critical third-party scripts
;<Script src="https://analytics.example.com/script.js" strategy="lazyOnload" />

Section 7: Internationalization SEO

Locale-based routing

// next.config.js
module.exports = {
  i18n: {
    locales: ["en", "es", "fr", "de"],
    defaultLocale: "en"
  }
}

Hreflang through alternates

export async function generateMetadata({ params }): Promise<Metadata> {
  return {
    alternates: {
      canonical: `/en/blog/${params.slug}`,
      languages: {
        en: `/en/blog/${params.slug}`,
        es: `/es/blog/${params.slug}`,
        fr: `/fr/blog/${params.slug}`,
        "x-default": `/en/blog/${params.slug}`
      }
    }
  }
}

This generates the correct <link rel="alternate" hreflang="..."> tags automatically.


Section 8: Monitoring and Validation

Lighthouse CI

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: npm start &
      - uses: treosh/lighthouse-ci-action@v11
        with:
          urls: |
            http://localhost:3000/
            http://localhost:3000/blog/sample-post
          budgetPath: ./lighthouse-budget.json

Meta tag validation script

#!/bin/bash
# Verify critical SEO elements exist in production HTML
URL="https://yoursite.com"

check_page() {
  local path=$1
  local html=$(curl -s "$URL$path")

  echo "Checking $path..."
  echo "$html" | grep -q '<title>' || echo "  MISSING: title tag"
  echo "$html" | grep -q 'meta name="description"' || echo "  MISSING: meta description"
  echo "$html" | grep -q 'rel="canonical"' || echo "  MISSING: canonical"
  echo "$html" | grep -q 'application/ld+json' || echo "  MISSING: structured data"
}

check_page "/"
check_page "/blog/sample-post"
check_page "/pricing"

Frequently Asked Questions

Does Next.js handle SEO automatically?

Next.js provides the infrastructure (metadata API, SSR/SSG, image optimization), but you need to configure it per page. The framework doesn't auto-generate titles, descriptions, or structured data — you define them through the metadata API.

Should I use App Router or Pages Router for SEO?

App Router has better SEO primitives: generateMetadata, file-based sitemap.ts, robots.ts, and streaming SSR. For new projects, use App Router. For existing Pages Router projects, the SEO capabilities are adequate but require more manual configuration.

How do I handle 404 pages for SEO?

// app/not-found.tsx
export const metadata = { title: "Page Not Found" }

export default function NotFound() {
  return <h1>404 - Page Not Found</h1>
}

Next.js automatically returns a 404 status code for the not-found.tsx page.

Can I use Next.js middleware for SEO redirects?

Yes. Middleware runs at the edge before the page renders:

// middleware.ts
export function middleware(request) {
  const redirects = { "/old-path": "/new-path" }
  const target = redirects[request.nextUrl.pathname]
  if (target) {
    return NextResponse.redirect(new URL(target, request.url), 301)
  }
}

Generate Content with This Template

Rankwise uses this template structure automatically. Create AI-optimized content in minutes instead of hours.

Try Rankwise Free
Newsletter

Stay ahead of AI search

Weekly insights on GEO and content optimization.