---
title: "How to Generate a Sitemap in Vue 3"
description: "Generate an XML sitemap for a Vue app with a Vite plugin, server-side route, or build script, then get it into Search Console."
canonical_url: "https://nuxtseo.com/learn-seo/vue/controlling-crawlers/sitemaps"
last_updated: "2026-07-16"
---

<key-takeaways>

- Sitemaps are limited to 50,000 URLs or 50MB; use a sitemap index for larger sites
- Only include indexable pages, skipping noindexed, redirected, or error pages
- Submit to Google Search Console and reference the sitemap in robots.txt

</key-takeaways>

The `sitemap.xml` file helps search engines discover your pages. Google considers sites "small" if they have [500 pages or fewer](https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview). You likely need a sitemap if you exceed this, have a new site with few backlinks, or update content frequently. Pair it with a solid [internal linking structure](/learn-seo/vue/routes-and-rendering/internal-linking) and a [pre-launch warmup](/learn-seo/pre-launch-warmup) strategy to speed up discovery.

## Static Sitemap

For small sites under 100 pages, create a static sitemap in your public directory:

```dir
public/
  sitemap.xml
```

Add your URLs with proper formatting:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://mysite.com/</loc>
    <lastmod>2026-01-03</lastmod>
  </url>
  <url>
    <loc>https://mysite.com/about</loc>
    <lastmod>2026-01-15</lastmod>
  </url>
</urlset>
```

Sitemaps are limited to [50,000 URLs or 50MB uncompressed](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap). Use UTF-8 encoding and absolute URLs only.

## Build-Time Generation with Vite

For most Vue projects, use [vite-plugin-sitemap](https://github.com/jbaubree/vite-plugin-sitemap) to auto-generate sitemaps during build:

```ts [vite.config.ts]
import Sitemap from 'vite-plugin-sitemap'

export default defineConfig({
  plugins: [
    Sitemap({
      hostname: 'https://mysite.com',
      dynamicRoutes: [
        '/blog/post-1',
        '/blog/post-2'
      ]
    })
  ]
})
```

After running `npm build`, this generates `sitemap.xml` and `robots.txt` in your dist folder.

If you're using Vue Router, generate routes from your router configuration instead of listing them by hand:

```ts [vite.config.ts]
import Sitemap from 'vite-plugin-sitemap'
import routes from './src/router/routes'

export default defineConfig({
  plugins: [
    Sitemap({
      hostname: 'https://mysite.com',
      dynamicRoutes: routes.map(route => route.path)
    })
  ]
})
```

Avoid hash-based routing (`/#/about`): search engines ignore everything after the `#`, so hash-routed pages collapse into a single URL and can't be listed individually in a sitemap. Use history mode instead.

## Server-Side Sitemap Generation

For sites with frequently changing content (e-commerce, blogs, news), generate sitemaps server-side:

<code-group>

```ts [Express]
import express from 'express'

const app = express()

app.get('/sitemap.xml', async (req, res) => {
  const pages = await fetchAllPages()

  const urls = pages.map(page => `
  <url>
    <loc>https://mysite.com${page.path}</loc>
    <lastmod>${page.updatedAt}</lastmod>
  </url>`).join('\n')

  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`

  res.type('application/xml').send(sitemap)
})
```

```ts [Vite]
// server.js for Vite SSR
import express from 'express'

const app = express()

app.use(async (req, res, next) => {
  if (req.path === '/sitemap.xml') {
    const pages = await fetchAllPages()

    const urls = pages.map(page => `
  <url>
    <loc>https://mysite.com${page.path}</loc>
    <lastmod>${page.updatedAt}</lastmod>
  </url>`).join('\n')

    const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`

    return res.type('application/xml').send(sitemap)
  }
  next()
})
```

```ts [H3]
import { defineEventHandler, setHeader } from 'h3'

export default defineEventHandler(async (event) => {
  if (event.path === '/sitemap.xml') {
    const pages = await fetchAllPages()

    const urls = pages.map(page => `
  <url>
    <loc>https://mysite.com${page.path}</loc>
    <lastmod>${page.updatedAt}</lastmod>
  </url>`).join('\n')

    const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`

    setHeader(event, 'Content-Type', 'application/xml')
    return sitemap
  }
})
```

</code-group>

This works with any [Node.js](https://nodejs.org) server or SSR framework. For SPAs without SSR, use build-time generation instead: you need every route listed before the build runs, which is what the [Vite](https://vite.dev) plugin's `dynamicRoutes` option is for.

## XML Sitemap Tags

A basic sitemap uses these elements:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://mysite.com/page</loc>
    <lastmod>2026-01-03</lastmod>
  </url>
</urlset>
```

