One sitemap file holds 50,000 URLs and 50 MB, uncompressed. A shop with 8,000 products, 400 categories, 60 pages and 2,000 blog posts is nowhere near that, and still benefits from splitting, because a file per type tells you at a glance which section stopped updating. This guide covers the limits, the index format, how to split (by type, by date, by language), what to leave out, and what WordPress core, Yoast and Rank Math generate on their own. The validator confirms the result in one run.
Quick answer
- Limits per file: 50,000 URLs and 50 MB uncompressed. An index file lists up to 50,000 child sitemaps and has the same 50 MB cap. Indexes cannot nest.
- Split by type (products, categories, posts, pages), by date for news and archives, or by language for multilingual sites. Aim for a few thousand URLs per child file.
- The index is a
<sitemapindex>with one<sitemap><loc>per child. Submit the index; Google reads the children. - Gzip is fine (
sitemap-1.xml.gz), and the 50 MB limit is measured before compression. - One
Sitemap:line in robots.txt pointing at the index, and the same URL submitted once in Search Console. - Run the sitemap validator on the site: it lists every sitemap file it finds with its type, URL count and problems.
Why splitting matters
The hard limit is the obvious reason: a single urlset with 50,001 URLs is outside the protocol, and the validator reports it as a problem. The 50 MB cap is easier to hit than it sounds once each <url> carries a lastmod, image entries and hreflang alternates; 20,000 product URLs with three image entries each can exceed it.
The practical reasons matter at any size (what a sitemap does for crawling). Google records when it last read each file; a per-type split means "posts read yesterday, products read 40 days ago" is visible in Search Console, and it points straight at the generator that broke. Regenerating a 5,000-URL child file when a product changes is cheaper than rebuilding one file with everything in it. Sections with different update rhythms can carry honest lastmod values: the news sitemap changes hourly, the pages sitemap once a quarter.
Splitting also keeps each file small enough to open in a browser or a text editor when something looks wrong, which a 40 MB file is not.
How getReport checks it
The report finds sitemaps the way a crawler does: the Sitemap: lines in robots.txt (the first three) and the default /sitemap.xml. Each file is fetched (up to 5 MB is read), gunzipped if it is compressed, and parsed with a strict XML parser. An index is recognised by its <sitemapindex> root, and its child <loc> entries are counted and status-checked with HEAD requests, so a child file that answers 404 or redirects shows up. The validator does not yet open each child and validate its contents; run it on a site whose robots.txt declares a child file directly, or check children one at a time, until the site crawl milestone adds that.

