---
title: "Sitemap Chunking"
description: "Split large sitemap sources into multiple files for performance and search engine limits."
canonical_url: "https://nuxtseo.com/docs/sitemap/advanced/chunking-sources"
last_updated: "2026-09-02T15:03:21.575Z"
---

## Introduction

When dealing with large datasets, sitemap sources can be chunked into multiple files to:

- Stay within search engine limits (50MB file size, 50,000 URLs)
- Improve generation performance
- Better manage memory usage

## Simple Configuration

Enable chunking on any named sitemap with sources:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  sitemap: {
    sitemaps: {
      posts: {
        sources: ['/api/posts'],
        chunks: true, // Uses default size of 1000
      }
    }
  }
})
```

This generates:

```shell
/sitemap_index.xml    # Master index
/posts-0.xml          # First chunk (1-1000)
/posts-1.xml          # Second chunk (1001-2000)
...
```

## Controlling URL Order

By default, `sortEntries: true` sorts all resolved URLs by `loc` before splitting them into chunks. The order returned by your source is not preserved.

Set `sortEntries: false` when chunk boundaries should follow your source order:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  sitemap: {
    sortEntries: false,
    sitemaps: {
      posts: {
        sources: ['/api/posts'],
        chunks: true,
      }
    }
  }
})
```

Keep the source order deterministic. Inserting or removing an entry before an existing chunk boundary moves URLs between sitemap files, so update the [sitemap index `lastmod`](/docs/sitemap/guides/best-practices#set-accurate-sitemap-index-lastmod) for every file that changed.

## Chunk Size Options

Configure chunk sizes using different approaches:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  sitemap: {
    // Global default
    defaultSitemapsChunkSize: 5000,

    sitemaps: {
      // Using boolean (applies default)
      posts: {
        sources: ['/api/posts'],
        chunks: true,
      },

      // Using number as size
      products: {
        sources: ['/api/products'],
        chunks: 10000,
      },

      // Using explicit chunkSize (highest priority)
      articles: {
        sources: ['/api/articles'],
        chunks: true,
        chunkSize: 2000,
      }
    }
  }
})
```

## Skipping the index source fetch (`chunkCount`)

By default the sitemap index calls your source to count URLs, so it knows how many `<sitemap>` entries to emit. At very large scale this cold-start fetch is the bottleneck. If you already know the number of chunks, declare it upfront and the index will skip the fetch entirely:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  sitemap: {
    sitemaps: {
      posts: {
        sources: ['/api/posts'],
        chunks: 5000,
        chunkCount: 100, // 100 chunk entries, no source fetch in the index
      },
    },
  },
})
```

Per-chunk renders still fetch on demand and slice. If your data set grows past the declared count, tail entries are unreachable; if it shrinks, trailing chunks render empty. Update the value when your data set changes (or remove it to fall back to fetching).

## Practical Examples

### E-commerce Site

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  sitemap: {
    defaultSitemapsChunkSize: 10000,
    sitemaps: {
      products: {
        sources: ['/api/products/all'],
        chunks: 2000,
      },
      categories: {
        sources: ['/api/categories'],
        chunks: true, // Uses default 10k
      }
    }
  }
})
```

### Large Content Site

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  sitemap: {
    sitemaps: {
      'blog-posts': {
        sources: ['/api/blog/posts'],
        chunks: 5000,
      },
      'authors': {
        sources: ['/api/authors'],
        chunks: false, // Explicitly disable
      }
    }
  }
})
```

## Source Implementation

