Astro Sitemap Setup: hreflang, Exclusions, lastmod

Generate, validate, and submit sitemap.xml in Astro with @astrojs/sitemap — hreflang pairs, filter exclusions, lastmod, and what Google actually does with it (June 2026).

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 hreflang annotations.
  • You ship draft, preview, or auth-only pages that must stay out of search.
  • You want to give Google a real lastmod signal 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:

  • i18n takes defaultLocale (must be one of the locales keys) and locales (a map of the URL path segment to an hreflang language attribute, letters and hyphens only). The integration then groups same-content pages and adds xhtml:link alternates automatically.
  • filter(page) receives the full absolute URL and runs once per page. Return true to keep it, false to drop it.
  • serialize(item) is your hook to set changefreq, priority, or lastmod per URL; return undefined to 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

LimitValueNotes
URLs per sitemap file50,000Hard cap in the sitemaps.org protocol
File size (uncompressed)50 MBUnchanged since the 2016 bump from 10 MB; gzip helps transfer but the 50 MB ceiling is on the uncompressed file
Sitemaps per index50,000Theoretical 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 lastmod to the build date for every page on every build — Google treats a lastmod that 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/sitemap integration produces one; stock Astro ships nothing. Add it with npx 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.xml once 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 lastmod honest and let Google recrawl on its own; use IndexNow for Bing/Yandex if you want faster pickup.
  • What lastmod date format does Google accept?: A valid W3C Datetime / ISO 8601 value such as 2026-06-04 or 2026-06-04T12:00:00+00:00. It must reflect a real content change, or Google discounts it.
  • Should I set per-page priority and changefreq?: They are low-value hints Google largely ignores for ranking. lastmod is 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 filter and also add a noindex meta tag on the page itself. Belt and suspenders — the sitemap is a discovery hint, not an indexing rule.

Tags: #Indie dev #Astro #SEO #Technical SEO #Indexing