3 Super Simple Steps To Optimize Your Next.js Website for SEO



Objectively, SEO can be thought of as the foundation of a successful online presence, ensuring that businesses, bloggers, and content creators reach the right audience at the right time.
Whether you're running an e-commerce store, managing a corporate website, or maintaining a personal blog, SEO serves as the bridge between your content and the millions of users searching for information, products, or services online. Some of those users are probably searching for articles just like this one right now, and without taking proper SEO precautions, none of them would be able to find this post—or the content in it that they're looking for.
Luckily, Next.js has some pretty simple (and effective) tricks up its sleeves in order to make this as painless as possible.
First, some assumptions. Let's assume you're utilizing TypeScript, and you want to be able to write simple, dynamic code—not just static text and XML. If that's correct, please proceed.
1. Create a sitemap.xml
file
Why
A sitemap.xml file is a crucial component of SEO that serves as a roadmap for search engine crawlers, helping them efficiently and correctly discover and index your site. They can also contain helpful metadata (such as the frequency in which the pages are updated and their last modified date) and in some instances different types of files such as PDFs, images, and videos to ensure that non-text content appears in specialized search results (like image or video searches).
How
Ok, I lied.
First things first please don't actually create an XML file, lol.
Instead, in order to take full advantage of what Next.js has to offer you're going to want to actually create a sitemap.ts
(TypeScript) file. There are a few places you could put it, but if you're not sure and you do have a src/app
directory, put it there. When we're finished Next.js will generate the XML file in your root directory during the build step so that the final URL ends up being your-domain.com/sitemap.xml
.
Next up is the code. First, let's add just our homepage. This will be the base URL of our site.
// sitemap.ts
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com',
lastModified: '2025-01-23',
changeFrequency: 'yearly',
},
]
}
...and BOOM! We're done, right?
Well yes, this will generate a perfect, very tiny sitemap file that only has your homepage in it and no other pages. We could add more individual pages such as "about us", "careers", etc....but what about dynamic pages such as blog posts or product detail pages? Do we have to manually add each post to that return array? Nope. See below:
// sitemap.ts
import type { MetadataRoute } from 'next';
import { getAllPosts } from "@/lib/api";
import { BASE_URL } from '@/lib/constants';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const allPosts = getAllPosts();
const allPostSlugs = allPosts.map(post => post.slug);
const postSitemap = allPostSlugs.map(slug => ({
url: `${BASE_URL}/posts/${slug}`,
changeFrequency: 'monthly' as const,
}));
return [
{
url: BASE_URL,
changeFrequency: 'yearly',
},
...postSitemap,
]
}
Walking through the above file and it's improvements, we imported the getAllPosts()
function that allows us to get data about all our blog posts. We also moved our homepage URL into a variable called BASE_URL
in our constants file so we only ever have to modify it in that once place if we need to change it.
We then set the function as an async funtion that returns a promise, run getAllPosts()
, and map through them to get the slug from each one in an array. Then, for each slug in the array we create an object for the sitemap that contains the URL of the post with the slug and it's changeFrequency
and any other details you may want to add such as lastModified
, priority
, etc. After that we just return an array that includes the homepage and any other single pages as we did before and then used the spread operator to add the posts, and ... that's it!
Hopefully that explaination made sense. For more on the topic, check out the Next.js sitemap documentation.
2. Create a robots.txt
file
Why
The robots.txt
guides search engine crawlers down the right path and helps prioritize what should be crawled. With proper use of the file you're able to block crawlers from accessing unnecessary or low-priority pages (such as duplicate content, admin areas, etc.), speed up crawling by excluding less important resources, support development and testing by blocking certain environments, and more.
How
Once again, we won't actually be creating a .txt
file manually. Start of by creating a robots.ts
(TypeScript again) file and placing it in that same directory as the sitemap.ts
file.
You can get really detailed with the number of allowd / disallowed entries in your file, but we're goibng to keep it relatively simple for this article:
// robots.ts
import type { MetadataRoute } from 'next';
import { BASE_URL } from '@/lib/constants';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: ['/', '/posts/'],
disallow: [],
},
sitemap: `${BASE_URL}/sitemap.xml`,
}
}
As you can see above we're importing that same BASE_URL
constant and returning an object with a single rules
object containing userAgent
(this is referring to the different crawlers, I'm allowing all of them here with the same rules), allow
(specifying which paths / pages to allow indexing to occur), and disallow
(just leaving this as an empty array for now, as I don't need to prevent any pages from being indexed).
Finally, inside that same object you can see there's a reference to where the sitemap is located.
And that's all for that! If you'd like to dive deeper into other settings or options, check out the Next.js robots.txt documentation.
3. Generate Metadata
Why
Adding the proper metadata and OpenGraph (OG) data to your website is very beneficial for SEO. It enhances how search engines and social platforms understand and display your content to your users. Adding relevant and context-specific titles and descriptions help users find what they're looking for. Adding items like url-specific images help add context to users when links are shared. And adding some types of metadata (like from Schema.org) helps add details to your search results that stand out such as rich snippets, star ratings, pricing, and product availability.
How
Once again we have a choice to use static markdown or dynamic. Once again, we'll choose dynamic since that's the more flexible and managable option, especially for larger sites that contain many pages.
Static Metadata
In our Next.js v15 we are starting out with an app/layout.tsx
file, and will put our static/default metadata in there. Doing this will allow Next to automatically generate the relevant <head>
elements for your pages.
// app/layout.tsx
import type { Metadata } from "next";
import {
BASE_URL,
HOME_OG_IMAGE_URL,
META_DESCRIPTION,
META_KEYWORDS,
META_TITLE_DEFAULT
} from "@/lib/constants";
export const metadata: Metadata = {
metadataBase: new URL(BASE_URL),
alternates: {
canonical: '/',
},
title: {
default: META_TITLE_DEFAULT,
template: '%s | ' + META_TITLE_DEFAULT,
},
description: META_DESCRIPTION,
keywords: META_KEYWORDS,
openGraph: {
description: META_DESCRIPTION,
images: [HOME_OG_IMAGE_URL],
},
};
Here you can see a pretty bare-bones but very functional metadata object. We're being consistent and defining our constants in a separate file and utilizing them all here. One thing to note is the %s
under the title template. This adds the name of your blog posts or articles before the rest of the default title, which is a nice touch. So for instance if the default title for your site was "MY SITE" and you had a blog post titled "Hello, World!" the title of that post would read "Hello, World! | MY SITE".
The OpenGraph description and images will show up when your page is linked on a social platform like Slack, Facebook, etc.
Dynamic Metadata
Now we just need to add a bit more code elsewhere to finish setting up the metadata for our dynamic/templatized pages such as articles and blog posts.
export async function generateMetadata(props: Params): Promise<Metadata> {
const params = await props.params;
const post = getPostBySlug(params.slug);
if (!post) {
return notFound();
}
const title = `${post.title}`;
return {
title,
keywords: post.tags.concat(META_KEYWORDS),
openGraph: {
title,
description: post.subTitle,
images: [post.ogImage.url],
},
};
}
Walking through this code snippet, we're waiting for the passed-in params and using them to pass the slug into a getPostBySlug()
function that our code already has. If there are no posts returned for that slug, we return the notFound()
function.
However if there is a post, we get the title, and return an object with the title, keywords, and OpenGraph details. Everywhere you see post.
is a reference to the actual article / blog post and the unique data that we enter on each one that is specific to that post.
For instance, here is the specific metadata for this very post you're reading:
---
title: "3 Super Simple Steps To Optimize Your Next.js Website for SEO"
subTitle: "Help your users find what they're looking for!"
excerpt: "SEO serves as the bridge between your content and the millions of users searching for information, products, or services online."
tags: ["seo", "next", "nextjs", "react", "reactjs", "metadata", "optimization", "optimize", "sitemap", "robots.txt", "simple", "search engine optimization"]
coverImage: "/assets/blog/seo-nextjs/cover-o.jpg"
date: "2025-01-23"
author:
name: Ryan Chapel
picture: "/assets/blog/authors/chaps.jpg"
ogImage:
url: "/assets/blog/seo-nextjs/cover-o.jpg"
---
For more information about Next.js metadata configuration, see their docs.
And that's a wrap! It looks like a lot of code on this website, but it's really pretty simple and straightforward. Adding these three steps will make your site much more accessible and findable to your audience, so what do you have to lose?
I hope this helps. There are many MANY more details about SEO we didn't get to in this article that may be included in a future article, so keep checking back for more.
If you found this useful, let me know! @chapeljuice.dev on Bluesky.