Back to Blog

Complete Next.js SEO Guide

A production-ready Next.js SEO workflow: Metadata API, per-post metadata, JSON-LD, Open Graph, sitemap and robots, plus CI checks. Includes next-seo and next-sitemap tips.

D

DJ Lim

Founder & CEO

4 min read

Complete Next.js SEO Guide

Shipping is fast. Keeping search engines happy without a CMS is hard. Titles drift, canonical tags vanish, and your Next.js sitemap quietly stops including new posts.

This is a complete workflow for Next.js SEO. It uses the native Metadata API for per post metadata, Open Graph, Twitter cards, JSON-LD, and first party app/sitemap.ts and app/robots.ts. Automated checks keep it from breaking again.


The checklist that never regresses

Wire these once, and they'll stay consistent:

  • Site defaults with the Next.js Metadata API
  • Per post generateMetadata mapped from MDX frontmatter
  • Canonical URLs and alternates
  • Open Graph and Twitter images
  • Article JSON-LD for posts
  • First party app/sitemap.ts and app/robots.ts
  • CI checks for missing descriptions, broken links, and sitemap shape

Together, these give you consistent Next.js SEO without adding a CMS.

Site wide defaults with the Metadata API

Define a single source of truth in app/layout.tsx using export const metadata and metadataBase.

export const metadata: Metadata = {
  title: {
    default: 'Brand',
    template: '%s · Brand'
  },
  description: 'What you do in one sentence',
  metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'),
  openGraph: {
    type: 'website',
    url: '/',
    images: ['/og.png']
  },
  twitter: {
    card: 'summary_large_image',
    site: '@brand'
  }
}

Tip: teams often see broken social images on Vercel because image URLs are relative. Set NEXT_PUBLIC_SITE_URL to your production domain so openGraph.images and canonical links resolve to absolute URLs.

Docs: Next.js Metadata overview and API Optimizing: Metadata and generateMetadata.

Per post metadata from MDX

Make generateMetadata build tags from frontmatter in app/blog/[slug]/page.tsx.

Example shape with a safe image fallback:

export async function generateMetadata({ params }): Promise<Metadata> {
  const fm = await getFrontmatter(params.slug)
  const url = '/blog/' + fm.slug
  const image = fm.image || '/og/default.png'
  return {
    title: fm.title,
    description: fm.description,
    alternates: { canonical: url },
    openGraph: {
      type: 'article',
      url,
      title: fm.title,
      description: fm.description,
      images: [image]
    },
    twitter: {
      card: 'summary_large_image',
      title: fm.title,
      description: fm.description,
      images: [image]
    }
  }
}

This keeps Next.js metadata in one place, driven by frontmatter so engineers do not hand write tags.

Add Article JSON-LD

Structured data improves rich result eligibility and helps search engines and AI systems understand your content.

Types: add schema-dts for type safety.

import type { Article } from 'schema-dts'
 
const data: Article = {
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: fm.title,
  description: fm.description,
  datePublished: fm.date,
  dateModified: fm.lastmod || fm.date,
  author: {
    '@type': 'Person',
    name: fm.author
  },
  image: image,
  mainEntityOfPage: new URL(url, process.env.NEXT_PUBLIC_SITE_URL).toString()
}

Render inline in the page component:

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>

Guides: Next.js JSON-LD how to JSON-LD and Google policies Search Gallery.

First party sitemap.xml and robots.txt

Generate both from code with the App Router.

app/sitemap.ts:

import type { MetadataRoute } from 'next'
 
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await allPosts()
  return [
    { url: 'https://example.com/', lastModified: new Date() },
    ...posts.map(p => ({
      url: 'https://example.com/blog/' + p.slug,
      lastModified: p.lastmod || p.date
    }))
  ]
}

app/robots.ts:

import type { MetadataRoute } from 'next'
 
export default function robots(): MetadataRoute.Robots {
  const site = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'
  return {
    rules: { userAgent: '*' },
    sitemap: site + '/sitemap.xml',
    host: site
  }
}

Docs: robots.txt and the sitemap file convention in the metadata section. If you already run next-sitemap, you can keep it for very large or multi domain sites. Otherwise, the built in API is simpler. Migration notes: migrate to App Directory sitemap.

Dynamic OG images without the yak shave

Two options that work well:

  • Static images: one per post in public/og/<slug>.png. Set openGraph.images: ['/og/<slug>.png'] and keep naming deterministic from frontmatter.

  • Dynamic images with @vercel/og: add a route at app/og/route.tsx:

import { ImageResponse } from '@vercel/og'
 
export const runtime = 'edge'
 
export async function GET(request: Request) {
  return new ImageResponse(
    <div>...</div>,
    { width: 1200, height: 630 }
  )
}

Then set openGraph.images to '/og?title=' + encodeURIComponent(fm.title).

Confirm the final URLs are absolute via metadataBase so cards render correctly off site.

Where next-seo and next-sitemap fit now

The App Router covers most tags through the Metadata API. next-seo is still fine if you like component ergonomics or you are migrating a Pages Router site. For new App Router projects, start native. Keep a tiny helper, for example buildArticleMeta(frontmatter): Metadata, so no one repeats openGraph or twitter structures. For advanced multi locale sitemaps or millions of URLs, next-sitemap remains useful.

next-seo repo: next-seo on GitHub.

Automated checks that catch SEO drift

Set up CI checks to catch missing metadata and broken links. Here's an example using GitHub Actions (you can adapt this to your CI platform):

name: seo-checks
on: [pull_request, push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - run: pnpm install --frozen-lockfile
      - run: pnpm typecheck && pnpm lint && pnpm build
      - name: Start preview
        run: pnpm next start & sleep 5
      - name: Metadata lint
        run: pnpm tsx scripts/check-metadata.ts
      - name: Link check
        run: npx linkinator http://localhost:3000 --retry 2 --timeout 4000
      - name: Sitemap smoke test
        run: curl -s http://localhost:3000/sitemap.xml | grep '/blog/'

Notes:

  • The metadata script should fail if title or description are missing, if description.length is outside 70 to 160, or if image is missing for posts.
  • For linkinator, you can switch to lychee if you prefer a Rust binary.
  • Cache pnpm and .next/cache to keep feedback under five minutes.

MDX frontmatter that scales

Pick a stable set of keys and stick to them.

  • Required: title, description, slug, date, image.
  • Optional: lastmod, tags, canonical override.
  • Derived: compute readingTime, url, and og path in one helper.

When frontmatter is stable, your generateMetadata, sitemap entries, and JSON-LD all stay consistent. This is the fastest way to prevent SEO drift and maintain reliable metadata across your site.

How Elevor helps

Elevor writes drafts in your voice and opens pull requests with consistent frontmatter, Next.js metadata, JSON-LD, and a suggested OG image. The PR includes a summary of changes, so a founder can review in minutes. On merge, your site publishes without touching a CMS. Weekly Autopilot keeps the cadence, and you keep full control.

If you care about search engines and AI systems citing your work, keep your content verifiable and structured. We wrote about that here Why AI Citations Matter. For a broader view on lean search strategy, see Startup SEO Strategy for the AI Overviews Era.


Quick start plan

  • Add site defaults in app/layout.tsx with the Metadata API.
  • Implement generateMetadata for app/blog/[slug]/page.tsx from MDX frontmatter.
  • Add Article JSON-LD with an inline script typed via schema-dts.
  • Create app/sitemap.ts and app/robots.ts.
  • Wire a CI job that fails on missing metadata or broken links.
  • Let Elevor open your next content PR so the structure stays correct.

Enhanced by Elevor, verified by DJ Lim.