---
title: "Dynamic URL Endpoints"
description: "Use runtime API endpoints to generate dynamic URLs for your sitemap."
canonical_url: "https://nuxtseo.com/docs/sitemap/guides/dynamic-urls"
last_updated: "2026-07-30T18:14:10.549Z"
---

## Introduction

A dynamic sitemap resolves URLs at request time from a live data source, rather than baking them into a static file at build. Use it when your routes come from a CMS, database, or other source that changes between deploys, for example blog posts, product pages, or user-generated content that new pages need to be crawled and indexed for without a rebuild.

If your routes are prerendered by Nuxt instead (`nuxi generate` or route rules), the module can extract sitemap data straight from the rendered HTML without a runtime endpoint at all; see [Nuxt Prerendering](/docs/sitemap/guides/prerendering). Reach for this guide when the crawler can't discover your URLs from the build output alone.

The module supports two types of data sources:

- JSON responses from API endpoints
- XML sitemaps from external sources

## URL Structure Reference

All sitemap URLs follow this structure, whether from JSON endpoints or the `urls` config:

```ts
interface SitemapUrl {
  loc: string // Required: The URL path (e.g., '/blog/my-post')
  lastmod?: string | Date // Optional: Last modified date (ISO 8601 format or Date object)
  changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'
  priority?: 0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1 // Optional: 0.0 to 1.0
  images?: ImageEntry[] // Optional: Array of image objects
  videos?: VideoEntry[] // Optional: Array of video objects
  news?: GoogleNewsEntry // Optional: Google News entry
  _sitemap?: string // Optional: Specify which sitemap this URL belongs to (for multi-sitemap setups)
  _encoded?: boolean // Optional: Mark the URL as already encoded
  _i18nTransform?: boolean // Optional: Automatically transform the URL for all locales
  alternatives?: Array<{ // Optional: For i18n/alternate language URLs
    hreflang: string // Language code (e.g., 'en', 'fr', 'es')
    href: string // Full URL to alternative version
  }>
}
```

## Endpoint Response Shape

A dynamic endpoint should return a JSON array of sitemap URL objects:

```json [/api/__sitemap__/urls]
[
  {
    "loc": "/blog/hello-world",
    "lastmod": "2026-06-29",
    "changefreq": "weekly",
    "priority": 0.8
  },
  {
    "loc": "/products/sku-123",
    "_sitemap": "products"
  }
]
```

Use absolute URLs only when the URL belongs to another origin. For your own site, prefer paths like `/blog/hello-world`; the module resolves them against your configured `site.url`.

You can verify the endpoint before wiring it into the sitemap:

```bash
curl http://localhost:3000/api/__sitemap__/urls
```

Then check the generated sitemap:

```bash
curl http://localhost:3000/sitemap.xml
```

## Using External XML Sitemaps

If you have an existing XML sitemap, you can reference it directly in your configuration:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  sitemap: {
    sources: [
      'https://example.com/sitemap.xml',
    ]
  }
})
```

## Dynamic URLs from External APIs

When fetching dynamic URLs from external APIs, you have two main approaches:

1. **Direct source configuration** - Use when the API returns data in the correct format
2. **Custom API endpoint** - Use when you need to transform data or implement caching

### 1. Using Source Configuration

For APIs that require authentication or custom headers, provide sources as an array with fetch options:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  sitemap: {
    sources: [
      // Unauthenticated endpoint
      'https://api.example.com/pages/urls',
      // Authenticated endpoint
      [
        'https://authenticated-api.example.com/pages/urls',
        { headers: { Authorization: 'Bearer <token>' } }
      ]
    ]
  }
})
```

### 2. Creating Custom Endpoints

The resolved sitemap is already cached server-side for [`cacheMaxAgeSeconds`](/docs/sitemap/advanced/performance) (10 minutes by default), so your endpoint isn't hit on every crawler request. Only add your own caching in the endpoint itself if the upstream API is slow or rate-limited.

**Step 1: Create the API endpoint**

Use the [`defineSitemapEventHandler()`](/docs/sitemap/nitro-api/nitro-hooks) helper to create type-safe sitemap endpoints:

<code-group>