Columns, left to right: the file, how it was found (robots.txt or the default path), its status, its type (index or urlset), the number of URLs or child sitemaps, the lastmod range found in the file, and either "valid" or the list of problems. For an index, the sampled-URLs table under it shows the child files and the status each returned; the "this page is listed" finding is skipped for an index because the index itself lists files, not pages.
Step by step
1. Count what you have
Before splitting, know the number of indexable URLs per type. In a CMS that is a query per post type; in a static site, a count of the output pages. Include only URLs that should be in a sitemap (see step 5). If every type is under 50,000 and the total is under 50 MB, one file is legal; split anyway if any type updates on its own schedule.
2. Write the index
The index file lists child sitemaps, nothing else. No <url> entries, no page URLs, and no other index files:
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://example.com/sitemap-pages.xml</loc>
<lastmod>2026-09-12</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-posts.xml</loc>
<lastmod>2026-09-24</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-products-1.xml.gz</loc>
<lastmod>2026-09-25</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-products-2.xml.gz</loc>
<lastmod>2026-09-25</lastmod>
</sitemap>
</sitemapindex><lastmod> on a <sitemap> entry is the last time that child file changed, which is a cheap hint for Google to skip children that did not. Set it from the child's own newest lastmod, not from the time the index was generated. Each child is a normal urlset:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/products/leather-tote/</loc>
<lastmod>2026-09-25</lastmod>
</url>
</urlset>Absolute URLs in every <loc>, including the children's. The children must be on the same host as the index, or declared in that host's robots.txt; a sitemap at https://example.com/ cannot list URLs on https://shop.example.com/ otherwise.
3. Choose the split
By type is the default and works for most sites: sitemap-pages.xml, sitemap-posts.xml, sitemap-products.xml, sitemap-categories.xml. When a type exceeds a few thousand URLs, number the files (sitemap-products-1.xml, -2, …) and fill them in a stable order (by ID or by publish date) so a product stays in the same file between regenerations.
By date suits archives and news: sitemap-2026-09.xml, sitemap-2026-08.xml. Old months never change, so their files can be generated once and cached forever; only the current month is rebuilt.
By language suits multilingual sites: sitemap-en.xml, sitemap-de.xml, sitemap-hr.xml, each listing that language's URLs with the full hreflang set under each <url>. The hreflang entries multiply the file size (each URL carries every alternate), so per-language files reach the 50 MB cap before the 50,000-URL cap; see hreflang in sitemaps vs in HTML.
4. Compress if the files are large
A urlset compresses about ten to one. Serve sitemap-products-1.xml.gz as a gzipped file and list the .gz URL in the index. The 50 MB limit applies to the uncompressed content, so compression buys transfer time, not capacity. The validator decompresses .gz files and marks them in the files table; a .gz file that is not actually gzipped is reported as not well-formed.
Alternatively leave the files as .xml and let the server compress the response with Content-Encoding: gzip; that is what most CMSs do and it needs no change to the index.
5. Leave out what does not belong
A sitemap lists canonical, indexable, live URLs. Everything else weakens Google's trust in the file:
- Pages with
noindex, in a meta tag or anX-Robots-Tagheader. - URLs that redirect. List the destination instead.
- 404 and 410 pages. Remove them when they are deleted.
- Parameter URLs (
?sort=price,?sessionid=), paginated pages after the first, internal search results. - Non-canonical variants:
http://,wwwon a bare-domain site, trailing-slash duplicates. - Attachment and media pages in WordPress, tag archives, author archives, unless they are content you want ranked.
Images and videos do not need their own URLs in the sitemap: the image and video extensions add <image:image> and <video:video> children under the page's <url>, with the extra namespaces on the urlset. The validator tolerates those extensions and validates the surrounding sitemap; it does not check the image entries themselves yet.
6. Declare the index once
In robots.txt, one line for the index. Listing every child there as well is harmless but pointless:
# https://example.com/robots.txt
User-agent: *
Disallow: /wp-admin/
Sitemap: https://example.com/sitemap.xmlThe declared URL must be the index's real address, not one that redirects to it. Then Search Console → Sitemaps → add the same URL. Google shows the index with its children underneath, each with its own "last read" date and discovered-URL count. Details on the file itself are in robots.txt: test it before it hides your site.
Platform notes
WordPress
Core generates an index at /wp-sitemap.xml with children per type: wp-sitemap-posts-post-1.xml, wp-sitemap-posts-page-1.xml, wp-sitemap-taxonomies-category-1.xml, wp-sitemap-users-1.xml. Each child holds up to 2,000 URLs (the wp_sitemaps_max_urls filter changes it), and files are numbered when a type exceeds that. Post types and taxonomies are included when they are public; excluding one is a filter (wp_sitemaps_post_types), not a setting.
Yoast SEO replaces core's sitemap with /sitemap_index.xml and children named post-sitemap.xml, page-sitemap.xml, product-sitemap.xml, category-sitemap.xml. Exclusions are in the SEO settings per content type and taxonomy, and per post via "Allow search engines to show this page" (a noindex post is left out automatically). Rank Math does the same at /sitemap_index.xml with a "Links per sitemap" setting under Rank Math → Sitemap Settings. Either plugin turns core's sitemap off; if /wp-sitemap.xml still answers 200 next to /sitemap_index.xml, the site has two sitemaps with different contents. Keep one.
Attachment pages deserve their own mention: a child attachment-sitemap.xml full of media URLs is the usual reason a WordPress sitemap lists thousands of thin pages. See Attachment pages: stop indexing your image pages.
Shopify
/sitemap.xml is an index generated by Shopify with children per type (products, collections, pages, blogs) and per market. Hidden products and unpublished pages are left out; nothing else is configurable.
Static sites and custom
Generators that walk the page list at build time (Hugo, Astro, Next.js sitemap packages, sitemap on npm) split into an index automatically above a configurable size; set the size to a few thousand and name the output files by type. Cache the old date-based files if the build gets slow. For a custom generator, write the children first, then the index from the list of children with each child's newest lastmod.
Verify
- The files table shows the index as
indexwith the expected number of children, status 200, "valid"; the sampled table shows each child answering 200 and landing on itself. - Run the validator with one child declared directly in robots.txt, or check a child's URL as the page, to confirm the child parses as a valid
urlsetwith a plausible count. curl -s https://example.com/sitemap.xml | grep -c "<sitemap>"matches the number of children you expect.- Search Console → Sitemaps lists the index as "Success" and every child with a discovered-URL count that matches yours, after the next read.
Common mistakes
- An index that lists another index. Google reads one level. Symptom: Search Console shows the nested index as a child with zero URLs. Flatten it.
- Page URLs in the index file. A
<url>inside a<sitemapindex>is not part of the protocol; Google expects<sitemap>entries pointing at files and will try to fetch your pages as sitemaps. Page URLs go in children only. - Children on another host or with relative paths.
<loc>/sitemap-posts.xml</loc>is invalid;https://cdn.example.com/sitemap-posts.xmlis ignored unless declared in that host's robots.txt. Absolute URLs on the same host. - Two sitemap systems live at once. Core and a plugin, or a plugin and a hand-written file, each with different contents. Google may read both. Disable one.
- Splitting by count only.
sitemap-1.xmltositemap-9.xmlwith mixed types tells you nothing when one stops updating. Split by type first, then number within a type. - Redirecting
/sitemap.xmlto the index. It works, but every fetch pays for the hop, and some tools stop at the redirect. Declare and submit the final URL.