While I was about to add a sitemap to Codersteps which is built completely using Next.js I thought why not document the process maybe someone else will find it helpful.

The goal of this article is to build a sitemap that reflects the current website structure, which means we will read articles from the database and use their links to generate an updated sitemap.

To make it easier I will work on a scenario of a simple blog with posts at /post/{slug} and static pages /, /about, and /contact.

The concept we will use

I will suppose you already have a working project that uses a database or some other kind of storage to store a list of posts with the following description using Prisma schema.

prisma/schema.prisma
TypeScript
model Post {
  id          Int       @id @default(autoincrement())
  title       String
  slug        String    @unique
  content     String?
  publishedAt DateTime?
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
}

The same idea applies if you're using some other type of ORM or using front-matter with markdown. the important thing here is the structure of the data.

I will keep using Prisma for demonstration cause it's simpler and you will be able to understand the idea even if you've never used it before.

Creating sitemap.xml page

Let's create a new file at pages/sitemap.xml.tsx then we will export a function that will return undefined.

pages/sitemap.xml.tsx
TypeScript
export default function Sitemap() {
  return undefined
}

By default, Next.js will return an HTML response with Content-Type: text/html; charset=utf-8 header and as we now sitemaps are XML files.

So we won't let Next.js render the sitemap page as it would with other pages, and we will use the getServerSideProps function to hook in and return an XML instead.

So let's go easy on it and add the setup for getServerSideProps.

pages/sitemap.xml.tsx
TypeScript
import { GetServerSideProps } from "next";

export default function Sitemap() {
  return undefined;
}

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { req, res } = context;

  res.writeHead(200, { "Content-Type": "application/xml" });
  res.end(`<?xml version="1.0" encoding="UTF-8"?>
    <app>Hello World!</app>
  `);

  return {
    props: {},
  };
};

Now Next.js will return an XML response because we set the Content-Type to XML instead of HTML, and then we sent back the response with a Hello World! XML file.

The res.end will stop the request lifecycle and send back the response with the XML content and the status code we've set.

Generate sitemap using sitemap.js

First, let's install it using npm install sitemap, because it's built with Typescript we won't need to install any extra types 😉.

The sitemap.js package makes generating an XML sitemap from a Javascript object a breeze 😬. to learn more about its features you can always go to the sitemap.js docs.

Generate sitemap for static pages

The sitemap.js provides a SitemapStream class that takes an object of options. we will provide it with the hostname and it will give us back a stream object.

A stream is used to asynchronously read and write data, thus a stream can be readable, writeable, or both.

We also have the streamToPromise function which takes a readable stream of the sitemap metadata (items object) and returns an XML sitemap string.

Let's write the code to generate a sitemap XML for our three static pages, then explain what it's doing a little more.

pages/sitemap.xml.tsx
TypeScript
import { Readable } from "stream";
import { GetServerSideProps } from "next";
import { SitemapStream, streamToPromise, EnumChangefreq } from "sitemap";

export default function Sitemap() {
  return undefined;
}

export type SitemapItemBase = {
  url: string;
  priority?: number;
  changefreq?: EnumChangefreq;
};

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { req, res } = context;

  const pages: SitemapItemBase[] = [
    {
      url: "/",
      changefreq: EnumChangefreq.DAILY,
      priority: 0.8,
    },
    {
      url: "/about",
      changefreq: EnumChangefreq.YEARLY,
      priority: 0.4,
    },
    {
      url: "/contact",
      changefreq: EnumChangefreq.YEARLY,
      priority: 0.4,
    },
  ];

  const items: SitemapItemBase[] = [...pages];

  const sitemapStream = new SitemapStream({
    hostname: `https://${req.headers.host}`,
  });
  const xmlString = await streamToPromise(
    Readable.from(items).pipe(sitemapStream)
  ).then((data) => data.toString());
  res.writeHead(200, { "Content-Type": "application/xml" });
  res.end(xmlString);

  return {
    props: {},
  };
};

First, we created the SitemapItemBase type and we used it to type the pages array that contains our static pages metadata: url, changefreq, and the priority.

Then using the spread operator we added the pages array items to the items array, we did this because we still need to add the dynamic posts metadata later on.

As we explained before we create a sitemapStream giving it the hostname, this will be used to make the relative links to absolute links.

Using the Readable class from the nodejs stream package we create a readable stream from the items array and then pipe it to the sitemapStream.

That will return a readable stream that will be passed to the streamToPromise which itself will return a promise that resolves to the XML sitemap that we use to send to the client.

It's a lot I know, but if you look at the code it's straight forward and if you're familiar with the streaming concept you'll be just fine.

Now when we visit the http://localhost:3000/sitemap.xml page we should see the sitemap that was generated for the static pages.

Generate sitemap for dynamic posts

After understanding the concept behind generating a sitemap with Next.js and sitemap.js it's now easy to scale it we just have to add more items to the items array.

it doesn't matter where that data is coming from as long as it has these three properties url, changefreq, and priority.

Using Prisma.js connection I will fetch the posts from the database and map them to be compatible with the SitemapItemBase type.

Untitled.js
JavaScript
import db from "@/lib/database";

const pages: SitemapItemBase[] = [
  // same items
];

const posts: SitemapItemBase[] = (
  await db.post.findMany({
    where: { publishedAt: { not: null } },
    select: { slug: true },
  })
).map((post) => ({
  url: `/posts/${post.slug}`,
  changefreq: EnumChangefreq.WEEKLY,
  priority: 0.5,
}));

const items: SitemapItemBase[] = [...pages, ...posts];

// Keep the rest the same

This step may differ if you're not using Prisma.js but you got the idea you only need a list of all relative links and you can adapt this step to your situation.

After mapping the slugs array to the SitemapItemBase array we add them to the items array that we use as a data source for our sitemap.xml.

If you have a static website, I think you can do a similar thing just inside Next.js getStaticProps function so you can have it run at the build time.

Conclusion

In this article, we created a sitemap.xml page for static pages and dynamic posts we used Prisma.js to demonstrate where the dynamic posts data is stored but it doesn't have to be.

I hope this article was helpful to you, see you in the next one 👊.