sitemap.xml Not Found (404) — Astro Integration / Site Fix

/sitemap.xml or /sitemap-index.xml returns 404, or Search Console says 'Couldn't fetch'. Five ranked causes with checks you run against dist/.

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?

SymptomMost likely causeJump to
dist/ has no sitemap* files at allIntegration not registered, or site missingCause 1, 2
dist/sitemap-index.xml exists locally but /sitemap.xml 404s in prodWrong filename submittedCause 4
Live URL returns 200 but content-type: text/htmlHost rewrite / SPA fallback caught itCause 3
File is under dist/blog/ or another prefixbase path offsetCause 5
File is valid (loads as XML in browser) but GSC still says Couldn't fetchGSC fetch delay or stale cacheFAQ

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:

  • site is an absolute http:// or https:// URL, conventionally with no trailing slash
  • sitemap() actually appears (with parentheses) inside integrations

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 <sitemapindex element

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:

  1. curl -I https://yourdomain.com/sitemap-index.xml returns 200 with content-type: application/xml.
  2. Opening the URL in a browser shows clean XML starting with <?xml and a <sitemapindex> element (not your site’s HTML shell).
  3. In Search Console, the submitted sitemap shows Success with 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.xml exists 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.txt so 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 site or base, 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.

Tags: #Hosting #Debug #Troubleshooting #Sitemap #SEO