Next.js 9.3 - generating a sitemap from dynamic content

Next.js 9.3 - generating a sitemap from dynamic content

Posted on by Petter Kjelkenes - last updated 01. April, 2020

There are a few ways we could generate a sitemap.xml using next.js.

  • Use a custom npm script that generates a sitemap.xml inside the public/ folder. This could work if you build your project on every content change in the CMS.
  • Utilize the pages folder (dynamic routes), creating a sitemap.xml.tsx file. The benefits is that we can generate sitemap without building our next app. We can cache the generated sitemap, if you have a statically generated next app.

We will look at how we can utlize the dynamic routes and use sanity as our CMS.

Install the packages

npm install --save sitemap


Lets create our sitemap file (typescript):

src/pages/sitemap.xml.tsx

import { GetServerSideProps } from "next";
import { SitemapStream, streamToPromise } from "sitemap";
import { createGzip } from "zlib";
import sanity from "../lib/sanity";

const Sitemap = () => {
  return <div>Should not be navigated trough via next Link. Use standard a href.</div>;
};
export default Sitemap;

let sitemap: Buffer | null = null;

const queryAllPages = `*[_type == "page" && slug.current != ''] {
  'slug': slug.current
} | order(_createdAt desc)`;

const queryAllPosts = `*[_type == "post" && slug.current != ''] {
  'slug': slug.current
} | order(_createdAt desc)`;

const mergedQuery = `
{
  "posts": ${queryAllPosts},
  "pages": ${queryAllPages}
}
`;

interface IData {
  pages: [{ slug: string }];
  posts: [{ slug: string }];
}

const addUrls = async (smStream: SitemapStream) => {
  const data: IData = await sanity.fetch<IData>(mergedQuery);
  const pageSlugs = data.pages.map((page) => page.slug);
  for (const slug of pageSlugs) {
    smStream.write({ url: `/${slug}` });
  }
  const postSlugs = data.posts.map((page) => page.slug);
  for (const slug of postSlugs) {
    smStream.write({ url: `/p/${slug}` });
  }
};

export const getServerSideProps: GetServerSideProps = async ({ res, req }) => {
  if (!req || !res) {
    return {
      props: {},
    };
  }
  res.setHeader("Content-Type", "application/xml");
  res.setHeader("Content-Encoding", "gzip");

  // If our sitemap is cached, we write the cached sitemap, no query to the CMS.
  if (sitemap) {
    res.write(sitemap);
    res.end();
    return {
      props: {},
    };
  }
  const smStream = new SitemapStream({ hostname: `https://${req.headers.host}/` });
  const pipeline = smStream.pipe(createGzip());

  try {
    await addUrls(smStream);
    smStream.end();
    const resp = await streamToPromise(pipeline);

    // cache the sitemap response (cache will be gone on next build.
    // This cache is only useful if your content is static, and you must build the next app on every content change in the cms
    sitemap = resp;

    res.write(resp);
    res.end();
  } catch (error) {
    console.log(error);
    res.statusCode = 500;
    res.write("Could not generate sitemap.");
    res.end();
  }

  return {
    props: {},
  };
};

So what is happening here?

const Sitemap = () => {
  return <div>Should not be navigated trough via next Link. Use standard a href.</div>;
};
export default Sitemap;

First of all, Next.js expects a component to be returned as a default export. Here we will export a component that says that this page can not be navigated via client side routing. This is because if we use the <Link> component this should never be generated on client side, and thus displaying this errror.

let sitemap: Buffer | null = null;

In this variable we store the generated sitemap, we only query the CMS the first time sitemap.xml is loaded from the URL /sitemap.xml. And then we store the result inside this sitemap variable. Note that this requires a new build in production, if you want to update the sitemap. For my case, i actually want this to happen because all my content is statically generated! If your content is not statically generated, you could omit this, and create some kind of TTL cache, so e.g. only generate a new sitemap every hour or so.


  return {
    props: {},
  };

So whats up with these return statements? Next.js expects props to be returned from getServerSideProps, but above each return we use res.end(), which will actually send the request as is before rendering the component. Which we want to do because we generate XML and not HTML from the component.

Comments