Ten years of WordPress, and I never once thought hard about how a sitemap actually gets built.
Install Rank Math, flip the sitemap toggle, done. The sitemap existed somewhere at domain.com/sitemap_index.xml and Google found it eventually. I didn’t look at the output. I didn’t know what fields it contained. It just worked, mostly — except for the one recurring headache that every WordPress dev knows.
Building one by hand in Astro forced me to actually understand what a sitemap is supposed to contain, and why some of the fields matter more than I assumed.
Where I Started — The Official Package
Astro has an official integration for this: @astrojs/sitemap. Install it, add it to astro.config.mjs, done.
npm install @astrojs/sitemap
// astro.config.mjs
import sitemap from '@astrojs/sitemap'
export default defineConfig({
site: 'https://astro-content-lab.vercel.app',
integrations: [mdx(), sitemap()],
})
Build it, and Astro generates sitemap-index.xml plus sitemap-0.xml, listing every static route on the site. It worked immediately — every URL on the site showed up correctly.
But the output was thin:
<url><loc>https://astro-content-lab.vercel.app/blog/hello-world/</loc></url>
Just a URL. That is still a valid sitemap. Google does not require priority or changefreq, and it ignores both values. The field I actually cared about was lastmod, because this project already stores content dates in frontmatter.
Adding Fields — And Hitting the Real Limit
The package supports a config object for this:
sitemap({
serialize(item) {
return {
...item,
lastmod: new Date(),
}
},
})
This added a date, but every page received the build time. Rebuilding the site made old content look newly modified even when its visible content had not changed.
The package’s serialize() callback can customize entries, and the integration is the sensible default for many Astro sites. My problem was narrower: mapping each generated URL back to the correct Content Collection entry and its date was more awkward than generating a small sitemap directly from the collections.
Switching to a Custom Route
Astro lets you define API routes — files that return raw output instead of rendered pages. A file at src/pages/sitemap.xml.ts can generate XML directly, with full access to getCollection().
// src/pages/sitemap.xml.ts
import type { APIRoute } from 'astro'
import { getCollection } from 'astro:content'
import { SITE_URL } from '../config'
export const GET: APIRoute = async () => {
const entries = []
entries.push({
url: SITE_URL + '/',
lastmod: new Date().toISOString(),
})
const posts = await getCollection('posts', ({ data }) => data.status === 'published')
for (const post of posts) {
entries.push({
url: `${SITE_URL}/blog/${post.id}/`,
lastmod: post.data.publishedAt.toISOString(),
})
}
// ... same pattern for reviews, compares, guides
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${entries.map(e => ` <url>
<loc>${e.url}</loc>
<lastmod>${e.lastmod}</lastmod>
</url>`).join('\n')}
</urlset>`
return new Response(xml, {
headers: { 'Content-Type': 'application/xml' },
})
}
This pulls publishedAt straight from each collection entry’s frontmatter. The output now has real dates:
<url>
<loc>https://astro-content-lab.vercel.app/reviews/hostinger-review/</loc>
<lastmod>2026-06-17T00:00:00.000Z</lastmod>
</url>
I removed priority and changefreq entirely. Google ignores them, so they added code without helping discovery or ranking.
I uninstalled @astrojs/sitemap after this. Not because it is a bad package. It is the better low-maintenance choice when you only need route discovery. For this demo, a custom route was easier to explain and gave direct access to collection dates.
The /sitemap.xml vs /sitemap_index.xml Question
Rank Math, and most WordPress SEO plugins, default to sitemap_index.xml. That’s what I was used to seeing in robots.txt for years without thinking about it.
For this project, I went with plain /sitemap.xml instead.
robots.txt then just needs to point at it:
User-agent: *
Allow: /
Sitemap: https://astro-content-lab.vercel.app/sitemap.xml
What WordPress Made Easy, and What It Cost
Rank Math’s sitemap “just worked” in the sense that I never had to think about XML structure, priority values, or lastmod formatting. That’s real value — most site owners shouldn’t need to know any of this.
But “just working” had its own failure mode I dealt with more than once: the sitemap rendering a blank white page, or throwing a red error in the plugin dashboard, for no obvious reason. The fix was almost always going into Permalinks settings and clicking Save — re-triggering the rewrite rules — sometimes more than once, sometimes without explanation for why it broke in the first place. A few times, nothing worked except reinstalling the plugin.
I never understood why that fixed it. I just knew the ritual.
Building this by hand in Astro is the opposite trade. I had to learn what lastmod should represent and notice when my first implementation was producing meaningless output. That is more upfront effort than installing a plugin.
The result is a sitemap I fully understand and control. No mystery permalink ritual. No plugin reinstall as a debugging strategy. If something is wrong, the bug is probably in forty lines of TypeScript I wrote. Full control includes full ownership of my mistakes.
Tough, but worth it. That’s becoming a theme in this series — not because manual is inherently better, but because understanding what’s actually happening under the hood means fewer moments of “I have no idea why this broke or why clicking Save fixed it.”
If you are cloning this project and want the least maintenance, start with @astrojs/sitemap. Use the custom src/pages/sitemap.xml.ts pattern when you genuinely need collection-aware dates. Whichever route you choose, submit the sitemap in Search Console and keep lastmod honest.
Google’s sitemap guidance is the reference for the fields it uses. The Astro sitemap integration remains the default starting point.
Part 3 ends with a much less certain machine-readable file: llms.txt. I built it, but I do not treat it as an SEO requirement.