How to dynamically create a sitemap with Sanity and Remix

Table of contents

In this article, we will explain how to dynamically create a sitemap using both Sanity and Remix.

We’ll learn by following an existing example of a sitemap built for Heavybit’s website, a San Francisco-based VC whose website is built with both technologies.

Remix is a React framework with excellent SSR support that enables the creation of fast websites and apps.

Sanity is a very customizable headless CMS that enables easy content creation and management.

To make the sitemap, we will use Sanity's query language (GROQ) to fetch and render content URLs in the sitemap.

What is a sitemap?

A sitemaps is an XML file that contain the list of a website's URLs.

It is used by search engines like Google to crawl through your website to eventually rank it on its result pages (SERPs).

How to create sitemap dynamically with Remix and Sanity

We can create a sitemap in a few steps only. In this example we use TypeScript as the language:

  1. Create the sitemap.xml file under the app/routes folder.
  2. Write a Sanity query to fetch content URLs in the dataset.
  3. Execute query in Remix' loader function.
  4. Render and return the XML response.

1. Create the sitemap file

You can create the sitemap file under your project's app/route folder with the name "sitemap[.]xml.tsx".

We used [.] because it allows Remix to escape this character and we can use it as route name.

Sitemap XML
Sitemap XML TSX file

2. Write a Sanity query to fetch content URLs

We can fetch data from the dataset with Sanity's query language (GROQ). You can learn more about GROQ from Sanity's docs.

We write a query to fetch all defined slugs from the dataset. After that, we do conditional checks to add correct paths according to the document type.

This query is based on Heavybit’s content, so feel free to adapt it to your Sanity and routing setup.

const slugsQuery = groq`*[defined(slug.current)]{
_type == "podcastShow" => {
"slug": "library/podcasts/" + slug.current
},
_type == "video" => {
"slug": "library/video/" + slug.current
},
_type == "article" => {
"slug": "library/article/" + slug.current
},
_type == "podcastEpisode" => {
"slug": "library/podcasts/" + podcastShow->slug.current + "/" + slug.current
},
_type == "press" => {
"slug": "press/" + slug.current
},
_type == "person" && generate == true => {
"slug": select(teamMember == true => "team/" , "community/") + slug.current
},
_type == "devGuild" => {
"slug": "devguild/" + slug.current
},
_type == "organization" && generate == true => {
"slug": "portfolio/" + slug.current
},
_type == "spotlight" && generate == true => {
"slug": "portfolio/spotlights/" + slug.current
},
_type == "page" => {
"slug": slug.current
}
}`;

Now, all we have to do is to create a function to execute this query with Sanity Client.

We are later going to use this function in Remix' loader function.

export const getSlugs = async () => {
const slugList = await sanityClient.fetch(slugsQuery);
return slugList;
};

3. Execute query in the Remix's loader function

Remix has a function type called loader function. It can be defined to any route and we can use it to fetch/load data server-side from any source.

You can read more about loader functions here.

Now we are going to create the async loader function in sitemap[.]xml file.

export const loader: LoaderFunction = async ({ request }) => {
};

After that, we can call our query function in the loader function to fetch all content URLs.

export const loader: LoaderFunction = async ({ request }) => {
const urls = await getSlugs();
};

4. Render and return the XML response

We can directly return the response in the loader function. All we have to do is use the new Response class, you can read about it here.

It takes two arguments, the first is our XML string and the second is the response header.

Let's create our render XML function. Since it's basic string, we can do it with simple loop.

const renderXML = (slugs: { slug?: string }[]) => {
const url = "https://www.heavyibt.com";
const sourceXML = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${slugs..filter(Boolean).map((item) => `<url>
<loc>${url}/${item.slug}</loc>
</url>`)}
</urlset>`;
return sourceXML;
};

Now it's time to throw the response in our loader function.

We also added cache header to prevent querying Sanity on every request.

export const loader: LoaderFunction = async ({ request }) => {
const slugs = await getSlugs();
return new Response(renderXML(slugs), {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"x-content-type-options": "nosniff",
"Cache-Control": `public, max-age=${60 * 10}, s-maxage=${60 * 60 * 24}`,
},
});
};

In the end the sitemap[.]xml file looks like this.

import { LoaderFunction, Response } from "@remix-run/node";
import { getSlugs } from "~/services";
export const loader: LoaderFunction = async ({ request }) => {
const slugs = await getSlugs();
return new Response(renderXML(slugs), {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"x-content-type-options": "nosniff",
"Cache-Control": `public, max-age=${60 * 10}, s-maxage=${60 * 60 * 24}`,
},
});
};
const renderXML = (slugs: { slug?: string }[]) => {
const url = "https://www.heavyibt.com";
const sourceXML = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${slugs.filter(Boolean).map((item) => `<url>
<loc>${url}/${item.slug}</loc>
</url>`)}
</urlset>`;
return sourceXML;
};

Conclusion

We created functions to fetch data from the Sanity dataset and used Remix' loader function to render and return a sitemap.xml.

Search engines can now use the sitemap file to index and crawl the website, and Heavybit's team doesn't have to manually create sitemaps and submit them to Google Search Console.

Heavybit's dynamically generated sitemap
Heavybit's dynamically generated sitemap

Recent articles

SEO best practices on Sanity

When we build a website with Sanity, we configure SEO best practices to rank higher on search engine result pages.
Omar Benseddik's photo
Omar Benseddik
2022-09-05 · 7 min

Translating Shopify stores with Sanity

At Tinloof, we have an internal library that does a lot of heavy lifting when it comes to building fast Remix websites that have their content managed from Sanity. A while ago, we...
Seif Ghezala's photo
Seif Ghezala
2023-01-31 · 4 min