Create a sitemap from Sanity CMS with Workers
In this tutorial, you will put together a Cloudflare Worker that creates and serves a sitemap using data from Sanity.io ↗, a headless CMS.
The high-level workflow of the solution you are going to build in this tutorial is the following:
- A URL on your domain (for example,
cms.example.com/sitemap.xml
) will be routed to a Cloudflare Worker. - The Worker will fetch your CMS data such as slugs and last modified dates.
- The Worker will use that data to assemble a sitemap.
- Finally, The Worker will return the XML sitemap ready for search engines.
Before you start, make sure you have:
- A Cloudflare account. If you do not have one, sign up ↗ before continuing.
- A domain added to your Cloudflare account using a full setup, that is, using Cloudflare for your authoritative DNS nameservers.
- npm ↗ and Node.js ↗ installed on your machine.
Cloudflare Workers provides a serverless execution environment that allows you to create new applications or augment existing ones without configuring or maintaining infrastructure.
While you can create Workers in the Cloudflare dashboard, it is a best practice to create them locally, where you can use version control and Wrangler, the Workers command-line interface, to deploy them.
Create a new Worker project using C3 (create-cloudflare
CLI):
In this tutorial, the Worker will be named cms-sitemap
.
Select the options in the command-line interface (CLI) that work best for you, such as using JavaScript or TypeScript. The starter template you choose does not matter as this tutorial provides all the required code for you to paste in your project.
Next, require the @sanity/client
package.
A default wrangler.toml
was generated in the previous step.
The wrangler.toml
file is a configuration file used to specify project settings and deployment configurations in a structured format.
For this tutorial your wrangler.toml
should be similar to the following:
You must update the [vars]
section to match your needs. See the inline comments to understand the purpose of each entry.
In this step you will add the boilerplate code that will get you close to the complete solution.
For the purpose of this tutorial, the code has been condensed into two files:
index.ts|js
: Serves as the entry point for requests to the Worker and routes them to the proper place.Sitemap.ts|js
: Retrieves the CMS data that will be turned into a sitemap. For a better separation of concerns and organization, the CMS logic should be in a separate file.
Paste the following code into the existing index.ts|js
file:
You do not need to modify anything in this file after pasting the above code.
Next, create a new file named Sitemap.ts|js
and paste the following code:
In steps 4 and 5 you will modify the code you pasted into src/Sitemap.ts
according to your needs.
The following query in src/Sitemap.ts
defines which data will be retrieved from the CMS. The exact query depends on your schema:
If necessary, adapt the provided query to your specific schema, taking the following into account:
- The query must return two properties:
slug
andlastmod
, as these properties are referenced when creating the sitemap. GROQ ↗ (Graph-Relational Object Queries) and GraphQL ↗ enable naming properties — for example,"lastmod": _updatedAt
— allowing you to map custom field names to the required properties. - You will likely need to prefix each slug with the base path. For
www.example.com/posts/my-post
, the slug returned ismy-post
, but the base path (/posts/
) is what needs to be prefixed (the domain is automatically added). - Add a sort to the query to provide a consistent order (
order(slug asc)
in the provided tutorial code).
The data returned by the query will be used to generate an XML sitemap.
The relevant code from src/Sitemap.ts
generating the sitemap and returning it with the correct content type is the following:
The URL (loc
) and last modification date (lastmod
) are the only two properties added to the sitemap because, according to Google ↗, other properties such as priority
and changefreq
will be ignored.
Finally, the sitemap is returned with the content type of application/xml
.
At this point, you can test the Worker locally by running the following command:
This command will output a localhost URL in the terminal. Open this URL with /sitemap.xml
appended to view the sitemap in your browser. If there are any errors, they will be shown in the terminal output.
Once you have confirmed the sitemap is working, move on to the next step.
Now that your project is working locally, there are two steps left:
- Deploy the Worker.
- Bind it to a domain.
To deploy the Worker, run the following command in your terminal:
The terminal will log information about the deployment, including a new custom URL in the format {worker-name}.{account-subdomain}.workers.dev
. While you could use this hostname to obtain your sitemap, it is a best practice to host the sitemap on the same domain your content is on.
In this step, you will make the Worker available on a new subdomain using a built-in Cloudflare feature.
One of the benefits of using a subdomain is that you do not have to worry about this sitemap conflicting with your root domain’s sitemap, since both are probably using the /sitemap.xml
path.
-
Log in to the Cloudflare dashboard ↗ and select your account.
-
In Account Home, select Workers & Pages, and then select your Worker.
-
Go to Settings > Triggers > Custom Domains > Add Custom Domain.
-
Enter the domain or subdomain you want to configure for your Worker.
For this tutorial, use a subdomain on the domain that is in your sitemap. For example, if your sitemap outputs URLs like
www.example.com
then a suitable subdomain iscms.example.com
. -
Select Add Custom Domain.
After adding the subdomain, Cloudflare automatically adds the proper DNS record binding the Worker to the subdomain.
-
To verify your configuration, go to your new subdomain and append
/sitemap.xml
. For example:
The browser should show the sitemap as when you tested locally.
You now have a sitemap for your headless CMS using a highly maintainable and serverless setup.