---
title: "VitePress SEO: Sitemaps, Meta Tags, and Search"
description: "Set up VitePress's built-in sitemap, per-page meta tags, and local search, then know when you actually need Nuxt Content instead."
canonical_url: "https://nuxtseo.com/learn-seo/vue/ssr-frameworks/vitepress"
last_updated: "2026-07-16"
---

<key-takeaways>

- [VitePress](https://vitepress.dev) pre-renders every page to static HTML at build time, so search engines get full content with no rendering delay
- Sitemap generation, per-page meta tags, and local search all ship built in, no extra packages needed for the basics
- It's static-only: no auth, no per-request data. Reach for Nuxt Content when you need SSR or dynamic pages

</key-takeaways>

VitePress is a static site generator built on [Vite](https://vite.dev) and Vue 3, designed for documentation sites and content-heavy blogs. Every page renders to HTML at build time, so crawlers see complete content on the first request. Navigation after that first load behaves like an SPA.

## When to Use VitePress

VitePress beats Nuxt Content when:

- You're building technical documentation with minimal setup
- Speed and simplicity matter more than flexibility
- Content is 100% static: no authentication, no per-request server logic
- You want built-in search and code highlighting without configuration

Choose Nuxt Content instead when you need SSR, routing that mixes markdown with non-markdown pages, or deeper integration with the Nuxt module ecosystem.

## SEO Features

### Built-in Sitemap Generation

VitePress includes [native sitemap support](https://vitepress.dev/guide/sitemap-generation), powered by the [`sitemap`](https://www.npmjs.com/package/sitemap) [npm](https://npmjs.com) package. Enable it in `.vitepress/config.ts`:

```ts [.vitepress/config.ts]
export default {
  sitemap: {
    hostname: 'https://mysite.com'
  }
}
```

This generates `sitemap.xml` in `.vitepress/dist` during build. For `<lastmod>` tags, enable the `lastUpdated` option:

```ts [.vitepress/config.ts]
export default {
  sitemap: {
    hostname: 'https://mysite.com'
  },
  lastUpdated: true
}
```

Any [`SitemapStream`](https://www.npmjs.com/package/sitemap) option can be passed straight into the `sitemap` config.

### Custom Sitemap Items

Use `transformItems` to modify sitemap entries before they're written:

```ts [.vitepress/config.ts]
export default {
  sitemap: {
    hostname: 'https://mysite.com',
    transformItems: (items) => {
      // Filter out draft pages
      return items.filter(item => !item.url.includes('/drafts/'))
    }
  }
}
```

### Build Hooks for Custom Sitemaps

For anything `transformItems` can't handle, use the [`buildEnd`](https://vitepress.dev/reference/site-config#buildend) hook, which runs after the SSG build finishes but before the CLI process exits:

```ts [.vitepress/config.ts]
import { writeFileSync } from 'node:fs'
import { SitemapStream, streamToPromise } from 'sitemap'

export default {
  async buildEnd(siteConfig) {
    const sitemap = new SitemapStream({ hostname: 'https://mysite.com' })

    const pages = await generatePageList(siteConfig)
    pages.forEach(page => sitemap.write(page))

    sitemap.end()
    const data = await streamToPromise(sitemap)
    writeFileSync('.vitepress/dist/sitemap.xml', data.toString())
  }
}
```

## Meta Tags Configuration

### Site-Level Meta Tags

Add meta tags globally using the [`head` config](https://vitepress.dev/reference/site-config):

```ts [.vitepress/config.ts]
export default {
  head: [
    ['meta', { name: 'description', content: 'My documentation site' }],
    ['meta', { property: 'og:type', content: 'website' }],
    ['meta', { property: 'og:image', content: 'https://mysite.com/og.png' }],
    ['link', { rel: 'icon', href: '/favicon.ico' }]
  ]
}
```

User-added tags render before the closing `</head>` tag, after VitePress's own built-in tags.

### Page-Level Meta Tags

Override meta tags per page using [frontmatter](https://vitepress.dev/reference/frontmatter-config):

```yaml
---
head:
  -
    - meta
    - name: description
      content: Custom description for this page
  -
    - meta
    - property: og:title
      content: Custom OG Title
  -
    - meta
    - name: keywords
      content: vitepress, seo, vue
---
```

Frontmatter tags append after site-level tags rather than replacing them. Set the same property at both levels and it renders twice in the HTML output, which [has caused duplicate OG tags](https://github.com/vuejs/vitepress/issues/975) for users who set `og:title` globally and again per page. Set dynamic properties like `og:title` only in frontmatter, or only globally, not both.

### Dynamic Meta Tags

For meta tags computed from page data, use [`transformPageData`](https://vitepress.dev/reference/site-config#transformpagedata):

```ts [.vitepress/config.ts]
export default {
  transformPageData(pageData) {
    pageData.frontmatter.head ??= []
    pageData.frontmatter.head.push([
      'meta',
      {
        property: 'og:title',
        content: pageData.frontmatter.layout === 'home'
          ? 'Homepage - My Site'
          : pageData.title
      }
    ])
  }
}
```

`transformPageData` runs during both dev and build. For a build-only equivalent, use [`transformHead`](https://vitepress.dev/reference/site-config#transformhead): it doesn't run during development, so it won't slow down the dev server.

## Built-in Search

VitePress supports [fuzzy full-text search](https://vitepress.dev/reference/default-theme-search) using an in-browser index, powered by [minisearch](https://github.com/lucaong/minisearch/). Enable it in `.vitepress/config.ts`:

```ts [.vitepress/config.ts]
export default {
  themeConfig: {
    search: {
      provider: 'local'
    }
  }
}
```

No external service required; search runs in the browser against an indexed bundle built at compile time. For larger sites, integrate [Algolia DocSearch](https://vitepress.dev/reference/default-theme-search) instead, which crawls your site and indexes it separately, worth it once local search starts feeling slow on a large docs set:

```ts [.vitepress/config.ts]
export default {
  themeConfig: {
    search: {
      provider: 'algolia',
      options: {
        appId: 'YOUR_APP_ID',
        apiKey: 'YOUR_API_KEY',
        indexName: 'YOUR_INDEX_NAME'
      }
    }
  }
}
```

## Clean URLs

VitePress can serve URLs without the `.html` extension:

```ts [.vitepress/config.ts]
export default {
  cleanUrls: true
}
```

Turn this on. Without it, `/page` and `/page.html` can both resolve, and if anything links to both forms, search engines can index them as separate, duplicate URLs. That matters for sitemaps and canonical tags: pick one form and make sure every internal link, sitemap entry, and canonical tag uses it.

## VitePress vs Nuxt Content

<table>
<thead>
  <tr>
    <th>
      Feature
    </th>
    
    <th>
      VitePress
    </th>
    
    <th>
      Nuxt Content
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Weekly npm downloads
      </strong>
    </td>
    
    <td>
      ~<a href="https://www.npmjs.com/package/vitepress" rel="nofollow">
        510K
      </a>
    </td>
    
    <td>
      ~<a href="https://www.npmjs.com/package/@nuxt/content" rel="nofollow">
        110K
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Build speed
      </strong>
    </td>
    
    <td>
      Fast, Vite-native
    </td>
    
    <td>
      Slower on large sites
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Use case
      </strong>
    </td>
    
    <td>
      Documentation, blogs, static content
    </td>
    
    <td>
      Full web apps with markdown
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        SSR
      </strong>
    </td>
    
    <td>
      Static-only
    </td>
    
    <td>
      Full SSR + SSG
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Search
      </strong>
    </td>
    
    <td>
      Built-in local search
    </td>
    
    <td>
      Requires integration
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Learning curve
      </strong>
    </td>
    
    <td>
      Simple, minimal config
    </td>
    
    <td>
      More to learn, more it can do
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Page directory
      </strong>
    </td>
    
    <td>
      Markdown only
    </td>
    
    <td>
      Mixes markdown + Vue pages
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Authentication
      </strong>
    </td>
    
    <td>
      <a href="https://github.com/vuejs/vitepress/discussions/548" rel="nofollow">
        Doesn't fit the static-generation model
      </a>
    </td>
    
    <td>
      Supported with SSR
    </td>
  </tr>
</tbody>
</table>

VitePress wins on performance-critical static sites where all content is known at build time. Nuxt Content is the better fit once you need complex routing, dynamic content, or SSR.

## Limitations

- **No SSR.** All content generates at build time; dynamic features like user authentication [don't fit the model](https://github.com/vuejs/vitepress/discussions/548)
- **Static content only.** Pages that need server-side data fetching need Nuxt or a custom Vite SSR setup
- **Smaller ecosystem.** Fewer plugins than Nuxt, though it's growing
- **Mixed routing is friction, not a feature.** Running VitePress inside an existing Nuxt project [tends to produce routing and layout conflicts](https://github.com/vuejs/vitepress/discussions/3785); running it as a separate site avoids the problem entirely

Using Nuxt? [@nuxtjs/seo](/learn-seo/nuxt) offers deeper SEO integration, handling sitemaps, robots.txt, structured data, and OG images with zero configuration. Learn more about [Nuxt Content for documentation](https://content.nuxt.com/).
