---
title: "SEO basics for a Next.js site"
description: "A guide to URL rewrite rules, sitemaps, robots.txt, page metadata, structured data, and file-based metadata for a Next.js site."
image: "https://cdn.sanity.io/images/6kxxmp0v/production/df2121aa147e8c12aa027d04d34c39eaa5a1671b-1920x1097.png?rect=0,48,1920,1003&w=1200&h=627&fit=crop&auto=format"
---

# SEO basics for a Next.js site

*By Hannah Reed, HGDR Consulting · Published 2024-03-15 · Source: https://www.hgdrconsulting.co.uk/blog/seo/seo-basics-for-a-nextjs-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:

```javascript
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](https://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](https://search.google.com/search-console/about) and [Bing Webmaster](https://www.bing.com/webmasters/help/add-and-verify-site-12184f8b) 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:

```typescript
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:

```typescript
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

```typescript
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:

```typescript
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`:

```typescript
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:

```typescript
// 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](https://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:

```tsx
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](https://search.google.com/test/rich-results) or the [Schema Markup Validator](https://validator.schema.org/).

Full documentation: [nextjs.org/docs/app/guides/json-ld](https://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.png` for favicons
- `opengraph-image.png` for a default Open Graph image
- `twitter-image.png` for a default Twitter card image
- `manifest.ts` for a web app manifest

For dynamic OG images generated per page, create an `opengraph-image.tsx` file that exports an `ImageResponse`:

```tsx
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](https://nextjs.org/docs/app/api-reference/file-conventions/metadata)

```json
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "SEO basics for a Next.js site",
  "description": "A guide to URL rewrite rules, sitemaps, robots.txt, page metadata, structured data, and file-based metadata for a Next.js site.",
  "datePublished": "2024-03-15T15:12:00.000Z",
  "dateModified": "2026-04-13T12:50:36.298Z",
  "author": {
    "@type": "Person",
    "name": "Hannah Reed"
  },
  "publisher": {
    "@type": "Organization",
    "name": "HGDR Consulting",
    "logo": {
      "@type": "ImageObject",
      "url": "https://www.hgdrconsulting.co.uk/img/HGDR-Consulting-logo.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://www.hgdrconsulting.co.uk/blog/seo/seo-basics-for-a-nextjs-site"
  },
  "articleSection": "SEO",
  "keywords": "",
  "wordCount": 456,
  "inLanguage": "en-GB",
  "image": "https://cdn.sanity.io/images/6kxxmp0v/production/df2121aa147e8c12aa027d04d34c39eaa5a1671b-1920x1097.png?rect=0,48,1920,1003&w=1200&h=627&fit=crop&auto=format"
}
```
