How to generate sitemap paths in NextJS with Sanity dynamic routes?

9 replies
Last updated: Jan 8, 2021
Anyone know how to create a sitemap when using NextJS with Sanity?
Not a Sanity specific question I know

I found this from Lee Rob
https://leerob.io/blog/nextjs-sitemap-robots
It doesn't generate the paths though just the file name so,
<https://mysite.com/folder/[file]>
AI Update

You're on the right track! The article you found from Lee Robinson shows how to create the sitemap file, but you need to populate it with actual paths from your Sanity content. Here's how to create a dynamic sitemap with Next.js and Sanity:

Generate Dynamic Paths from Sanity

First, create a GROQ query to fetch all your documents that should appear in the sitemap. Add this to your queries.ts file:

export const SITEMAP_QUERY = defineQuery(`
  *[_type in ["page", "post"] && defined(slug.current)] {
    "href": select(
      _type == "page" => "/" + slug.current,
      _type == "post" => "/posts/" + slug.current,
      slug.current
    ),
    _updatedAt
  }
`)

This query dynamically builds the full path for each document type and includes the last updated date.

Create the Sitemap Route

Next.js App Router has a special sitemap.ts route convention that returns a MetadataRoute.Sitemap type. Create app/sitemap.ts:

import { MetadataRoute } from "next";
import { client } from "@/sanity/lib/client";
import { SITEMAP_QUERY } from "@/sanity/lib/queries";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  try {
    const paths = await client.fetch(SITEMAP_QUERY);

    if (!paths) return [];

    const baseUrl = process.env.VERCEL
      ? `https://${process.env.VERCEL_URL}`
      : "http://localhost:3000";

    return paths.map((path) => ({
      url: new URL(path.href!, baseUrl).toString(),
      lastModified: new Date(path._updatedAt),
      changeFrequency: "weekly",
      priority: 1,
    }));
  } catch (error) {
    console.error("Failed to generate sitemap:", error);
    return [];
  }
}

Generate TypeScript Types

Since you created a new query, regenerate your types:

npm run typegen

Test Your Sitemap

Visit http://localhost:3000/sitemap.xml to see your dynamically generated sitemap. You should see properly formatted XML with all your Sanity content paths:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://mysite.com/about</loc>
    <lastmod>2025-01-10T14:13:34.000Z</lastmod>
    <changefreq>weekly</changefreq>
    <priority>1</priority>
  </url>
  <!-- more URLs... -->
</urlset>

The beauty of this approach is that your sitemap automatically updates whenever you publish content in Sanity, and you can customize the changeFrequency and priority values based on your content types.

For more details on SEO optimization with Sanity and Next.js, check out the SEO optimized content with Next.js course.

Show original thread
9 replies
We've set it to an API route inside of Next, with this code:
const client = require('../../client')
const sm = require('sitemap')

const defaultUrls = [
  { url: '/', changefreq: 'daily', priority: 1 },
  { url: '/pricing', priority: 0.5 },
  { url: '/pricing/compare', priority: 0.5 },
  { url: '/docs', priority: 0.7 },
  { url: '/community', priority: 0.7 },
  { url: '/blog/', changefreq: 'weekly',  priority: 0.7 },
]

async function getSitemap() {
  const {routes, blogposts} = await client.fetch(`
  {
    "routes": *[_type == "route" && includeInSitemap],
    "blogposts": *[_type == 'post' && includeInSitemap == true && publishedAt < $now] | order(publishedAt desc) {
      slug
    }
  }
  `)

  const urls = routes.filter(({slug = {}}) => slug.current)
    .reduce((acc, route) => ([...acc, {
      url: route.slug.current,
      priority: route.sitemapPriority || 0.5
    }]), defaultUrls)

  const blogUrls = blogposts
    .filter(({slug = {}}) => slug.current)
    .map(post => {
      return {
        url: `/blog/${post.slug.current}`,
        priority: 0.5
      }
    })

  return sm.createSitemap ({
    hostname: '<https://www.sanity.io>',
    cacheTime: 600000,
    urls: urls.concat(blogUrls)
  })
}


module.exports = function sitemapXML(req, res, next) {
  res.setHeader('Content-Type', 'application/xml')
  getSitemap()
    .then(result => {
      res.send(result.toString())
    })
    .catch(next)
}

So, do I add this to a scripts folder and call it in the
next.config.js
like in Lee's example
user Y
?
I got this working a different way. Not sure if it is the best way or not but I am using
getServerSideProps
on a page file that is called
sitemap.xml.tsx
and set the headers to text/xml.
Also what is
includeInSitemap
in the query?
Could you link an example
user U
?
import sanity from "@sanity/client";
import groq from "groq";

export default function SiteMap() {
  return <div>loading</div>;
}

export async function getServerSideProps({ res }) {
  const baseUrl = `<https://cwp-www.vercel.app>`;

  const query = groq`{
    "pages": *[_type == 'sitePage']{slug},
  	"service": *[_type == 'service']{slug},
    "people": *[_type == 'person' && title->name != 'Service Ambassador']{slug} ,
	  "article": *[_type == 'article']{slug}
    }`;

  const client = sanity({
    projectId: "YOUR PROJECTID",
    dataset: "YOUR DATASET",
    token: "", // or leave blank to be anonymous user
    useCdn: false, // `false` if you want to ensure fresh data
  });

  const urls = await client.fetch(query);

  const pages = urls.pages.map((page) => {
    const slug = page.slug.current === "/" ? "/" : `/${page.slug.current}`;
    return `${baseUrl}${slug}`;
  });

  const service = urls.service.map((page) => {
    const slug = `/service/${page.slug.current}`;
    return `${baseUrl}${slug}`;
  });

  const people = urls.people.map((page) => {
    const slug = `/people/${page.slug.current}`;
    return `${baseUrl}${slug}`;
  });

  const article = urls.article.map((page) => {
    const slug = `/article/${page.slug.current}`;
    return `${baseUrl}${slug}`;
  });

  const locations = [...pages, ...service, ...people, ...article];

  const createSitemap = () => `<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="<http://www.sitemaps.org/schemas/sitemap/0.9>">
        ${locations
          .map((location) => {
            return `<url>
                        <loc>${location}</loc>
                        <changefreq>weekly</changefreq>
                    </url>
                  `;
          })
          .join("")}
    </urlset>
    `;
  res.setHeader("Content-Type", "text/xml");
  res.write(createSitemap());
  res.end();

  return {
    props: {},
  };
}
That's great
user U
, do you mind if I document this on my blog?
I'll credit you, do you have somewhere I can point to or will just your name there be enough?
Cool!
Yea I don’t mind. Just the name would be cool. Thanks!

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?