<table>
<thead>
  <tr>
    <th>
      Tag
    </th>
    
    <th>
      Status
    </th>
    
    <th>
      Google Uses?
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code className="language-html shiki shiki-themes github-light github-light material-theme-palenight" language="html" style="">
        <span class="sx-uw">
          <
        </span>
        
        <span class="sFfpx">
          loc
        </span>
        
        <span class="sx-uw">
          >
        </span>
      </code>
    </td>
    
    <td>
      Required
    </td>
    
    <td>
      Yes, the page URL
    </td>
  </tr>
  
  <tr>
    <td>
      <code className="language-html shiki shiki-themes github-light github-light material-theme-palenight" language="html" style="">
        <span class="sx-uw">
          <
        </span>
        
        <span class="sFfpx">
          lastmod
        </span>
        
        <span class="sx-uw">
          >
        </span>
      </code>
    </td>
    
    <td>
      Recommended
    </td>
    
    <td>
      Yes, <a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap" rel="nofollow">
        if consistently accurate
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code className="language-html shiki shiki-themes github-light github-light material-theme-palenight" language="html" style="">
        <span class="sx-uw">
          <
        </span>
        
        <span class="sFfpx">
          changefreq
        </span>
        
        <span class="sx-uw">
          >
        </span>
      </code>
    </td>
    
    <td>
      Skip
    </td>
    
    <td>
      <a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap" rel="nofollow">
        No, Google ignores this
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code className="language-html shiki shiki-themes github-light github-light material-theme-palenight" language="html" style="">
        <span class="sx-uw">
          <
        </span>
        
        <span class="sFfpx">
          priority
        </span>
        
        <span class="sx-uw">
          >
        </span>
      </code>
    </td>
    
    <td>
      Skip
    </td>
    
    <td>
      <a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap" rel="nofollow">
        No, Google ignores this
      </a>
    </td>
  </tr>
</tbody>
</table>

