Fix Your Sitemap for Astro
Astro uses the @astrojs/sitemap integration to emit sitemap-index.xml and sitemap-0.xml at build time. Misconfigured site URLs, missing getStaticPaths entries, and SSR routes commonly cause empty or incomplete sitemaps.
Astro sites break sitemap generation in a handful of predictable ways: hybrid rendering (some static, some SSR), dynamic routes with incomplete getStaticPaths, and Content Collections where drafts leak through. The integration is simple on the surface but has sharp edges once you mix rendering modes.
Debugged an Astro 4 site with Content Collections a few weeks back. 340 markdown posts, sitemap listing 47. The issue: their [...slug].astro page was reading from a collection filtered at render time, but getStaticPaths pulled from a separate (stale) export. After aligning the two, all 340 showed up.
Common Astro Sitemap Issues
- Missing
sitefield inastro.config.mjscausing@astrojs/sitemapto skip silently - Dynamic routes (
[slug].astro) producing empty sitemaps becausegetStaticPathsisn't exhaustive - SSR/server-rendered routes skipped entirely - the sitemap only knows about prerendered pages
- i18n routes
/enand/deemitted withouthreflangalternates - Draft posts from Content Collections leaking in when the schema doesn't include
draft - Preview and 404 pages included without an explicit filter
- Trailing-slash mismatch between
trailingSlash: 'never'in config and generated URLs - Hybrid sites where you set
output: 'server'globally instead ofoutput: 'hybrid', losing all static URLs
Working astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
trailingSlash: 'never',
integrations: [
sitemap({
filter: (page) =>
!page.includes('/admin') &&
!page.includes('/preview') &&
!page.includes('/draft'),
customPages: [
// SSR routes you want indexed
'https://example.com/dashboard-public',
],
i18n: {
defaultLocale: 'en',
locales: { en: 'en-US', de: 'de-DE', fr: 'fr-FR' },
},
serialize(item) {
if (item.url.includes('/blog/')) {
item.changefreq = 'weekly';
item.priority = 0.7;
}
return item;
},
}),
],
});Content Collections and drafts
Astro Content Collections don't have a native "draft" flag - you add it to your schema:
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
schema: z.object({
title: z.string(),
pubDate: z.date(),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
// In [...slug].astro
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map(p => ({ params: { slug: p.slug }, props: { post: p } }));
}The filter in getCollection drops drafts from prerendered pages, which means they never enter the sitemap in the first place. Much cleaner than filtering at the sitemap level.
i18n with hreflang
Pass the i18n option to the sitemap integration and it emits xhtml:link alternates automatically - as long as your routes actually exist for each locale. If /de/about doesn't exist but /en/about does, the sitemap still lists only /en/about. Translation gaps show up as missing alternates in GSC's International Targeting report.
Hybrid rendering gotchas
If you set output: 'server' globally, every route becomes SSR and your sitemap comes out empty. Use output: 'hybrid' and mark specific pages as export const prerender = true for anything you want indexed. Then add any remaining SSR URLs through customPages.
Step-by-Step Fix Guide
- Install with
npm install @astrojs/sitemapand add it to integrations - Set
site: 'https://yourdomain.com'inastro.config.mjs - Pass a filter callback excluding admin, preview, and draft routes
- For dynamic routes, make
getStaticPathsexhaustive. For SSR routes to index, usecustomPages - Add
i18nwithdefaultLocaleandlocalesto emit hreflang - Add
draftto your Content Collection schema and filter ingetCollection - Run
npm run build, inspectdist/sitemap-index.xml, verify URL count matches expectations - Verify with
curl https://yoursite.com/sitemap-index.xmland submit to Google Search Console