Sitemaps are crucial for helping search engines crawl and index your website. However, generating and managing them can be challenging in a headless WordPress setup.
In this article, you’ll learn how to build simple sitemaps for Headless WordPress using Next.js’ App Router and WPGraphQL.
Table of Contents
Prerequisites
Before you begin, make sure you have:
- Basic command line experience
- Foundational knowledge of headless WordPress
- Familiarity with Next.js App Router
Steps for setting up:
1. Set up a WordPress site and get it running. Local by WP Engine is a good local dev environment to use. In your WP admin, add content to the posts and pages.
2. Install and activate the WPGraphQL plugin.
3. Clonethe repo for this project by copying and pasting this command in your terminal:
npx degit Fran-A-Dev/sitemaps-headlesswp-next14 my-project
4. You will find .env.local.example
file inside the root of the Next.js project with the environment variables below.
Replace “https://your.wordpress.site”
with the address of your WordPress site then delete the .example
so that it becomes your actual .env.local
file.
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_GRAPHQL_ENDPOINT=https://your.wordpress.site/graphql
Code language: JavaScript (javascript)
6. Run npm install
to install the dependencies.
7. Run npm run dev
to get the server running locally.8. You should now be able to visit the path http://localhost:3000/sitemap.xml in a web browser and see your sitemap:
Overall Project Structure
Before we get into creating the sitemap, let’s quickly break down the overall structure of our project to give us insight into the pages and posts that will show up in our sitemap.
├── app/
│ ├── [...uri]/
│ ├── about/
│ ├── post/[uri]
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.jsx
│ ├── loading.jsx
│ ├── page.jsx
│ └── sitemap.js
[...uri]/page.jsx
: The catch-all route in app/[...uri]/page.jsx
captures any URL path and uses it to fetch and display the corresponding WordPress page content. It converts URL segments into a WordPress URI, queries the GraphQL API for the page data, and renders the content. This allows our Next.js app to serve any WordPress page at any URL path.
about/page.jsx
: Our hardcoded Next.js page which will serve as our Next.js path in our sitemap
post/[uri]/page.jsx
: This file handles individual WordPress post pages by fetching post content via GraphQL API based on the URI parameter and rendering it with the post title and content.
page.jsx
: Our home page that renders post titles and links us to their single post detail page.
The Next.js sitemap.js file
Next.js provides the sitemap.(js|ts)
file convention to programmatically generate a sitemap by exporting a default function that returns an array of URLs. If using TypeScript, a Sitemap
type is available. This is what we are using in our project.
Let’s break down our code. Navigate to app/sitemap.js
in your code editor:
const GET_ALL_CONTENT = `
query GetAllContent {
posts(first: 100) {
nodes {
uri
modified
title
}
}
pages(first: 100) {
nodes {
uri
modified
title
}
}
}
`;
Code language: JavaScript (javascript)
This GraphQL query fetches the first 100 posts and pages from your WordPress backend using WPGraphQL. For each entry, it grabs the uri
, modified
timestamp, and title
. The uri will be used to construct full URLs for the sitemap.
Following that, we have the main function that Next calls when generating the sitemap. This pulls from your environment variables or defaults to localhost:3000
if none is set. Then we create a basic staticRoutes
array that includes just the homepage. This route has high SEO priority, a changeFreq
of "daily"
, and a lastModified
timestamp set to the current date.
Next, we send a POST request to your WordPress GraphQL endpoint. The request includes standard headers and a JSON payload with our GET_ALL_CONTENT
query. We’re also using Next.js’ revalidate option to tell it to regenerate the sitemap every 60 seconds (an ISR strategy).
Once the response comes back, we parse it as JSON. If there are any GraphQL errors in the response, we log them and gracefully fall back to returning only the homepage entry. This ensures that even if the fetch fails, your app won’t break and search engines still get something valid back.
export default async function sitemap() {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000";
// Single homepage entry
const staticRoutes = [
{
url: baseUrl,
lastModified: new Date(),
changeFreq: "daily",
priority: 1,
},
];
try {
const response = await fetch(process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: GET_ALL_CONTENT,
}),
next: { revalidate: 60 },
});
const json = await response.json();
if (json.errors) {
console.error("GraphQL Errors:", json.errors);
return staticRoutes;
}
Code language: JavaScript (javascript)
If all goes well, we process the response data next. For posts, we map each one into a route object that includes its full URL, the last modified date (if available), and a few SEO properties like changeFreq: "weekly"
and priority: 0.7
. Trailing slashes are cleaned up for consistency.
const data = json.data;
const postRoutes = data.posts.nodes.map((post) => ({
url: `${baseUrl}${post.uri}`.replace(/\/+$/, "/"),
lastModified: post.modified || new Date(),
changeFreq: "weekly",
priority: 0.7,
}));
Code language: JavaScript (javascript)
We do something similar for pages, but with a twist: we first filter out known duplicate URIs like the homepage or front page. Pages are typically less volatile than blog posts, so we give them a slightly higher priority of 0.8 but a lower update frequency of “monthly”.
const pageRoutes = data.pages.nodes
.filter((page) => !["/", "/front-page/", "/home/"].includes(page.uri))
.map((page) => ({
url: `${baseUrl}${page.uri}`.replace(/\/+$/, "/"),
lastModified: page.modified || new Date(),
changeFreq: "monthly",
priority: 0.8,
}));
Code language: JavaScript (javascript)
Once we have all our routes — the static homepage, the posts, and the pages — we combine them into one big array.
const allRoutes = [...staticRoutes, ...pageRoutes, ...postRoutes];
To prevent any potential duplicates (like /about vs /about/), we use a Map() to normalize and deduplicate the URLs. The result is a clean, unique list of all sitemap entries.
const uniqueRoutes = Array.from(
new Map(
allRoutes.map((route) => [route.url.replace(/\/+$/, ""), route])
).values()
);
Code language: JavaScript (javascript)
Finally, we return the unique list of routes. If anything goes wrong in the try block — whether it’s a failed network request, a JSON parsing issue, or something else — we catch the error and fall back to serving just the homepage.
return uniqueRoutes;
} catch (error) {
console.error("Error generating sitemap:", error);
return staticRoutes;
}
Code language: JavaScript (javascript)
Conclusion
We hope this article helped you understand how to create a simple sitemap in headless WordPress with WPGraphQL and Next.js App Router!
As always, we’re super stoked to hear your feedback and learn about the headless projects you’re working on, so hit us up in the Headless WordPress Discord!