---
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
::

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>`{lang="html"} 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>`{lang="html"} 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

| Feature                  | VitePress                                                                                     | Nuxt Content                                         |
| ------------------------ | --------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| **Weekly npm downloads** | ~[510K](https://www.npmjs.com/package/vitepress)                                              | ~[110K](https://www.npmjs.com/package/@nuxt/content) |
| **Build speed**          | Fast, Vite-native                                                                             | Slower on large sites                                |
| **Use case**             | Documentation, blogs, static content                                                          | Full web apps with markdown                          |
| **SSR**                  | Static-only                                                                                   | Full SSR + SSG                                       |
| **Search**               | Built-in local search                                                                         | Requires integration                                 |
| **Learning curve**       | Simple, minimal config                                                                        | More to learn, more it can do                        |
| **Page directory**       | Markdown only                                                                                 | Mixes markdown + Vue pages                           |
| **Authentication**       | [Doesn't fit the static-generation model](https://github.com/vuejs/vitepress/discussions/548) | Supported with SSR                                   |

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/).