Google only uses `<lastmod>` [if it's consistently and verifiably accurate](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap), meaning it can compare the date against the page's actual last modification. Update it when main content, structured data, or links change, not on every deploy.

Don't bother with `<changefreq>` or `<priority>`: they increase file size without adding value.

## Sitemap Index for Large Sites

If you exceed 50,000 URLs or 50MB, [split your sitemap into multiple files](https://developers.google.com/search/docs/crawling-indexing/sitemaps/large-sitemaps):

```xml [sitemap-index.xml]
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://mysite.com/sitemap-products.xml</loc>
    <lastmod>2026-01-03</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://mysite.com/sitemap-blog.xml</loc>
    <lastmod>2026-01-10</lastmod>
  </sitemap>
</sitemapindex>
```

Submit the index file to Google Search Console and it will crawl every sitemap listed inside it.

## Push-Based Discovery with IndexNow

Sitemaps are pull-based: crawlers check them on their own schedule. [IndexNow](https://www.indexnow.org/) is push-based. It's a protocol that notifies search engines the moment you create, update, or delete a page, instead of making them wait to rediscover it on the next crawl. Without it, [discovery can take days to weeks](https://www.indexnow.org/); Bing, Yandex, Seznam, Naver, and Yep all support it. Google does not.

Vue has no built-in module for this. Ping the endpoint yourself after a deploy:

```ts
await fetch('https://api.indexnow.org/indexnow', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    host: 'mysite.com',
    key: 'your-indexnow-key',
    keyLocation: 'https://mysite.com/your-indexnow-key.txt',
    urlList: ['https://mysite.com/new-page']
  })
})
```

## Specialized Sitemaps

### News Sitemap

News publishers should create [separate news sitemaps](https://developers.google.com/search/docs/crawling-indexing/sitemaps/news-sitemap) with articles from the last 2 days:

```xml [sitemap-news.xml]
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
  <url>
    <loc>https://mysite.com/article</loc>
    <news:news>
      <news:publication>
        <news:name>Site Name</news:name>
        <news:language>en</news:language>
      </news:publication>
      <news:publication_date>2026-01-29T12:00:00+00:00</news:publication_date>
      <news:title>Article Title</news:title>
    </news:news>
  </url>
</urlset>
```

Remove articles older than 2 days to keep the sitemap fresh; don't create new sitemaps daily, update the existing one.

### Image Sitemap

For galleries or image-heavy sites, add image metadata:

```xml [sitemap.xml]
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
  <url>
    <loc>https://mysite.com/page</loc>
    <image:image>
      <image:loc>https://mysite.com/image.jpg</image:loc>
      <image:title>Image Title</image:title>
    </image:image>
  </url>
</urlset>
```

[Image sitemaps help Google find images loaded via JavaScript](https://developers.google.com/search/docs/crawling-indexing/sitemaps/image-sitemaps) or not directly linked in HTML.

## Submit to Google Search Console

After generating your sitemap, [submit it to Google Search Console](https://support.google.com/webmasters/answer/7451001):

1. [Verify site ownership](/learn-seo/vue/launch-and-listen/search-console) (DNS, HTML file upload, or Google Analytics)
2. Navigate to **Sitemaps** under **Indexing**
3. Enter your sitemap URL: `https://mysite.com/sitemap.xml`
4. Click **Submit**

Google processes the sitemap and reports status:

- **Success**: sitemap accepted, pages discovered
- **Pending**: processing in progress
- **Couldn't fetch**: URL wrong or blocked by robots.txt

Check the Sitemaps report for coverage stats, indexing errors, and discovered URLs.

### Before Submitting

Validate your sitemap:

- XML syntax is valid (use an [online validator](https://www.xml-sitemaps.com/validate-xml-sitemap.html))
- All URLs return 200 status (not 404 or 500)
- URLs match [canonical URLs](/learn-seo/vue/controlling-crawlers/canonical-urls) exactly
- `<lastmod>` dates are accurate
- File uses UTF-8 encoding

### Alternative Submission

Reference your sitemap in `robots.txt`:

```txt [public/robots.txt]
User-agent: *
Allow: /

Sitemap: https://mysite.com/sitemap.xml
```

Google discovers this automatically when [crawling your robots.txt](/learn-seo/vue/controlling-crawlers/robots-txt).

### Troubleshooting "Couldn't Fetch"

Search Console sometimes shows **"Couldn't fetch"** or **"Sitemap could not be read"** even though the sitemap validates and loads fine in a browser. [Google lists several causes](https://support.google.com/webmasters/answer/7451001) for this status: the sitemap is blocked by `robots.txt`, the URL is wrong (a 404), the site has an unresolved manual action, or the fetch simply failed.

1. **Rule out the obvious first.** Confirm the sitemap URL loads directly, isn't blocked in `robots.txt`, and your site has no pending manual action.
2. **Use the URL Inspection tool** in Search Console. Paste your sitemap URL and click **Live test**. If Page fetch shows "Successful" and Crawl allowed shows "Yes", the sitemap itself is fine and the error is transient.
3. **Give it time.** Google says some fetch errors are transient and resolve on later crawl attempts, so wait and check back rather than resubmitting immediately.

If it's still failing after checking the above, remove and resubmit the sitemap in Search Console to force a re-fetch, or rename the sitemap path (for example `/sitemap.xml` to `/sitemap-index.xml`) so Google treats it as new.

Using Nuxt? [Nuxt SEO](/docs/nuxt-seo/getting-started/introduction) handles sitemap generation, indexing, and robots.txt automatically. [Learn more about sitemaps in Nuxt →](/learn-seo/nuxt/controlling-crawlers/sitemaps)

## Checklist

<checklist id="vue-sitemaps">

- Sitemap includes every indexable URL, none blocked by `robots.txt` or `noindex`
- URLs use history mode, no `#` hash routing
- `<lastmod>` dates are accurate, not blanket-set on every build
- Sitemap stays under 50,000 URLs and 50MB, split into an index if not
- Sitemap submitted in Google Search Console and referenced in `robots.txt`

</checklist>
