How to build a dynamic sitemap generator in Next.js and Sanity CMS

An XML sitemap is a crucial component of SEO. It, along with a robots.txt file, ensures Google can find and crawl all the pages and aids it in understanding the structure of your website. For tiny sites like this one, it's not as important, but it's still good practice to have one on every site you build.
I had a tough time finding articles on building dynamic sitemaps for a blog that uses Next.js 14 and Sanity v3. While Next.js and Sanity strongly support their products working in tandem, technical SEO isn't well-documented.
Running the posts query with the react-loader
like I did in the blog pages proved problematic. Besides, I only needed the slug and _updatedAt field, not the whole post. So, in the end, I decided to run a new query right in my sitemap.ts file.
Ok, without further ado, here's how it works:
In the root of the app folder, create a file called sitemap.ts
.
Import MetadataRoute
, SanityDocument
, and the client
you set up when you integrated Sanity into your project.
import { MetadataRoute } from 'next';
import type { SanityDocument } from '@sanity/client';
import { client } from '@/sanity/lib/client';
Now, grab your data from Sanity with the following GROQ query:
async function getData() {
const query = `*[_type == "post"] {
"currentSlug": slug.current,
"updated": _updatedAt
}`;
const data = await client.fetch(query);
return data;
}
Next, create an async
function, and call it sitemap()
or whatever:
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
}
Call your getData()
function:
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const data = await getData()}
}
Then map through the data:
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const data = await getData();
const posts: MetadataRoute.Sitemap = data.map((post: SanityDocument) => ({
url: `https://erikjonsberg.dev/posts/${post.currentSlug}`,
lastModified: post.lastModified,
changeFrequency: 'weekly',
priority: 0.9,
}));
};
Return the static pages:
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const data = await getData();
const posts: MetadataRoute.Sitemap = data.map((post: SanityDocument) => ({
url: `https://erikjonsberg.dev/posts/${post.currentSlug}`,
lastModified: post.lastModified,
changeFrequency: 'weekly',
priority: 0.9,
}));
return [
{
url: 'https://erikjonsberg.dev/',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 1,
},
{
url: 'https://erikjonsberg.dev/contact',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.8,
},
{
url: 'https://erikjonsberg.dev/posts',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.9,
},
];
}
And finally, return the posts:
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const data = await getData();
const posts: MetadataRoute.Sitemap = data.map((post: SanityDocument) => ({
url: `https://erikjonsberg.dev/posts/${post.currentSlug}`,
lastModified: post.lastModified,
changeFrequency: 'weekly',
priority: 0.9,
}));
return [
{
url: 'https://erikjonsberg.dev/',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 1,
},
{
url: 'https://erikjonsberg.dev/contact',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.8,
},
{
url: 'https://erikjonsberg.dev/posts',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.9,
},
...posts,
];
}
If all goes well, go to http://localhost:3000/sitemap.xml and see your shiny new sitemap that will now update whenever you add or edit a post.

Thanks for reading. I hope you found this article helpful and enjoyable.