
Photo by Growtika
Next.js SEO
Eric Stober / May 27, 2025
Next.js is a powerful React framework. If you'd like to know more about it check out my Introduction to Next.js post.
In this post we will be covering some of the important steps you can take to improve your Next.js website's SEO. The steps are based on my own implementation of SEO for Next.js web applications. Pleae note: I will only be covering the Next.js App Router in this post.
What is SEO?
SEO is short for search engine optimization, which is the process of improving a website's visibility in the search results of search engines such as Google, Bing, etc. This process helps your website be seen by more users. There are many factors that contribute to SEO, however we will not get into all of those factors in this post.
Meta Tags
Meta tags are HTML tags that provide information about a web page to search engines. These tags are
placed within the <head>
section.
Standard meta tags you should include:
- title
- description
- keywords
- robots
- viewport
- charSet
Meta Tags for Next.js App Router
Static Metdata
Static metadata can be defined by adding export const metadata
inside either page.tsx
or
layout.tsx
:
export const metadata: Metadata = {
title: 'Enter your title',
description:
'Enter your description',
keywords: [
'Enter',
'Your',
'Keywords',
],
openGraph: {
url: 'https://enter-your-domain.com',
type: 'website',
title: 'Enter your title',
description:
'Enter your description',
images: [
{
url: 'Enter your image url',
width: 1200,
height: 630,
alt: 'Enter your image alt'
}
]
},
twitter: {
card: 'summary_large_image',
title: 'Enter your title',
description:
'Enter your description',
images: [
{
url: 'Enter your image url',
width: 1200,
height: 630,
alt: 'Enter your image alt'
}
]
},
alternates: {
canonical: 'https://enter-your-domain.com'
},
icons: {
icon: [
{
url: '/favicon.ico',
type: 'image/x-icon'
}
],
shortcut: [
{
url: '/favicon.ico',
type: 'image/x-icon'
}
]
}
};
Dynamic Metadata
Dynamic metadata can be defined by using the generateMetadata
function. This is great for dynamic
pages such as blog posts [slug]/page.tsx
:
export async function generateMetadata({
params
}: {
params: { slug: string };
}): Promise<Metadata> {
const post = await getPostBySlug(params.slug);
if (!post) {
return {
title: 'Post Not Found | Enter your title',
description: 'The requested post could not be found.'
};
}
const { metadata } = post;
const { title, summary, image } = metadata;
return {
title: `${title} | Enter your title`,
description: summary || 'Read this amazing post on my blog.',
openGraph: {
title: `${title} | Enter your title`,
description: summary || 'Read this amazing post on my blog.',
images: image
? [
{
url: image,
width: 1200,
height: 630,
alt: title
}
]
: undefined
},
twitter: {
card: 'summary_large_image',
title: `${title} | Enter your title`,
description: summary || 'Read this amazing post on my blog.',
images: image ? [image] : undefined
}
};
}
Note: If using the Next.js App Router, the charSet and viewport meta tags are automatically added.
JSON-LD Schema
JSON-LD stands for JavaScript Object Notation for Linked Data. JSON-LS is used to implement schema markup, which helps search engines understand the content of web pages.
You can use this tool to assist with generating a JSON-LD Schema Schema Markup Generator Tool.
Here is an example JSON-LD Schema:
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: 'Enter your title',
image: 'Enter your image url',
datePublished: 'Enter your publish date',
author: {
'@type': 'Person',
name: 'Enter your author name'
},
publisher: {
'@type': 'Organization',
name: 'Enter your name or organization name',
logo: {
'@type': 'ImageObject',
url: 'Enter your image url'
}
},
description: 'enter your descriptions',
url: `https://enter-your-domain.com/enter-your-page`
};
Then place inside the page's HTML:
export default function Page() {
return (
<div>
{/* other parts */}
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</div>
);
}
Test your JSON-LD Schema with Google's Rich Results Test.
Sitemap
A sitemap outlines the structure of your website so that search engines can crawl and index your website more efficiently. The example below will demonstrate how to inlcude both static and dynamic pages in your sitemap.
Define the sitemap.ts
file in the app
folder.
import { getAllPostSlugsAndDates } from '@/lib/posts';
import type { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const defaultPages: MetadataRoute.Sitemap = [
{
url: 'https://enter-your-domain.com',
lastModified: new Date().toISOString(),
changeFrequency: 'monthly',
priority: 1
},
{
url: 'https://enter-your-domain.com/posts',
lastModified: new Date().toISOString(),
changeFrequency: 'monthly',
priority: 0.9
},
{
url: 'https://enter-your-domain.com/contact',
lastModified: new Date().toISOString(),
changeFrequency: 'monthly',
priority: 0.9
}
];
const postSlugs = getAllPostSlugsAndDates();
const postPages = postSlugs.map(post => {
return {
url: `https://enter-your-domain.com/${post.slug}`,
lastModified: post.publishedAt,
changeFrequency: 'daily' as const,
priority: 0.8
};
});
const sitemap = [...defaultPages, ...postPages];
return sitemap;
}
You can access the sitemap via https://enter-your-domain.com/sitemap.xml.
robots.txt
The robots.txt
file tells search engines which pages to crawl and which pages to ignore. You can
prevent search engines from crawling a page by adding it to the disallow list.
Define the robots.ts
file in the app
folder.
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: ['/'],
disallow: ['/admin/']
},
sitemap: ['https://eric-stober.com/sitemap.xml']
};
}
You can access the sitemap via https://enter-your-domain.com/robots.txt.
Conclusion
By implementing the examples outlined in this post you can optimize your Next.js website SEO.
Here is a recap of the important points we touched on:
- Meta Tags
- JSON-LD Schema
- Sitemap
- robots.txt