
SEO basics for a Next.js site
A guide to URL rewrite rules, sitemaps, robots.txt, page metadata, structured data, and file-based metadata for a Next.js site.
Next.js has a built-in Metadata API that handles many SEO elements well. This guide covers the essentials: URL rewrite rules, sitemaps, robots.txt, page metadata, and structured data.
URL rewrite rules
A site can be accessed via multiple URL variations: www and non-www, trailing slash and non-trailing slash. For example, a page called "Penguins" could be reached at:
- https://www.example.co.uk/penguins
- https://www.example.co.uk/penguins/
- https://example.co.uk/penguins
- https://example.co.uk/penguins/
A redirect rule that resolves all variations to a single canonical version prevents duplicate content and protects organic ranking potential.
Open your next.config.js (or next.config.ts for TypeScript) and add an async redirects function:
async redirects() {
return [
{
source: "/:path*",
has: [{ type: "host", value: "example.co.uk" }],
destination: "https://www.example.co.uk/:path*",
permanent: true,
}
]
}Full documentation: nextjs.org/docs/app/api-reference/next-config-js/redirects
Sitemap and robots.txt
Search engines use these files to understand which parts of a site to crawl and index. Set up a Google Search Console and Bing Webmaster account to submit your sitemap directly.
Next.js provides native file conventions for both. Create these files in your app directory.
app/sitemap.ts
For a straightforward site with known routes, a static sitemap works well:
import type { MetadataRoute } from "next"
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: "https://www.example.co.uk",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 1.0,
},
{
url: "https://www.example.co.uk/blog",
lastModified: new Date(),
changeFrequency: "daily",
priority: 0.8,
},
]
}For sites with dynamic content (a CMS-driven blog, for example), fetch your pages and generate the entries at build time:
import type { MetadataRoute } from "next"
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await fetchAllPosts()
const postEntries = posts.map((post) => ({
url: `https://www.example.co.uk/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: "weekly" as const,
priority: 0.7,
}))
return [
{ url: "https://www.example.co.uk", lastModified: new Date(), priority: 1.0 },
...postEntries,
]
}app/robots.ts
import type { MetadataRoute } from "next"
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://www.example.co.uk/sitemap.xml",
}
}These files generate /sitemap.xml and /robots.txt automatically. No plugins or postbuild scripts needed.
For larger sites with thousands of pages, the generateSitemaps function splits output across multiple sitemap files with an index. See the Next.js sitemap documentation for details.
Page metadata and OG tags
The Metadata API lets you define titles, descriptions, and Open Graph tags per page directly in your page files.
For static pages, export a metadata object:
export const metadata = {
title: "Blog",
description: "Articles on search marketing and growth strategy",
}For dynamic pages where metadata depends on route parameters or CMS data, use generateMetadata:
import type { Metadata } from "next"
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.date,
images: post.coverImage ? [{ url: post.coverImage }] : undefined,
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
},
}
}Set character limits for meta titles (50-70 characters) and descriptions (120-170 characters) including spaces. These limits help your metadata display fully in search results.
A root layout can define a title template that applies across all child pages:
// app/layout.tsx
export async function generateMetadata(): Promise<Metadata> {
return {
title: { template: "%s | Brand Name", default: "Brand Name" },
description: "Your site description",
openGraph: { images: ["/og-default.jpg"] },
}
}Child pages then only set their specific title and description. The template wraps them automatically.
Full documentation: nextjs.org/docs/app/api-reference/functions/generate-metadata
Structured data with JSON-LD
Structured data helps search engines understand the content type and context of a page. JSON-LD is the recommended format.
Add a <script> tag with type="application/ld+json" to your page component:
export default function ArticlePage({ post }) {
const schema = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.excerpt,
datePublished: post.date,
author: {
"@type": "Person",
name: post.author,
},
publisher: {
"@type": "Organisation",
name: "Your Brand",
},
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
<article>{/* page content */}</article>
</>
)
}Common schema types for business sites include BlogPosting, Organisation, FAQPage, and Product. Each has specific required and recommended fields.
Validate your structured data with Google's Rich Results Test or the Schema Markup Validator.
Full documentation: nextjs.org/docs/app/guides/json-ld
File-based metadata
Next.js recognises specific filenames in the app directory as metadata. Drop these files in and they're picked up automatically:
favicon.ico/icon.pngfor faviconsopengraph-image.pngfor a default Open Graph imagetwitter-image.pngfor a default Twitter card imagemanifest.tsfor a web app manifest
For dynamic OG images generated per page, create an opengraph-image.tsx file that exports an ImageResponse:
import { ImageResponse } from "next/og"
export const size = { width: 1200, height: 630 }
export const contentType = "image/png"
export default async function Image({ params }) {
const post = await getPost(params.slug)
return new ImageResponse(
(
<div style={{ fontSize: 48, background: "white", width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", padding: 48 }}>
{post.title}
</div>
),
{ ...size }
)
}Full documentation: nextjs.org/docs/app/api-reference/file-conventions/metadata


