A Next.js sitemap can return valid XML while missing newly published pages, pointing to a preview domain, or leaving its child files undiscoverable. Start by identifying which code produces the public response, then compare that response with the URLs your website should expose.
Use the symptom to choose the first check:
| What you see | Start here |
|---|---|
/sitemap.xml returns 404 after splitting the sitemap |
Check the generated child paths and your index or discovery references |
| The file loads but recent articles are missing | Compare the complete CMS inventory with the delivered XML and its refresh trigger |
| A sitemap URL opens a login page or HTML | Inspect the response body and the proxy, authentication or hosting rule that handled it |
| XML lists pages that redirect or declare another canonical | Reconcile the URL builder and inclusion policy with the real page responses |
These 11 Next.js sitemap mistakes focus on App Router applications. The examples follow current Next.js documentation checked on October 25, 2026; the shard-ID example specifically uses the Next.js 16 API. Check your installed version, Cache Components setting and hosting model before adapting code. Older Pages Router and third-party generator setups need their own matching implementation.
This is a documentation-based troubleshooting guide, with illustrative code rather than results from a live Next.js experiment. A sitemap helps URL discovery; Google does not guarantee crawling or indexing every listed page.
1. Debugging the wrong sitemap generator
Before changing a filter or cache setting, establish which implementation owns the requested URL. A repository can contain an old XML file, a package configuration and an App Router metadata route at the same time. Editing one does not prove it produced the response you fetched.
| Implementation | What to trace |
|---|---|
| Native App Router sitemap | app/sitemap.ts, or src/app/sitemap.ts when the project uses src |
| Static XML | The source file, generated artifact and hosting path that serves it |
| Third-party generator | Its configuration, build command, output directory and deployed files |
| Custom Route Handler or Pages Router endpoint | Its URL mapping, data loader and response serialization |
The native Next.js sitemap file convention accepts an array of URL objects from a default export. A root app/sitemap.ts normally serves /sitemap.xml; the source filename is not the public request path.
Record the installed package version in your project:
node -p "require('next/package.json').version"
Search the repository for sitemap, inspect the actual build command, and identify one owner for each intended public sitemap endpoint. If two implementations target the same path, resolve that collision deliberately and rebuild. Do not assume that one file silently takes priority or delete an older file before confirming whether another deployment still depends on it.
Keep the requested path, owning source file and active deployment revision together in your notes. That makes the next change attributable to the response you are diagnosing.
2. Building URLs from a preview host or the wrong path
A sitemap generator needs the canonical public URL contract, not whichever hostname happened to run the build. Using a preview deployment address, a CMS API origin or localhost can produce syntactically valid XML containing the wrong destinations.
Google requires fully qualified sitemap URLs. Choose the intended scheme, hostname, base path and trailing-slash policy explicitly. Then use the same routing rules as the actual pages.
For a small illustrative site, the native pattern is:
// app/sitemap.ts
import type { MetadataRoute } from 'next'
const PUBLIC_ORIGIN = 'https://www.example.com'
export default function sitemap(): MetadataRoute.Sitemap {
const paths = ['/', '/guides/cache-basics']
return paths.map((path) => ({
url: new URL(path, PUBLIC_ORIGIN).href,
}))
}
Replace the example origin and paths with your real public routes. If the website lives beneath /docs, include that prefix in the route contract. With the URL constructor, a leading slash resolves from the origin root; it does not preserve a base directory you placed in the base URL.
For dynamic paths, reuse the application's route builder. A single-segment slug may need encodeURIComponent; a catch-all slug contains multiple segments and needs segment-aware handling. Encoding the entire guides/cache-basics path as one segment changes its meaning.
Compare a normal slug, a nested route and a non-ASCII slug against their actual canonical destinations. Correct the builder when those disagree; do not repair the XML afterward with a global string replacement.
3. Exporting only the first page of CMS records
A dynamic route such as app/blog/[slug]/page.tsx describes how to handle a slug. It does not supply the sitemap with every published slug in your CMS. Your generator still needs a complete eligible inventory.
Check collection pagination, query limits, locale selection and publication filters before blaming the framework. A fetch that returns the first 100 records can generate perfectly valid XML while omitting the rest of the collection.
Define the expected set independently of the current sitemap. Include public static routes and all intended published dynamic routes, then compare exact URLs. Equal totals alone are insufficient: one missing article and one unexpected draft can cancel each other out.
This small JavaScript fixture demonstrates the comparison:
const expected = new Set([
'https://www.example.com/',
'https://www.example.com/blog/first-guide',
'https://www.example.com/blog/new-guide',
])
const delivered = new Set([
'https://www.example.com/',
'https://www.example.com/blog/first-guide',
])
console.log({
missing: [...expected].filter((url) => !delivered.has(url)),
unexpected: [...delivered].filter((url) => !expected.has(url)),
})
For this fixture, new-guide is missing and the unexpected set is empty. In a real audit, populate expected from the complete approved content export and delivered from every sitemap child file. Keep the original list too, so converting to a Set does not hide duplicate entries.
Follow your CMS's documented pagination contract until all pages are read. Distinguish a confirmed empty collection from a failed or incomplete request. Catching an upstream error and returning [] turns an unavailable inventory into an apparently successful empty sitemap; treat that as a generation failure requiring investigation.
4. Listing URLs that disagree with search signals
Completeness does not mean exporting every record or every URL the application can serve. The sitemap should identify the canonical pages you intend to make discoverable in search.
Compare inclusion decisions with the deployed page:
| Page state | Sitemap decision |
|---|---|
| Published public page, intended canonical, usable content and successful response | Include its preferred URL |
| Draft, private preview or scheduled content not yet public | Exclude until the page is genuinely public and eligible |
| Old URL permanently redirected to a replacement | List the final replacement when eligible |
| Duplicate URL declaring another page as canonical | Prefer the intended canonical URL |
| Deliberately noindexed page | Exclude from the indexable sitemap set |
| Missing page or error response | Fix the page or remove the obsolete entry |
A 200 status is only one check. A successful response can still be a login screen, an error template or the wrong article. Inspect the content and indexing directives as well as the status.
Google treats sitemap inclusion as a canonicalization signal, not a command that overrides conflicting page signals. Our canonical-tag troubleshooting guide explains how to investigate a different declared or Google-selected canonical.
Keep the inclusion predicate close to the content contract: published status, publication time, public route and intended indexing policy. Compare language variants against real published translations; use the separate Next.js hreflang checks for cluster-level validation.
Screpy's Bulk HTTP Status Checker can help inspect a bounded batch of listed URLs. Follow its current input and coverage limits, and investigate incomplete results. It does not replace checking each page's canonical, content or Google indexing status.
5. Assuming an async sitemap is always fresh
Adding async to the sitemap function does not establish when its output changes. The Next.js sitemap reference describes this special Route Handler as cached by default unless it uses a request-time API or dynamic configuration.
Trace the complete publishing path:
CMS publication -> data retrieval -> sitemap generation -> delivered response
A change may be delayed at the source, in cached data, in the generated route response or at a hosting cache. Fetching the right record inside the function is not proof that a visitor received newly generated XML.
Choose a refresh mechanism supported by the installed version and rendering configuration. The current Route Segment Config documentation notes that dynamic, revalidate and fetchCache are removed when Cache Components is enabled. A snippet copied from an application without that feature is not a universal fix.
Use a controlled content change in your own environment to establish the actual behavior:
| Change | Expected sitemap result |
|---|---|
| Publish an eligible article | Its final canonical URL appears after the intended refresh |
| Rename its slug | The new URL replaces the old entry |
| Unpublish the article | Its URL leaves the eligible set |
| Save an unpublished draft | No new public sitemap entry appears |
Record the event time, content identifier, refresh event and first ordinary sitemap response showing the expected set. Check within the freshness window your publishing system promises. Do not claim success from a cache-busting request when the ordinary public URL still serves the old file.
6. Expecting runtime regeneration from a static export
A static export and a running Next.js server do not provide the same update mechanisms. If the application uses output: 'export', the deployed site consists of generated files. There is no Next.js process at that static host to regenerate a sitemap on demand.
Next.js documents static export support and limitations, including the lack of Incremental Static Regeneration and request-dependent Route Handlers.
| Delivery model | What must happen after relevant content changes |
|---|---|
| Build-generated sitemap on static hosting | Regenerate and publish the affected artifact through the build or export pipeline |
| Sitemap generated by a running Next.js application | Execute the supported refresh or regeneration path and verify delivered output |
| Separate externally generated sitemap file | Run its own generator and publish that file; updating the page application alone may not update it |
A CMS webhook that updates a page somewhere else does not automatically rewrite an exported XML artifact. Conversely, making the sitemap current does not create a missing HTML page in the export. Both need to represent the same publication state.
Inspect the completed export directory and the files actually uploaded by the deployment process. Verify the sitemap exists at the expected path and includes a newly published route whose page artifact also exists.
If generation needs information only available from an incoming request, reconsider that dependency for a static build. Obtain public content at generation time or choose a delivery architecture that can execute the required request-time logic. Renaming sitemap.ts to a custom Route Handler does not, by itself, add a server to static hosting.
7. Reading a sitemap shard ID with the wrong API shape
When generateSitemaps() creates multiple sitemap routes, the sitemap function needs to select the correct records for each ID. Next.js 16 passes that ID as a Promise<string>, according to the current API reference and version history.
Await it before validation or selection. This self-contained fixture shows two small shards; it is an API-shape example, not a recommendation to split a two-page website:
// app/products/sitemap.ts - Next.js 16
import type { MetadataRoute } from 'next'
const entriesById: Record<string, MetadataRoute.Sitemap> = {
'0': [{ url: 'https://www.example.com/products/blue-mug' }],
'1': [{ url: 'https://www.example.com/products/red-mug' }],
}
export async function generateSitemaps() {
return Object.keys(entriesById).map((id) => ({ id }))
}
export default async function sitemap(
props: { id: Promise<string> }
): Promise<MetadataRoute.Sitemap> {
const id = await props.id
if (!Object.hasOwn(entriesById, id)) {
throw new Error('Unknown sitemap shard')
}
return entriesById[id]
}
The corresponding documented paths are /products/sitemap/0.xml and /products/sitemap/1.xml. Check these actual endpoints rather than assuming the source file's location implies /products/sitemap.xml/0 or an extensionless path.
Older tutorials may show a numeric synchronous ID or development URLs from earlier releases. Match the installed version's contract; do not mix an old input type with current routing assumptions.
For a database-backed implementation, validate the resolved ID before using it to select a partition. Every emitted ID must map to an intentional dataset, and an unknown ID must not accidentally return the entire collection.
8. Splitting records into overlapping or oversized files
A sitemap must stay within both limits: 50,000 URLs and 50 MB uncompressed. Splitting by URL count alone can still produce an oversized file when entries contain additional data. These are Google's documented limits, not a recommended target size for every site.
The partitioning rule must also preserve membership. Use the same publication and indexing filters when counting records and generating each shard.
For a numeric-key partition, half-open boundaries avoid including the boundary record twice:
| Shard | Illustrative membership rule |
|---|---|
| 0 | id >= 0 AND id < 50000 |
| 1 | id >= 50000 AND id < 100000 |
These are key ranges, not a promise of 50,000 eligible records per shard. Deleted IDs, drafts and gaps change the actual count. Confirm that the chosen key domain and range plan cover every eligible record; do not calculate the number of these ranges from row count alone.
If using pagination instead, specify stable ordering. Offset pages read while content is inserted or removed can shift membership between requests. A generation snapshot or persisted manifest can keep a multi-file generation consistent; the right implementation depends on the datastore and publishing pipeline.
Preserve a manifest containing each shard ID, public URL, eligible-record count and generation revision. Verify:
- The union of all shard URL sets equals the approved inventory.
- No URL appears in multiple shards unintentionally.
- Every delivered file satisfies both the URL and uncompressed-byte limits.
- Added or removed shards are reflected in discovery references.
If the data changes during generation, recheck the completed set against the same snapshot or publication revision before publishing it.
9. Assuming generateSitemaps creates a root index
Generating child sitemaps and publishing a sitemap index are separate jobs. The current generateSitemaps documentation promises the child routes, such as /products/sitemap/0.xml. It does not promise an automatically generated root index.
This distinction is also reflected in the Next.js root-index issue: a maintainer identified the requested automatic index as an existing feature gap in August 2026. Do not submit /sitemap.xml merely because a tutorial says the framework creates it. Request it and inspect its contents first.
Choose a deliberate discovery method:
- Publish a sitemap index whose entries reference every current child sitemap.
- Or declare the individual sitemap URLs in
robots.txtand submit the appropriate files in Search Console.
For the two-child fixture above, the native robots convention can declare the same addresses:
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
},
sitemap: [
'https://www.example.com/products/sitemap/0.xml',
'https://www.example.com/products/sitemap/1.xml',
],
}
}
Next.js supports an array for the robots sitemap field. This minimal example is not a replacement for an existing site's deliberate crawl rules. Merge the sitemap declarations into your actual policy.
If using an index, its root must be sitemapindex; a page sitemap uses urlset. Fetch every referenced child and reconcile its membership. For nested sitemap locations, also check Google's location scope and Search Console submission requirements. Keep the manifest, index or robots declarations synchronized whenever the shard set changes.
10. Shipping malformed XML or misleading update dates
Inspect the raw document, even when the browser displays a readable table. A response containing HTML, invalid XML or the wrong root namespace is not repaired by a successful HTTP status.
Use UTF-8, the correct sitemap namespace and escaped XML values. The namespace remains http://www.sitemaps.org/schemas/sitemap/0.9; changing that identifier to HTTPS does not secure the website.
For example, an ampersand in a URL must be serialized as & inside XML. Only include the parameterized URL if it is actually the intended canonical page; correct escaping does not make a duplicate URL eligible.
A minimal document looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.example.com/guides/cache-basics</loc>
<lastmod>2026-10-20</lastmod>
</url>
</urlset>
That date is illustrative. Populate lastModified from a verified significant change to the public page, or omit the optional value when you cannot maintain it accurately. A CMS save time or build time is not necessarily a meaningful content revision. Google uses accurate lastmod values and ignores priority and changefreq; neither field fixes missing URLs.
For a downloaded page sitemap, this Python check parses XML and reports entry and duplicate counts:
from collections import Counter
from pathlib import Path
import xml.etree.ElementTree as ET
root = ET.fromstring(Path("sitemap.xml").read_bytes())
ns = "{http://www.sitemaps.org/schemas/sitemap/0.9}"
if root.tag != ns + "urlset":
raise ValueError("Expected a page sitemap, not an index or HTML")
urls = [(node.text or "").strip() for node in root.findall(ns + "url/" + ns + "loc")]
duplicates = [url for url, count in Counter(urls).items() if count > 1]
print({"entries": len(urls), "duplicates": duplicates})
Parsing is only a first check; this snippet does not validate every protocol rule or visit listed pages. Screpy's Sitemap Validator checks one document's supported structure and field values. Validate each relevant child separately and retain any partial-coverage warning.
11. Verifying localhost while production returns something else
A development response cannot establish what Google can fetch from the public hostname. Inspect the ordinary deployed URL, the full response body and every referenced child.
For example, download a page sitemap and its headers with:
curl.exe --silent --show-error --dump-header sitemap-headers.txt --output sitemap.xml https://www.example.com/products/sitemap/0.xml
Use curl instead of curl.exe where appropriate, and replace the fictional URL. This GET intentionally does not follow redirects automatically: inspect the status and any Location header before requesting the verified destination. Check that the downloaded file is the expected XML, not an HTML login page or hosting error.
If access differs from localhost, inspect authentication rules, locale redirects, rewrites and hosting protection. In current Next.js, Proxy can run on requests and return or redirect responses; older projects may use middleware.ts. An exclusion covering only /sitemap.xml may miss nested shard paths. Check every sitemap route against the actual matcher, while preserving protection for private application routes.
Keep an acceptance record for the release. This CSV is a reusable template, not observed test results:
requested_url,revision,checked_at_utc,status,root_type,url_count,missing_urls,unexpected_urls,result
https://www.example.com/products/sitemap/0.xml,REPLACE,REPLACE,REPLACE,REPLACE,REPLACE,REPLACE,REPLACE,pending
https://www.example.com/products/sitemap/1.xml,REPLACE,REPLACE,REPLACE,REPLACE,REPLACE,REPLACE,REPLACE,pending
Complete it only after fetching the real files, reconciling their combined URL set and checking representative listed pages for their expected content, canonical and indexing policy. Repeat after a publishing change to verify freshness as well as initial delivery.
Then inspect the Search Console Sitemaps report. A fetch or parsing error concerns the sitemap; successful processing still does not mean every listed URL is indexed. Investigate page-level outcomes separately instead of repeatedly changing valid XML.
The broader Screpy XML sitemap guide covers ongoing sitemap audits. For a Next.js release, the useful stopping point is concrete: the ordinary public files match the intended inventory, their discovery references work, and the publishing workflow keeps them current.