Chunks of the same sitemap share one resolved URL cache entry in production, so your source endpoint is called once per [cache window](/docs/sitemap/advanced/performance#cache-time), not once per chunk. Write the endpoint the same way you would without chunking.

Use `defineSitemapEventHandler()`{lang="ts"}. It types the return value as `SitemapUrlInput[]`{lang="ts"} and adds no caching of its own:

```ts [server/api/products/all.ts]
import { defineSitemapEventHandler } from '#imports'

export default defineSitemapEventHandler(async () => {
  const products = await db.products.findAll({
    select: ['id', 'slug', 'updatedAt']
  })

  return products.map(product => ({
    loc: `/products/${product.slug}`,
    lastmod: product.updatedAt
  }))
})
```

Switch to `defineCachedEventHandler()`{lang="ts"} when the query itself is expensive, or when several named sitemaps read this endpoint. Annotate the return value, because this handler has no sitemap-aware types:

```ts [server/api/products/all.ts]
import type { SitemapUrlInput } from '#sitemap/types'

export default defineCachedEventHandler(async (): Promise<SitemapUrlInput[]> => {
  const products = []
  const cursor = db.products.cursor({
    select: ['slug', 'updatedAt']
  })

  for await (const product of cursor) {
    products.push({
      loc: `/products/${product.slug}`,
      lastmod: product.updatedAt
    })
  }

  return products
}, {
  maxAge: 60 * 60, // 1 hour cache
  name: 'sitemap-products'
})
```

See [Which handler should I use?](/docs/sitemap/guides/dynamic-urls#which-handler-should-i-use) for the full rule.

## Debugging

Check chunk configuration and performance:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  sitemap: {
    debug: true,
    sitemaps: {
      products: {
        sources: ['/api/products'],
        chunks: 5000
      }
    }
  }
})
```

Visit `/__sitemap__/debug.json` to see chunk details and generation metrics.

## When built-in chunking isn't enough

Built-in chunking splits a sorted URL list by position. Delete one URL and every chunk after it shifts. The files change, but their `lastmod` dates have no way to say so.

If you need honest `lastmod` dates, chunk at the source instead. The same applies when your data outgrows a fixed set of sitemaps while the server runs. In both cases, register the chunks yourself with the [`sitemap:sitemaps-resolved`](/docs/sitemap/nitro-api/nitro-hooks) hook.

You decide the boundaries in your database. Each chunk gets a source endpoint that returns only its own URLs. Then you register one sitemap per chunk.

```ts [server/plugins/sitemap.ts]
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('sitemap:sitemaps-resolved', async ({ sitemaps }) => {
    // chunkCount comes from your database: ceil(totalGames / CHUNK_SIZE)
    for (let chunk = 0; chunk < chunkCount(); chunk++) {
      sitemaps[`games-${chunk}`] = {
        sitemapName: `games-${chunk}`,
        sources: [`/api/__sitemap__/games?chunk=${chunk}`],
      }
    }
  })
})
```

```ts [server/api/__sitemap__/games.ts]
export default defineSitemapEventHandler((event) => {
  const { chunk } = getQuery(event)
  // SELECT ... WHERE id >= chunk * CHUNK_SIZE AND id < (chunk + 1) * CHUNK_SIZE
  return gamesForChunk(Number(chunk)).map(game => ({
    loc: `/game/${game.id}/${game.slug}`,
    lastmod: game.updatedAt,
  }))
})
```

Each request fetches one chunk, so a chunk only pays for its own URLs. To set an honest `lastmod` per chunk in the index, pull it from your data with the [`sitemap:index-resolved`](/docs/sitemap/nitro-api/nitro-hooks) hook.

### Chunk by stable ranges

Pick boundaries that don't move. If chunk 2 holds games 10001 to 15000, it should always hold those games. Delete game 10002 and chunk 2 keeps its boundaries, so its `lastmod` only changes when its own content changes. Positional chunks shift on every delete, which makes their `lastmod` meaningless.

Boundaries don't have to be numeric. Whatever is stable in your data works: months of publication, category slugs, ID hashes.

### Growing and shrinking

The hook starts from a fresh copy of the sitemap config on every run. Don't try to be incremental. Register everything you want each time.

Growth needs no special handling: the next run registers the new chunk. Shrinking is the same in reverse. Stop registering a chunk and it leaves the index; its route returns a 404. `delete` is only needed for static sitemaps you declared in `nuxt.config` and no longer want.

### Caching

In development the hook runs on every request, so changes show up immediately.

In production with `cacheMaxAgeSeconds` set, the resolved sitemap list is cached for the same window as the sitemaps themselves. Register or remove freely. The change lands on the next refresh, on the same schedule your sitemap content already follows.

One timing detail after a removal: the sitemap list and the sitemap index refresh on their own schedules. For up to one cache window, the index may still list a sitemap whose route already returns a 404. Crawlers retry. If the window feels too long for your traffic, lower `cacheMaxAgeSeconds`.

For a complete working setup, see the [`dynamic-sitemaps` example](https://github.com/nuxt-modules/sitemap/tree/main/examples/dynamic-sitemaps).