```ts [Simple]
import type { SitemapUrlInput } from '#sitemap/types'
// server/api/__sitemap__/urls.ts
import { defineSitemapEventHandler } from '#imports'

export default defineSitemapEventHandler(() => {
  return [
    {
      loc: '/about-us',
      // Specify which sitemap this URL belongs to
      _sitemap: 'pages',
    },
  ] satisfies SitemapUrlInput[]
})
```

```ts [Multiple Sitemaps]
import type { SitemapUrl } from '#sitemap/types'
// server/api/__sitemap__/urls.ts
import { defineSitemapEventHandler } from '#imports'

export default defineSitemapEventHandler(async () => {
  const [posts, pages] = await Promise.all([
    $fetch<{ path: string, slug: string }[]>('https://api.example.com/posts')
      .then(posts => posts.map(p => ({
        loc: `/blog/${p.slug}`, // Transform to your domain structure
        _sitemap: 'posts',
      } satisfies SitemapUrl))),
    $fetch<{ path: string }[]>('https://api.example.com/pages')
      .then(pages => pages.map(p => ({
        loc: p.path,
        _sitemap: 'pages',
      } satisfies SitemapUrl))),
  ])
  return [...posts, ...pages]
})
```

```ts [WordPress Example]
// server/api/__sitemap__/wordpress.ts
import { defineSitemapEventHandler } from '#imports'

export default defineSitemapEventHandler(async () => {
  const posts = await $fetch('https://api.externalwebsite.com/wp-json/wp/v2/posts')

  return posts.map(post => ({
    // Transform external URL to your domain
    loc: `/blog/${post.slug}`, // NOT post.link
    lastmod: post.modified,
    changefreq: 'weekly',
    priority: 0.7,
  }))
})
```

```ts [Dynamic i18n]
import type { SitemapUrl } from '#sitemap/types'
// server/api/__sitemap__/urls.ts
import { defineSitemapEventHandler } from '#imports'

export default defineSitemapEventHandler(async () => {
  const config = useRuntimeConfig()
  const baseUrl = config.public.siteUrl
  const locales = config.public.i18n.locales.map(locale => locale.code)
  const isoLocales = Object.fromEntries(
    config.public.i18n.locales.map(locale => ([locale.code, locale.iso]))
  )

  // Example: Fetch data for each locale
  const apiQueries = locales.map(locale =>
    $fetch(`${config.public.apiEndpoint}/sitemap/${locale}/products`)
  )

  const sitemaps = await Promise.all(apiQueries)

  return sitemaps.flat().map(entry => ({
    // explicit sitemap mapping
    _sitemap: isoLocales[entry.locale],
    loc: `${baseUrl}/${entry.locale}/product/${entry.url}`,
    alternatives: entry.alternates?.map(alt => ({
      hreflang: isoLocales[alt.locale],
      href: `${baseUrl}/${alt.locale}/product/${alt.url}`
    }))
  } satisfies SitemapUrl))
})
```

</code-group>

**Step 2: Configure the endpoint**

Add your custom endpoint to the sitemap configuration:

<code-group>

```ts [Single Sitemap]
export default defineNuxtConfig({
  sitemap: {
    sources: [
      '/api/__sitemap__/urls',
    ]
  }
})
```

```ts [Multiple Sitemaps]
export default defineNuxtConfig({
  sitemap: {
    sitemaps: {
      posts: {
        sources: [
          '/api/__sitemap__/urls/posts',
        ]
      },
      pages: {
        sources: [
          '/api/__sitemap__/urls/pages',
        ]
      }
    }
  }
})
```

</code-group>

## Handling Pre-Encoded URLs

By default, the module automatically encodes URL paths. This handles special characters like spaces and unicode (e.g., emojis, accented characters).

If your API or CMS returns URLs that are already encoded, mark them with `_encoded: true` to prevent double-encoding.

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

export default defineSitemapEventHandler(async () => {
  // URLs from your API are already encoded
  const urls = await $fetch<{ path: string }[]>('https://api.example.com/pages')
  // e.g. [{ path: '/products/%24pecial-offer' }, { path: '/blog/%F0%9F%98%85' }]

  return urls.map(url => ({
    loc: url.path,
    _encoded: true,
  }))
})
```

<note>

When `_encoded: true` is set, the module skips automatic encoding entirely. Make sure your URLs are properly encoded.

</note>
