You submit https://yourdomain.com/sitemap.xml to Google Search Console and it comes back Couldn't fetch or 404. This is the most common first-deploy bug on Astro sites (and on Next / Hugo for the same reasons). The single most likely cause: @astrojs/sitemap is installed but never registered in astro.config.mjs, so the build silently never writes a sitemap to dist/. The second: site is missing, so the integration skips generation. The third: you submitted the wrong filename — the integration outputs sitemap-index.xml, not sitemap.xml.
Fastest fix: open astro.config.mjs, confirm site: 'https://yourdomain.com' is set and sitemap() (with the parentheses) is inside the integrations array, run npm run build, then ls dist/sitemap*. If you see sitemap-index.xml, submit that URL — not /sitemap.xml.
This article lists five causes by hit rate, each with a check you can run directly against dist/ or against the live URL.
Which bucket are you in?
| Symptom | Most likely cause | Jump to |
|---|---|---|
dist/ has no sitemap* files at all | Integration not registered, or site missing | Cause 1, 2 |
dist/sitemap-index.xml exists locally but /sitemap.xml 404s in prod | Wrong filename submitted | Cause 4 |
Live URL returns 200 but content-type: text/html | Host rewrite / SPA fallback caught it | Cause 3 |
File is under dist/blog/ or another prefix | base path offset | Cause 5 |
File is valid (loads as XML in browser) but GSC still says Couldn't fetch | GSC fetch delay or stale cache | FAQ |
Common causes
Ordered by hit rate, highest first.
1. @astrojs/sitemap not registered in astro.config
npm install @astrojs/sitemap isn’t enough — you have to add it to the integrations array:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://yourdomain.com',
integrations: [sitemap()],
});
Without integrations: [sitemap()], dist/sitemap-index.xml is never generated and the build still exits 0 — no error to alert you.
How to spot it:
grep -n "sitemap" astro.config.mjs
If you see the import line but no sitemap() call inside integrations, it’s not registered.
2. site field is empty
@astrojs/sitemap uses site to build the absolute URL for every entry, so it refuses to run without it. Per the official docs, site is required and must begin with http:// or https:// (as of June 2026). With site missing, the integration logs a build-time warning that most people scroll past and skips generation entirely — nothing lands in dist/.
How to spot it: run npm run build and read the integration output. A missing site produces a warning along the lines of [@astrojs/sitemap] The Sitemap integration requires the site astro.config option. If you see that line, this is your cause.
3. Host rewrite catching /sitemap* as unmatched
Same trap as RSS — Vercel / Netlify SPA fallbacks like /* -> /index.html swallow the sitemap and return your app shell. Pure-static Astro deploys usually escape this, but if you’ve hand-edited vercel.json rewrites or netlify.toml redirects, double-check.
How to spot it:
curl -I "https://yourdomain.com/sitemap.xml"
curl -I "https://yourdomain.com/sitemap-index.xml"
A 200 with content-type: text/html (instead of application/xml) means the SPA fallback took over and served HTML.
4. Wrong URL — it’s sitemap-index.xml, not sitemap.xml
@astrojs/sitemap produces sitemap-index.xml plus one or more numbered files (sitemap-0.xml, sitemap-1.xml, …). A new file is split off every 45,000 URLs (the default entryLimit, as of June 2026). It does not produce a plain sitemap.xml. If you only submitted /sitemap.xml to Search Console, you’ll get a 404.
How to spot it:
ls dist/sitemap*
If you see sitemap-index.xml and sitemap-0.xml but no plain sitemap.xml, submit the index URL.
5. Base path offset
If your Astro config has base: '/blog', the sitemap emits to dist/blog/sitemap-index.xml, and hitting /sitemap.xml (without the base) 404s.
How to spot it:
grep -n "base:" astro.config.mjs
find dist -name "sitemap*"
If the actual location includes a base prefix, that’s the bug — submit the full path including the prefix (https://yourdomain.com/blog/sitemap-index.xml).
Shortest path to fix
Ordered by ROI. The first three usually solve 80% of cases.
Step 1: Register @astrojs/sitemap in the config
Minimum working config:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://yourdomain.com', // required, absolute http(s) URL
integrations: [
sitemap({
filter: (page) => !page.includes('/admin/'),
changefreq: 'weekly',
priority: 0.7,
}),
],
});
Two things to confirm:
siteis an absolutehttp://orhttps://URL, conventionally with no trailing slashsitemap()actually appears (with parentheses) insideintegrations
Step 2: Build locally and verify the artifact
rm -rf dist/
npm run build
ls dist/sitemap*
head -20 dist/sitemap-index.xml
Expected:
dist/sitemap-0.xml
dist/sitemap-index.xml
sitemap-index.xml should reference https://yourdomain.com/sitemap-0.xml. If site was wrong, you’ll see it here — e.g. references to http://localhost:4321/sitemap-0.xml, which Google can’t fetch in production. Fix site and rebuild before deploying.
Step 3: Curl-verify in production
curl -I "https://yourdomain.com/sitemap-index.xml"
curl -s "https://yourdomain.com/sitemap-index.xml" | head -10
Pass criteria:
- Status
200 content-type: application/xml- Body contains a
<sitemapindexelement
A 200 returning HTML means a rewrite is intercepting. Check vercel.json / netlify.toml for the SPA fallback and exclude /sitemap* from it.
Step 4: Submit the right URL to Search Console
In Search Console, open the property, then Indexing -> Sitemaps in the left nav. Under “Add a new sitemap”, enter the path only:
sitemap-index.xml
Note the -index (unless you’ve split your sitemap via the entryLimit or custom serialization options). Status should flip to Success within minutes to a few hours; a fresh property can take a day. If it sits on Couldn't fetch, see the FAQ — that is often a fetch delay, not a real failure.
Also reference it from robots.txt so other crawlers find it:
Sitemap: https://yourdomain.com/sitemap-index.xml
That helps Bing, Yandex, etc. auto-discover it. Make sure robots.txt does not Disallow the sitemap path — Google honors robots.txt when fetching sitemaps, and a blocking rule is a common cause of Couldn't fetch.
Step 5: CI smoke test after deploy
#!/usr/bin/env bash
set -e
URL="https://yourdomain.com/sitemap-index.xml"
status=$(curl -s -o /dev/null -w "%{http_code}" "$URL")
[[ "$status" == "200" ]] || { echo "Sitemap status: $status"; exit 1; }
curl -s "$URL" | grep -q "<sitemapindex" || { echo "Not a sitemap index"; exit 1; }
echo "Sitemap OK"
Wire it into your deploy-success hook. Any commit that accidentally removes the sitemap() call gets caught immediately, before Search Console ever sees the 404.
How to confirm it’s fixed
You’re done when all three of these are true:
curl -I https://yourdomain.com/sitemap-index.xmlreturns200withcontent-type: application/xml.- Opening the URL in a browser shows clean XML starting with
<?xmland a<sitemapindex>element (not your site’s HTML shell). - In Search Console, the submitted sitemap shows
Successwith a discovered URL count greater than zero.
If 1 and 2 pass but Search Console still shows Couldn't fetch, the file is fine — it’s a Google-side delay. See the FAQ.
Prevention
- Add a CI assertion that
dist/sitemap-index.xmlexists post-build, or run the Step 5 smoke test post-deploy - After Search Console submission, audit the status weekly to catch URL-count drops
- Reference the sitemap from
robots.txtso Bing / Yandex auto-discover it, and confirm robots.txt does not disallow the path - Don’t hardcode sitemap paths in templates — trust the integration’s default output
- Whenever you change
siteorbase, re-run the sitemap test as part of the same diff
FAQ
Search Console says Couldn't fetch but the URL loads fine in my browser — what now?
This is usually a fetch delay or a stale cache on Google’s side, not a real error. First confirm the file is reachable: curl -I should return 200 and application/xml. If it does, leave the submission in place — Google often clears Couldn't fetch to Success within a day or two on its own. If it’s stuck for several days, remove the sitemap entry, resubmit it, and use URL Inspection on the sitemap URL to force a fresh fetch. As a last resort for a genuinely stuck cache, deploy the sitemap under a slightly different path and submit that.
Why is there no sitemap.xml, only sitemap-index.xml?
By design. @astrojs/sitemap always emits an index file (sitemap-index.xml) that points to one or more numbered child files (sitemap-0.xml, and more once you pass 45,000 URLs). Submit and link the index URL; Google follows it to the children automatically.
Do I have to submit the sitemap to Search Console at all?
No — listing Sitemap: https://yourdomain.com/sitemap-index.xml in robots.txt lets crawlers discover it on their own. But submitting in Search Console gives you the discovered/indexed URL counts and surfaces parse errors, so it’s worth doing for any site you care about.
My build passes but dist/ has no sitemap. Why no error?
A missing site or a missing sitemap() registration is a non-fatal condition: the integration logs a warning and exits cleanly, so npm run build returns 0. Re-run the build and read the integration output for [@astrojs/sitemap] lines, or add the Step 5 smoke test so a missing artifact fails CI loudly.
I’m on SSR / output: 'server' — does the sitemap still get generated?
Yes, but only for routes known at build time (your prerendered/static pages). Fully dynamic SSR routes won’t appear automatically. Use the integration’s customPages option to inject dynamic URLs, or generate a separate sitemap for them.