NNicheStackTutorials
← All tutorials
SEO & Web Vitals

Auto-Generate sitemap.xml for Every Static Framework (Hugo, Astro, VitePress, Eleventy)

Unified sitemap checklist: lastmod, priority, image entries, and split sitemaps for 500+ posts.

·12 min read·SEO & Web Vitals
Auto-Generate sitemap.xml for Every Static Framework (Hugo, Astro, VitePress, Eleventy) cover

Problem: Forgotten manual sitemap after migration. 40 new posts not indexed for 6 weeks. Fix: Auto-generate on every build. Zero maintenance.

Minimum viable sitemap fields

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/posts/core-web-vitals/</loc>
    <lastmod>2026-05-01</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>

lastmod must reflect real file change date — Google ignores fake daily lastmod.

Hugo

hugo.toml:

[sitemap]
  changefreq = 'monthly'
  priority = 0.5

[taxonomies]
  # disable tag pages in sitemap if thin content

Exclude low-value pages:

{{- /* layouts/_default/sitemap.xml */ -}}
{{- range .Data.Pages -}}
  {{- if and (not .Params.noindex) (ne .Kind "taxonomy") -}}
  <url>...</url>
  {{- end -}}
{{- end -}}

Set noindex: true on tag pages with < 3 posts.

Astro

npx astro add sitemap
// astro.config.mjs
import sitemap from '@astrojs/sitemap';
export default defineConfig({
  site: 'https://example.com',
  integrations: [sitemap({
    filter: (page) => !page.includes('/tags/'),
    serialize(item) {
      if (item.url.includes('/posts/')) item.priority = 0.8;
      return item;
    },
  })],
});

VitePress

No built-in sitemap. Post-build script:

// scripts/sitemap.mjs
import { SitemapStream, streamToPromise } from 'sitemap';
import { createWriteStream } from 'fs';
import { glob } from 'glob';

const links = (await glob('.vitepress/dist/**/*.html'))
  .filter(f => !f.includes('404'))
  .map(f => ({ url: f.replace('.vitepress/dist','').replace('/index.html','/'), changefreq: 'weekly' }));

const stream = new SitemapStream({ hostname: 'https://example.com' });
links.forEach(l => stream.write(l));
stream.end();
await streamToPromise(stream).then(data => createWriteStream('public/sitemap.xml').write(data));

robots.txt pointer

User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml

Put in static/robots.txt (Hugo) or public/robots.txt (VitePress/Astro).

Split sitemaps (500+ URLs)

Google limit: 50,000 URLs / 50MB per file.

<!-- sitemap_index.xml -->
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap><loc>https://example.com/sitemap-posts.xml</loc></sitemap>
  <sitemap><loc>https://example.com/sitemap-pages.xml</loc></sitemap>
</sitemapindex>

Submit once, verify forever

Search Console → Sitemaps → submit URL. CI check:

xmllint --noout public/sitemap.xml && echo OK
curl -s https://example.com/sitemap.xml | grep -c '<loc>'

Alert if URL count drops > 10% between deploys (broken generator).

Image sitemap (optional boost)

Add xmlns:image namespace for tutorial screenshots. See image SEO checklist post.

Results after fixing

Indexed pages: 312 → 398 (+27%)
Time to index new post: 14 days → 3 days
Crawl errors in GSC: 47 → 0

Sitemap is infrastructure. Automate it or forget it.

Tags: SitemapXMLSearch ConsoleHugoAstro