A Nuxt sitemap is ready for production when it contains the intended public URLs, can refresh on the schedule your content needs, and serves the expected XML from the deployed site. A dynamic page working in the browser does not prove its URL reached the sitemap, and a runtime source does not guarantee an uncached response.
Start with the raw source data, follow it through Nuxt's sitemap configuration, and then inspect the public XML and destination pages. These seven checks help locate a missing or stale URL at the layer responsible instead of repeatedly changing unrelated SEO settings.
The examples follow the current @nuxtjs/sitemap documentation. Version 9.1.0 was current when checked on October 30, 2026; its package metadata declares support for Nuxt 4.6 or later within 4.x, or Nuxt 5. Check your installed versions before adapting code from older Nuxt tutorials. This is a documentation-based workflow, not a report of tests on a customer site, and a correct sitemap does not guarantee indexing.
1. Confirm sitemap ownership and the public origin
Check the project's installed packages before choosing configuration syntax:
npm ls nuxt @nuxtjs/sitemap @nuxtjs/seo
The current installation guide notes that @nuxtjs/seo already includes the sitemap module. Avoid registering it twice just because a standalone example does so. Also inspect whether public/sitemap.xml, a custom server/routes/sitemap.xml.* handler, or a build script already owns the same URL. Pick one intended generator and verify which response the deployment actually serves.
For a project managing the module directly, a minimal configuration could be:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/sitemap'],
site: {
url: 'https://www.example.com',
},
sitemap: {
sources: ['/api/__sitemap__/products'],
},
})
This assumes the source endpoint in check 2 and a runtime-capable deployment. Retain your existing umbrella-module setup if it already provides the sitemap.
Set the production site.url deliberately. For your own site's entries, prefer paths such as /products/blue-chair; the module resolves them against that public site URL. An API's hostname is not automatically the public hostname of the pages whose records it returns.
The Nuxt Site Config reference explains why request headers alone are a poor canonical identity, especially across previews, proxies, or multiple hostnames. Check environment overrides and your site's slash policy as well. Inspect actual <loc> values for an accidental localhost origin, preview domain, duplicated base path, or redirecting URL form.
Older examples using hostname, routes, cacheTime, or cacheTtl may describe a different module generation. An option name in a search result is not evidence that your installed version supports it.
2. Reconcile dynamic sources with published records
A route file such as pages/products/[slug].vue describes a pattern, not every product slug in your CMS. Nuxt's automatically discovered application sources and your supplied user sources are separate inputs. The data sources guide explains which routes can come from pages, prerendering, route rules, and supported content integrations.
First inspect the endpoint configured in sources. The dynamic URL contract expects a JSON array of sitemap entries, or a supported XML sitemap source. A typical CMS response wrapped in {items, nextCursor} needs an adapter; passing that wrapper through is not the same as returning all URLs.
Here is a complete illustrative adapter for a cursor-based API. It assumes raw, single-segment product slugs, boolean publication/indexability fields, and a real content-update timestamp. Replace the example API with your server-side content integration. Its response contract is:
{
"items": [
{
"slug": "blue-chair",
"published": true,
"indexable": true,
"updatedAt": "2026-10-29T10:00:00Z"
}
],
"nextCursor": null
}
The endpoint below follows every page, rejects repeated cursors and duplicate eligible paths, and fails rather than returning a successful empty sitemap when the source contract breaks:
// server/api/__sitemap__/products.ts
import {defineSitemapEventHandler} from '#sitemap/server'
import type {SitemapUrlInput} from '#sitemap/types'
type Row = {
slug: string
published: boolean
indexable: boolean
updatedAt: string
}
type Page = {items: unknown[], nextCursor: string | null}
export default defineSitemapEventHandler(async () => {
const urls: SitemapUrlInput[] = []
const cursors = new Set<string>()
const paths = new Set<string>()
let cursor: string | null = null
const unavailable = () => new Error('Sitemap temporarily unavailable')
for (let pageNumber = 0; pageNumber < 100; pageNumber++) {
const page: Page = await $fetch('https://cms.example.com/sitemap-records', {
query: cursor === null ? {} : {cursor},
})
if (!page || !Array.isArray(page.items) ||
!(page.nextCursor === null ||
(typeof page.nextCursor === 'string' && page.nextCursor.length > 0))) {
throw unavailable()
}
for (const item of page.items) {
const row = item as Row
if (!row || typeof row.slug !== 'string' ||
typeof row.published !== 'boolean' ||
typeof row.indexable !== 'boolean') throw unavailable()
if (!row.published || !row.indexable) continue
if (!row.slug || row.slug.trim() !== row.slug || /[\\/?#]/.test(row.slug) ||
row.slug === '.' || row.slug === '..' ||
typeof row.updatedAt !== 'string' ||
!Number.isFinite(Date.parse(row.updatedAt))) throw unavailable()
const loc = `/products/${row.slug}`
if (paths.has(loc)) throw unavailable()
paths.add(loc)
urls.push({loc, lastmod: new Date(row.updatedAt).toISOString()})
}
if (page.nextCursor === null) return urls
if (cursors.has(page.nextCursor)) throw unavailable()
cursors.add(page.nextCursor)
cursor = page.nextCursor
}
throw unavailable()
})
The module encodes these raw paths. If your real source already returns encoded paths, use its documented _encoded: true flag only for correctly encoded entries; encoding them again can change the URL.
The 100-page guard is an explicit bound for this example, not a module limit. Reaching it fails visibly rather than silently truncating the tail. Your real adapter must also handle the upstream API's authentication, snapshot consistency, rate limits, and validated response contract. Keep credentials on the server. A publicly reachable sitemap source should expose only information intended to be public.
The handler materializes its result in memory. For very large catalogs, design bounded source partitions rather than assuming later XML chunking makes this fetch cheap.
Reconcile the source with an independently approved content export. Check a newly published record, one near a pagination boundary, a removed record, and a deliberate exclusion. Compare actual URL sets, not only totals: one missing product and one unexpected draft can cancel each other out numerically. When comparing the final whole-site sitemap, include legitimate static application routes too.
3. Choose build-time or runtime generation
Choose the sitemap's update mechanism from the content's publication workflow, not from whether a page has a dynamic route parameter.
| Configuration or deployment | When sitemap data can change | What to verify |
|---|---|---|
A urls function in module configuration |
During build-time generation | A content update triggers the required rebuild. |
sources on a live Nitro deployment |
At runtime, subject to caching | The deployed server can reach the source and refresh its result. |
| A prerendered sitemap | When that file is regenerated and deployed | The released XML contains the latest approved inventory. |
zeroRuntime: true |
At build time | No workflow depends on runtime sitemap generation. |
| A fully static deployment | Through newly deployed files | A server endpoint present in source code is not assumed to exist on the host. |
The module's prerendering guide explains HTML extraction and explicit sitemap prerendering. Prerendered pages can contribute URLs, images, or modification metadata, but a crawler still needs a way to discover the intended pages. A route hidden from links is not automatically a complete CMS inventory.
Zero runtime mode automatically prerenders sitemap routes and removes runtime generation code. It suits sites whose URL inventory changes with deployments. It is a poor match when editors must publish new pages between deployments and expect the sitemap to reflect them without a rebuild.
Nuxt's rendering documentation distinguishes static output from server-backed rendering. Inspect the actual deployment artifact and hosting behavior. A source endpoint working during development does not establish that the static production host can execute it.
For a controlled publication, record which event refreshes the sitemap: a build, a source refresh, or a new static-file deployment. Keep that event separate from the page becoming publicly available.
4. Apply exclusions without confusing indexing controls
Make the sitemap's inclusion policy explicit. Exclude unpublished content, internal account flows, search-result variants, and URLs that deliberately canonicalize elsewhere when they are outside your chosen search inventory. A product being out of stock does not automatically mean its useful, publicly available page should disappear.
For example, these options exclude account routes and product slugs beginning with preview-:
// Relevant sitemap options in nuxt.config.ts
sitemap: {
exclude: ['/account/**', /^\/products\/preview-/],
},
Check the matching syntax carefully. The current filtering guide says string wildcards match whole path segments. /products/* matches one product segment; /products/preview-* is not a supported partial-segment wildcard. Use a regular expression for that intent rather than assuming a glob behaves like your shell.
For an individual page, the module also supports build-time page metadata:
<script setup lang="ts">
definePageMeta({sitemap: false})
</script>
Its page metadata documentation notes that these exclusions are extracted at build time. Runtime router changes do not automatically update that build-time information. Confirm both the route your application serves and the XML the module emits.
Keep three decisions separate:
- Sitemap exclusion controls whether the URL is listed in this discovery file.
- An indexing directive controls whether a supporting search engine should index an accessible page.
- Authentication and authorization protect private content.
Google's noindex guidance explicitly requires Googlebot to access the page to see its noindex directive. Blocking crawling in robots.txt does not by itself guarantee removal from search. Likewise, a sitemap filter does not make a public page private.
If the page is a duplicate, align its sitemap membership with the actual canonical policy. Verify the real page's head and response headers; do not infer every indexing decision from the source adapter's indexable field alone.
5. Verify locale and named-sitemap membership
A URL can exist in a source yet be absent from the child file you are checking. Inspect the sitemap index and the configured membership rules before deciding the record was lost.
For a site separating general pages from products, the relevant configuration might be:
sitemap: {
sitemaps: {
pages: {
includeAppSources: true,
exclude: ['/products/**'],
},
products: {
sources: ['/api/__sitemap__/products'],
chunks: 5000,
},
},
},
The named-sitemap includeAppSources default is false, according to the configuration reference. A dedicated product source does not automatically establish where your homepage and other application pages belong. Confirm their inclusion elsewhere instead of disabling application sources globally and forgetting them.
For localized sites, follow the module's actual i18n integration rules. They vary with the locale strategy, different-domain configuration, and custom page mappings. In particular:
| Dynamic entry choice | What to verify |
|---|---|
| No explicit i18n transformation | Dynamic URLs normally appear only in the default locale sitemap. |
_i18nTransform: true |
Generated locale paths correspond to pages that really exist, including translated path mappings. |
_sitemap: 'fr-FR' |
That exact configured sitemap name exists; it is not an arbitrary display label. |
Explicit alternatives |
Every alternate points to the intended available language version. |
A locale sitemap uses the locale's language value, or its code when no language is configured. Assigning _sitemap to a nonexistent name logs an error and omits that URL, rather than creating the missing sitemap for you.
Do not generate every language variant just because it is syntactically possible. A translated route must actually serve the appropriate content and agree with its canonical and language annotations. Inspect raw XML too: a stylesheet can hide hreflang details that remain present in the document.
Keep a small membership matrix with one example from each content type and locale. Record its source path, resolved public URL, expected child sitemap, and actual child sitemap. This separates source omissions from routing and membership mistakes.
6. Trace stale URLs through the cache layers
The current module's production cacheMaxAgeSeconds default is 600 seconds. Its dynamic-source documentation says this caches the resolved sitemap, including source fetching, normalization, and sorting. Development and prerendering do not use that module cache in the same way.
defineSitemapEventHandler adds sitemap-aware typing, not another cache. If you wrap an expensive source in defineCachedEventHandler, that endpoint has its own freshness policy. A CDN or hosting cache can then retain the delivered XML independently.
| Layer | Evidence to compare when XML is stale |
|---|---|
| Content store or upstream API | Publication state, intended path, content-update timestamp, pagination completion |
| Source endpoint | The actual returned public paths and dates, plus any endpoint cache |
| Sitemap module | Resolved XML after the configured production cache refresh |
| Hosting or CDN | Public response body, cache headers, final URL, and deployed revision |
Ten minutes is therefore not a guaranteed end-to-end update deadline. Separate caches, stale-response policies, source errors, and deployment timing can extend what a crawler receives. A fresh raw source alongside old XML points to a different layer than an already stale source response.
Use an approved content change, or a nonpublic test environment, to record a controlled sequence. Do not create disposable production pages solely to run this check without authorization.
| Change to observe | Expected source evidence | Expected sitemap evidence | Record actual time/result |
|---|---|---|---|
| Publish an eligible page | New path appears | Correct public URL appears in its child file | Pending observation |
| Change substantive content | Genuine modification date changes | Corresponding lastmod updates if supplied |
Pending observation |
| Withdraw or exclude a page | Eligibility/path changes as intended | Entry leaves the appropriate sitemap | Pending observation |
These rows are an observation worksheet, not measured results. Keep the source response and XML from the same observation period.
The chunking guide also notes that chunks of the same sitemap share a resolved-URL cache entry. Do not assume each child request creates a fresh upstream read. Conversely, separate named sitemaps can resolve their sources independently, so shared expensive sources may need their own deliberate caching strategy.
7. Validate deployed XML and its destinations
Fetch the actual public sitemap, saving both headers and body. Replace this example with your configured root sitemap or sitemap index URL. On Windows, use curl.exe explicitly if curl is a PowerShell alias:
curl --fail --location --dump-header sitemap-headers.txt --output sitemap.xml https://www.example.com/sitemap.xml
Check the final URL, status, content type, and XML root. An HTML fallback can return 200 at /sitemap.xml without providing a sitemap. A styled browser view can also hide details, so inspect the raw response.
If the root is a sitemap index, download its actual child files before comparing page URLs. Verify every referenced child is reachable, including the final chunk. The module's chunking options split a resolved source list; they do not fetch the missing second page of your CMS API. An explicit chunkCount that is too small can leave tail entries unreachable, while an oversized value can produce empty trailing chunks.
Compare the full approved inventory
Save the expected absolute URLs independently in expected-urls.txt, one per line. Include the static pages and locales covered by the files you are checking. Save this small Python 3 recipe as audit_urls.py, then pass local child urlset files as arguments:
from collections import Counter
from pathlib import Path
import sys
import xml.etree.ElementTree as ET
ns = '{http://www.sitemaps.org/schemas/sitemap/0.9}'
lines = Path('expected-urls.txt').read_text(encoding='utf-8-sig').splitlines()
expected = {line.strip() for line in lines if line.strip()}
if not expected or not sys.argv[1:]:
raise SystemExit('Provide an expected URL list and local urlset files')
actual = []
for filename in sys.argv[1:]:
root = ET.parse(filename).getroot()
if root.tag != ns + 'urlset':
raise SystemExit('Supply child urlset files, not an index or HTML')
entries = root.findall(ns + 'url')
if not entries:
raise SystemExit('Empty sitemap: ' + filename)
for entry in entries:
locs = entry.findall(ns + 'loc')
if len(locs) != 1 or not (locs[0].text or '').strip():
raise SystemExit('Missing or repeated loc in ' + filename)
actual.append(locs[0].text.strip())
counts = Counter(actual)
findings = {
'missing': sorted(expected - set(actual)),
'unexpected': sorted(set(actual) - expected),
'duplicates': sorted(url for url, count in counts.items() if count > 1),
}
for name, urls in findings.items():
print(name, len(urls), *urls, sep='\n')
raise SystemExit(1 if any(findings.values()) else 0)
python audit_urls.py products-0.xml products-1.xml pages.xml
Use your real downloaded filenames. This checks exact membership and duplicates; it does not normalize away hostname or slash differences, crawl pages, or validate every XML/schema requirement. A match also depends on the expected inventory being correct.
Google limits an individual sitemap to 50,000 URLs or 50MB uncompressed. Check file sizes as well as counts. Use lastmod only when it reflects a real significant change to that page; a new build timestamp applied to every unchanged entry is not useful evidence of freshness. Google ignores priority and changefreq values, as its sitemap documentation explains.
Finally, inspect representative listed destinations. Confirm that the intended page content is present, the URL and canonical agree, and the response does not unexpectedly redirect, return a soft error, require a login, or apply noindex. Check relevant crawling restrictions separately.
Verify the sitemap's discovery URL in robots.txt or Search Console, and retain the response snapshots with your release record. Our XML sitemap guide covers the broader submission and indexing workflow. A valid file, a successful submission, and a page being indexed are separate outcomes.