A correct sitemap is the cheapest SEO improvement you can ship. A wrong one quietly leaks draft pages into the index or feeds Google broken URLs and burns crawl budget. This guide gets the bilingual, exclusion-aware version right the first time.
TL;DR
Add @astrojs/sitemap (v3.7.x, as of June 2026), set site: in astro.config.mjs, and use the filter option to drop drafts and the i18n option to emit hreflang alternates. Submit sitemap-index.xml once in Google Search Console and let Google poll it. The old ping endpoint is dead (404 since late 2023), so freshness now comes from an honest lastmod, not pings.
When you need a custom sitemap config
The default integration covers most static blogs. You need the config below when any of these are true:
- Your site has more than ~30 URLs you want indexed.
- You have multiple languages or regional variants and want correct
hreflangannotations. - You ship draft, preview, or auth-only pages that must stay out of search.
- You want to give Google a real
lastmodsignal when content actually changes.
If you run a private internal tool or a single landing page, skip this — a sitemap adds noise without benefit there.
Step by step
1. Install the official integration
npx astro add sitemap
This adds @astrojs/sitemap to package.json and wires it into astro.config.mjs. The integration requires Astro 3 or later; on current Astro 5 it works unchanged.
2. Configure astro.config.mjs
site: is mandatory. If you forget it, the integration emits a file with no URLs and no error — a silent failure that wastes a deploy cycle. This is the shape that works for a bilingual content site:
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://yourdomain.com',
trailingSlash: 'always',
build: { format: 'directory' },
integrations: [
sitemap({
i18n: {
defaultLocale: 'en',
locales: { en: 'en', zh: 'zh-CN' },
},
filter: (page) =>
!page.includes('/drafts/')
&& !page.includes('/preview/')
&& !page.includes('/admin/')
&& !page.endsWith('/404/'),
serialize: (item) => {
// Tag homepage as daily, articles as weekly, everything else monthly
if (item.url.match(/\/articles\/[^/]+\/$/)) item.changefreq = 'weekly';
else if (item.url === 'https://yourdomain.com/') item.changefreq = 'daily';
else item.changefreq = 'monthly';
return item;
},
}),
],
});
A note on the options that matter:
i18ntakesdefaultLocale(must be one of thelocaleskeys) andlocales(a map of the URL path segment to anhreflanglanguage attribute, letters and hyphens only). The integration then groups same-content pages and addsxhtml:linkalternates automatically.filter(page)receives the full absolute URL and runs once per page. Returntrueto keep it,falseto drop it.serialize(item)is your hook to setchangefreq,priority, orlastmodper URL; returnundefinedto remove an entry entirely.
3. Build and confirm the files exist
npm run build
ls -la dist/sitemap*.xml
# dist/sitemap-index.xml
# dist/sitemap-0.xml
head -20 dist/sitemap-0.xml
The integration always writes a sitemap-index.xml plus one or more sitemap-N.xml files, even for a small site. The index is the one you submit.
4. Check the hreflang output
Each URL group should carry xhtml:link alternates so Google serves the right language per region:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://yourdomain.com/en/articles/astro-sitemap-setup/</loc>
<xhtml:link rel="alternate" hreflang="en"
href="https://yourdomain.com/en/articles/astro-sitemap-setup/" />
<xhtml:link rel="alternate" hreflang="zh-CN"
href="https://yourdomain.com/zh/articles/astro-sitemap-setup/" />
<changefreq>weekly</changefreq>
</url>
</urlset>
If the alternates are missing, the most common cause is an i18n.locales key that does not match the actual path segment (for example locales: { zh: 'zh-CN' } while your URLs use /zh-cn/).
5. Point robots.txt at the sitemap
Drop a public/robots.txt so crawlers discover it without a manual submit:
User-agent: *
Allow: /
Disallow: /drafts/
Disallow: /preview/
Disallow: /admin/
Sitemap: https://yourdomain.com/sitemap-index.xml
6. Sanity-check the URL count after deploy
curl -s https://yourdomain.com/sitemap-index.xml | grep -c '<loc>'
curl -s https://yourdomain.com/sitemap-0.xml | grep -c '<loc>'
# sitemap-0 count should roughly equal (en articles + zh articles + hub pages)
If the count is wildly off from your page count, your filter is too aggressive or site: was wrong.
7. Submit once in Search Console
Submit sitemap-index.xml in Google Search Console under Sitemaps. You never resubmit unless the URL itself changes — Google polls it on its own schedule. There is no longer a ping endpoint to call: Google’s https://www.google.com/ping?sitemap=... URL was deprecated in June 2023 and has returned 404 since late 2023, because the vast majority of unauthenticated submissions were spam.
If you want faster discovery on Bing and Yandex, use IndexNow instead of any ping URL — that is the supported modern path for nudging crawlers after a content change.
Sitemap limits
| Limit | Value | Notes |
|---|---|---|
| URLs per sitemap file | 50,000 | Hard cap in the sitemaps.org protocol |
| File size (uncompressed) | 50 MB | Unchanged since the 2016 bump from 10 MB; gzip helps transfer but the 50 MB ceiling is on the uncompressed file |
| Sitemaps per index | 50,000 | Theoretical ceiling of 2.5 billion URLs total |
@astrojs/sitemap splits automatically once you cross 45,000 entries, so you rarely touch these limits by hand.
Common pitfalls
- Forgetting
site:in config — Astro silently produces an empty, useless sitemap. - Listing pages that 404 or redirect — every wrong URL chips at your crawl budget.
- Including draft or test URLs and watching them get indexed before you notice.
- Setting
lastmodto the build date for every page on every build — Google treats alastmodthat never matches real changes as noise and eventually stops trusting it. - Submitting one sitemap per language separately when a single sitemap index with hreflang is cleaner and avoids conflicting signals.
FAQ
- Does Astro generate a sitemap automatically?: No. Only the
@astrojs/sitemapintegration produces one; stock Astro ships nothing. Add it withnpx astro add sitemap. - How big can a sitemap be?: 50,000 URLs or 50 MB uncompressed per file. The Astro integration splits into multiple files behind a
sitemap-index.xmlonce you exceed roughly 45,000 URLs. - Do I need to ping Google when the sitemap changes?: No. The ping endpoint was deprecated in June 2023 and now returns 404. Keep
lastmodhonest and let Google recrawl on its own; use IndexNow for Bing/Yandex if you want faster pickup. - What
lastmoddate format does Google accept?: A valid W3C Datetime / ISO 8601 value such as2026-06-04or2026-06-04T12:00:00+00:00. It must reflect a real content change, or Google discounts it. - Should I set per-page
priorityandchangefreq?: They are low-value hints Google largely ignores for ranking.lastmodis the one signal that still influences crawl scheduling, so spend your effort there. - What if some pages should not be in the sitemap?: Exclude them via
filterand also add anoindexmeta tag on the page itself. Belt and suspenders — the sitemap is a discovery hint, not an indexing rule.