Prerendering Vue SPAs for SEO · Nuxt SEO

[NuxtSEO](https://nuxtseo.com/ "Home")

- [Modules](https://nuxtseo.com/docs/nuxt-seo/getting-started/introduction)
- [Tools](https://nuxtseo.com/tools)
- [Pro](https://nuxtseo.com/pro)
- [Learn SEO](https://nuxtseo.com/learn-seo/nuxt) [Releases](https://nuxtseo.com/releases)

[1.4K](https://github.com/harlan-zw/nuxt-seo)

[Nuxt SEO on GitHub](https://github.com/harlan-zw/nuxt-seo)

Learn SEO

Master search optimization

Nuxt

 Vue

[SEO Checklist](https://nuxtseo.com/learn-seo/checklist) [Pre-Launch Warmup](https://nuxtseo.com/learn-seo/pre-launch-warmup) [Backlinks & Authority](https://nuxtseo.com/learn-seo/backlinks)

[Mastering Meta](https://nuxtseo.com/learn-seo/vue/mastering-meta)

- [Titles](https://nuxtseo.com/learn-seo/vue/mastering-meta/titles)
- [Meta Description](https://nuxtseo.com/learn-seo/vue/mastering-meta/descriptions)
- [Social Sharing](https://nuxtseo.com/learn-seo/vue/mastering-meta/social-sharing)
- [Schema.org](https://nuxtseo.com/learn-seo/vue/mastering-meta/schema-org)
- [Migrating vue-meta](https://nuxtseo.com/learn-seo/vue/mastering-meta/migrating-vue-meta)
- [Rich Results](https://nuxtseo.com/learn-seo/vue/mastering-meta/rich-results)
- [Image Alt Text](https://nuxtseo.com/learn-seo/vue/mastering-meta/alt-text)

[ Controlling Crawlers](https://nuxtseo.com/learn-seo/vue/controlling-crawlers)

- [Robots.txt](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/robots-txt)
- [Sitemaps](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/sitemaps)
- [Robot Meta Tag](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/meta-tags)
- [Canonical Link Tag](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/canonical-urls)
- [HTTP Redirects](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/redirects)
- [Duplicate Content](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/duplicate-content)
- [llms.txt](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/llms-txt)

[ SPA SEO](https://nuxtseo.com/learn-seo/vue/spa)

- [Prerendering](https://nuxtseo.com/learn-seo/vue/spa/prerendering)
- [Dynamic Rendering](https://nuxtseo.com/learn-seo/vue/spa/dynamic-rendering)
- [Hydration & SEO](https://nuxtseo.com/learn-seo/vue/spa/hydration)

[ Routes & Rendering](https://nuxtseo.com/learn-seo/vue/routes-and-rendering)

- [URL Structure](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/url-structure)
- [Pagination](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/pagination)
- [Trailing Slashes](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/trailing-slashes)
- [Query Parameters](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/query-parameters)
- [Hreflang & i18n](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/i18n)
- [404 Pages](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/404-pages)
- [Dynamic Routes](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/dynamic-routes)
- [Internal Linking](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/internal-linking)
- [Rendering Modes](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/rendering)
- [Programmatic SEO](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/programmatic-seo)
- [Security](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/security)

[ SSR Frameworks](https://nuxtseo.com/learn-seo/vue/ssr-frameworks)

- [Nuxt vs Quasar](https://nuxtseo.com/learn-seo/vue/ssr-frameworks/nuxt-vs-quasar)
- [Custom Vite SSR](https://nuxtseo.com/learn-seo/vue/ssr-frameworks/vite-ssr)
- [VitePress SEO](https://nuxtseo.com/learn-seo/vue/ssr-frameworks/vitepress)

[ Launch & Listen](https://nuxtseo.com/learn-seo/vue/launch-and-listen)

- [Getting Indexed](https://nuxtseo.com/learn-seo/vue/launch-and-listen/going-live)
- [Google Search Console](https://nuxtseo.com/learn-seo/vue/launch-and-listen/search-console)
- [Core Web Vitals](https://nuxtseo.com/learn-seo/vue/launch-and-listen/core-web-vitals)
- [Indexing Issues](https://nuxtseo.com/learn-seo/vue/launch-and-listen/indexing-issues)
- [SEO Monitoring](https://nuxtseo.com/learn-seo/vue/launch-and-listen/seo-monitoring)
- [Site Migration](https://nuxtseo.com/learn-seo/vue/launch-and-listen/site-migration)
- [IndexNow](https://nuxtseo.com/learn-seo/vue/launch-and-listen/indexnow)
- [Debugging](https://nuxtseo.com/learn-seo/vue/launch-and-listen/debugging)
- [AI Search Optimization](https://nuxtseo.com/learn-seo/vue/launch-and-listen/ai-optimized-content)

1. [Learn SEO for Vue](https://nuxtseo.com/learn-seo)
2.
3. [Spa](https://nuxtseo.com/learn-seo/vue/spa)
4.
5. [Prerendering](https://nuxtseo.com/learn-seo/vue/spa/prerendering)

# Prerendering Vue SPAs for SEO

Build-time and on-demand prerendering for client-side Vue apps. How to get SPA performance with SSR indexing.

[![Harlan Wilton](https://avatars.githubusercontent.com/u/5326365?v=4)Harlan Wilton](https://x.com/harlan-zw)12 mins read Updated Jan 29, 2026

What you'll learn

- Prerendering generates HTML at build time. deploy as static files to any CDN
- **Speculation Rules API** is the modern standard for speeding up SPA navigation
- Use `vite-ssg` for modern projects, `Prerender.io` for legacy SPAs with frequent updates

Prerendering gives you SPA performance with SSR indexing. You fire up a headless browser at build time, load your routes, dump the HTML. Google sees content immediately.

[Google can execute JavaScript](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics), but it's slower and less reliable than HTML. Prerendering removes the wait.

## [When to Prerender](#when-to-prerender)

Use prerendering when:

- You have a client-side SPA (no SSR framework)
- Routes are known at build time
- Content changes daily or less frequently
- You want fast deployment (static files, no server)

Don't prerender when:

- Content changes hourly (use SSR instead)
- Routes depend on user data (profiles, dashboards)
- You have 10,000+ dynamic pages (builds time out)

If your `/about` and `/pricing` pages never change, prerender them. If you're showing user-generated content or real-time data, you need [SSR](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/rendering).

## [Modern Prefetching: Speculation Rules API](#modern-prefetching-speculation-rules-api)

While prerendering handles the _initial load_, the **Speculation Rules API** (widely supported in 2026) speeds up _subsequent navigation_. It tells the browser to pre-render the next page in a hidden background tab, making the transition instant.

This is superior to the old `<link rel="prefetch">` because it can execute JavaScript in the background before the user clicks.

components/Speculation.vue

```
<script setup lang="ts">
import { onMounted } from 'vue'

// Prerender the /pricing page when this component mounts
onMounted(() => {
  const script = document.createElement('script')
  script.type = 'speculationrules'
  script.textContent = JSON.stringify({
    prerender: [
      {
        source: 'list',
        urls: ['/pricing', '/signup'],
        // 'moderate' triggers on hover/mousedown
        eagerness: 'moderate'
      }
    ]
  })
  document.head.appendChild(script)
})
</script>
```

Use this in your SPA to make navigation feel instant, which improves **Interaction to Next Paint (INP)** for page transitions.

## [Build-Time vs On-Demand Prerendering](#build-time-vs-on-demand-prerendering)

**Build-time prerendering** (vite-ssg):

- Generates HTML during `npm run build`
- Deploy as static files to CDN
- Fast serving, no runtime cost
- Can't handle frequently updated content

**On-demand prerendering** (Prerender.io, Rendertron):

- Service prerenders when crawler visits
- Detects bots by user agent
- Serves fresh content to crawlers
- Costs money, adds latency

Most Vue apps need build-time prerendering. On-demand is for SPAs with frequent content changes that can't use SSR.

## [Build-Time: vite-ssg (Recommended)](#build-time-vite-ssg-recommended)

[vite-ssg](https://github.com/antfu/vite-ssg) prerenders Vue apps using Vite's SSR capabilities. It's the modern replacement for `prerender-spa-plugin`.

Install:

```
npm install -D vite-ssg
```

Update your entry file:

src/main.ts

vite.config.ts

```
import { ViteSSG } from 'vite-ssg'
import App from './App.vue'
import routes from './routes'

export const createApp = ViteSSG(
  App,
  { routes },
  ({ app, router, initialState }) => {
    // Install plugins, setup app
  }
)
```

```
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [vue()],
  ssgOptions: {
    script: 'async',
    formatting: 'minify'
  }
})
```

Build generates HTML files matching your routes:

```
npm run build
# Creates dist/index.html, dist/about/index.html, etc.
```

Deploy `dist/` to [Netlify](https://netlify.com), [Vercel](https://vercel.com), Cloudflare Pages. anywhere that serves static files.

**Limitations:**

- Define routes at build time (no `/user/:id` unless you generate all IDs)
- Build time increases with route count
- Client-side navigation still works after hydration

## [Build-Time: prerender-spa-plugin (Legacy)](#build-time-prerender-spa-plugin-legacy)

[prerender-spa-plugin](https://github.com/chrisvfritz/prerender-spa-plugin) uses Puppeteer to render pages. Works with [webpack](https://webpack.js.org) and Vue CLI, but the project saw its last update in 2019. **Do not use for new projects.** Use `vite-ssg` instead.

If you're on Vue CLI:

```
npm install -D prerender-spa-plugin
```

vue.config.js

src/main.js

```
const path = require('node:path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')

module.exports = {
  configureWebpack: {
    plugins: [
      new PrerenderSPAPlugin({
        staticDir: path.join(__dirname, 'dist'),
        routes: ['/', '/about', '/pricing'],
        renderer: new PrerenderSPAPlugin.PuppeteerRenderer({
          renderAfterDocumentEvent: 'render-event'
        })
      })
    ]
  }
}
```

```
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.mount('#app')

// Signal prerender completion
document.dispatchEvent(new Event('render-event'))
```

**Why vite-ssg wins:**

- Maintained actively
- No Puppeteer dependency (faster builds)
- Built for [Vite](https://vite.dev) (modern tooling)
- Better DX

## [On-Demand: Prerender.io](#on-demand-prerenderio)

[Prerender.io](https://prerender.io/framework/vue-js/) renders pages when crawlers visit. It detects bots by user agent, serves cached HTML to them, serves your SPA to humans.

Setup with Express middleware:

Install

server.ts

```
npm install prerender-node
```

```
import express from 'express'
import prerender from 'prerender-node'

const app = express()

app.use(prerender.set('prerenderToken', 'YOUR_TOKEN'))

app.use(express.static('dist'))

app.get('*', (req, res) => {
  res.sendFile('dist/index.html')
})
```

Prerender.io caches rendered pages, serves them to crawlers. Your users still get fast client-side navigation.

**Costs:**

- Free tier: 250 cached pages
- Paid: Starts at $30/month for 10,000 pages

**When to use:**

- Content updates hourly but you can't use SSR
- Product catalogs with 1000+ pages
- You need SEO but have legacy SPA codebase

**Don't use if:**

- You can implement SSG (it's free)
- Content rarely changes (build-time wins)

## [On-Demand: Rendertron (Deprecated)](#on-demand-rendertron-deprecated)

[Rendertron](https://github.com/GoogleChrome/rendertron) is Google's open-source prerendering service. **It is no longer maintained.** Use Prerender.io or migrate to SSR.

## [Detecting Prerendered Environment](#detecting-prerendered-environment)

Your Vue app needs to know when it's being prerendered to avoid browser-only APIs:

vite-ssg

prerender-spa-plugin

```
import { onMounted } from 'vue'

export default {
  setup() {
    onMounted(() => {
      // Only runs in browser, not during prerender
      if (typeof window !== 'undefined') {
        initializeAnalytics()
      }
    })
  }
}
```

```
if (window.__PRERENDER_INJECTED) {
  // Code that should only run during prerender
}
else {
  // Normal browser code
}
```

Common mistakes:

- Calling `localStorage` during prerender (crashes build)
- Fetching from `http://localhost` (fails in CI)
- Forgetting `data-server-rendered="true"` on root element

Add to your root element so Vue hydrates instead of re-rendering:

```
<div id="app" data-server-rendered="true"></div>
```

## [Dynamic Routes](#dynamic-routes)

Prerendering dynamic routes requires generating all possible paths:

```
// vite.config.ts
export default {
  ssgOptions: {
    async includedRoutes(paths) {
      // Fetch all blog post slugs
      const posts = await fetch('https://api.example.com/posts')
        .then(r => r.json())

      return posts.map(p => \`/blog/${p.slug}\`)
    }
  }
}
```

For 1000+ dynamic routes, consider:

- SSR instead (no build-time cost)
- Incremental builds (prerender popular pages only)
- On-demand services (Prerender.io)

## [Testing Prerendered Output](#testing-prerendered-output)

After building, verify Google sees content:

**1. Check static HTML**

```
npm run build
cat dist/about/index.html
```

Should contain your actual content, not just `<div id="app"></div>`.

**2. [Google Search Console URL Inspection](https://search.google.com/search-console/)**

- Test live URL
- View rendered HTML
- Check screenshot

**3. Fetch as Googlebot**

```
curl -A "Mozilla/5.0 (compatible; Googlebot/2.1)" https://yoursite.com/about
```

If you see empty content, prerendering failed.

## [Common Mistakes](#common-mistakes)

**Mistake 1: Blocking JavaScript in robots.txt**

```
# ❌ Breaks prerendering detection
User-agent: *
Disallow: /*.js$
```

[Google needs JavaScript to hydrate your app](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics). Never block `.js` or `.css` from Googlebot. See our [robots.txt guide](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/robots-txt).

**Mistake 2: Prerendering user-specific content**

Prerendering generates static HTML. If your route shows different content per user, you need SSR.

**Mistake 3: Not handling async data**

Prerender runs before async requests complete:

```
// ❌ Data won't be in prerendered HTML
export default {
  async setup() {
    const data = await fetch('/api/data')
    return { data }
  }
}
```

Use `renderAfterDocumentEvent` to wait for data:

```
// Emit when data loads
fetch('/api/data').then((data) => {
  document.dispatchEvent(new Event('render-event'))
})
```

**Mistake 4: Large route counts**

Prerendering 10,000 routes takes hours and often fails in CI. Solutions:

- Prerender popular pages only (use analytics data)
- Switch to SSR
- Use on-demand prerendering

## [Comparison Table](#comparison-table)

| Approach | Cost | Setup | Best For |
| --- | --- | --- | --- |
| **vite-ssg** | Free | Easy | Modern Vue apps, <1000 routes |
| **prerender-spa-plugin** | Free | Medium | Legacy Vue CLI projects (Legacy) |
| **Prerender.io** | $30+/mo | Easy | Frequent updates, can't use SSR |
| **Rendertron** | Hosting | Hard | **Deprecated** |

## [Using Nuxt?](#using-nuxt)

Nuxt handles prerendering automatically with `nuxi generate`. It supports hybrid rendering (SSR + SSG mixed), ISR (Incremental Static Regeneration), and advanced route rules.

[Learn more about Nuxt prerendering →](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/rendering)

[The 2026 SEO Checklist for Nuxt & Vue Pre-launch setup, post-launch verification, and ongoing monitoring. Interactive checklist with links to every guide.](https://nuxtseo.com/learn-seo/checklist) [Haven't launched yet? Start with the Pre-Launch Warmup](https://nuxtseo.com/learn-seo/pre-launch-warmup)

---

[SPA SEO SEO for SPAs explained: why single page applications struggle with search engines and how to fix it. Learn when you need SSR, when prerendering works, and when client-side rendering is fine.](https://nuxtseo.com/learn-seo/vue/spa) [Dynamic Rendering Dynamic rendering serves pre-rendered HTML to crawlers while users see JavaScript. Google no longer recommends this. use SSR or SSG instead.](https://nuxtseo.com/learn-seo/vue/spa/dynamic-rendering)

On this page

- [When to Prerender](#when-to-prerender)
- [Modern Prefetching: Speculation Rules API](#modern-prefetching-speculation-rules-api)
- [Build-Time vs On-Demand Prerendering](#build-time-vs-on-demand-prerendering)
- [Build-Time: vite-ssg (Recommended)](#build-time-vite-ssg-recommended)
- [Build-Time: prerender-spa-plugin (Legacy)](#build-time-prerender-spa-plugin-legacy)
- [On-Demand: Prerender.io](#on-demand-prerenderio)
- [On-Demand: Rendertron (Deprecated)](#on-demand-rendertron-deprecated)
- [Detecting Prerendered Environment](#detecting-prerendered-environment)
- [Dynamic Routes](#dynamic-routes)
- [Testing Prerendered Output](#testing-prerendered-output)
- [Common Mistakes](#common-mistakes)
- [Comparison Table](#comparison-table)
- [Using Nuxt?](#using-nuxt)

[GitHub](https://github.com/harlan-zw/nuxt-seo) [ Discord](https://discord.com/invite/275MBUBvgP)

### [NuxtSEO](https://nuxtseo.com/ "Home")

- [Getting Started](https://nuxtseo.com/docs/nuxt-seo/getting-started/introduction)
- [MCP](https://nuxtseo.com/docs/nuxt-seo/guides/mcp)

Modules

- [Robots](https://nuxtseo.com/docs/robots/getting-started/introduction)
- [Sitemap](https://nuxtseo.com/docs/sitemap/getting-started/introduction)
- [OG Image](https://nuxtseo.com/docs/og-image/getting-started/introduction)
- [Schema.org](https://nuxtseo.com/docs/schema-org/getting-started/introduction)
- [Link Checker](https://nuxtseo.com/docs/link-checker/getting-started/introduction)
- [SEO Utils](https://nuxtseo.com/docs/seo-utils/getting-started/introduction)
- [Site Config](https://nuxtseo.com/docs/site-config/getting-started/introduction)
- [Skew Protection](https://nuxtseo.com/docs/skew-protection/getting-started/introduction)
- [AI Ready](https://nuxtseo.com/docs/ai-ready/getting-started/introduction)

### [NuxtSEO Pro](https://nuxtseo.com/pro "Home")

- [Getting Started](https://nuxtseo.com/pro)
- [Dashboard](https://nuxtseo.com/pro/dashboard)
- [Pro MCP](https://nuxtseo.com/docs/nuxt-seo-pro/mcp/installation)

### [Learn SEO](https://nuxtseo.com/learn-seo "Learn SEO")

Nuxt

- [Mastering Meta](https://nuxtseo.com/learn-seo/nuxt/mastering-meta)
- [Controlling Crawlers](https://nuxtseo.com/learn-seo/nuxt/controlling-crawlers)
- [Launch & Listen](https://nuxtseo.com/learn-seo/nuxt/launch-and-listen)
- [Routes & Rendering](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering)
- [Staying Secure](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/security)

Vue

- [Vue SEO Guide](https://nuxtseo.com/learn-seo/vue)
- [Mastering Meta](https://nuxtseo.com/learn-seo/vue/mastering-meta)
- [Controlling Crawlers](https://nuxtseo.com/learn-seo/vue/controlling-crawlers)
- [SPA SEO](https://nuxtseo.com/learn-seo/vue/spa)
- [SSR Frameworks](https://nuxtseo.com/learn-seo/vue/ssr-frameworks)
- [SEO Checklist](https://nuxtseo.com/learn-seo/checklist)
- [Pre-Launch Warmup](https://nuxtseo.com/learn-seo/pre-launch-warmup)
- [Backlinks & Authority](https://nuxtseo.com/learn-seo/backlinks)

### [Tools](https://nuxtseo.com/tools "SEO Tools")

- [Social Share Debugger](https://nuxtseo.com/tools/social-share-debugger)
- [Robots.txt Generator](https://nuxtseo.com/tools/robots-txt-generator)
- [Meta Tag Checker](https://nuxtseo.com/tools/meta-tag-checker)
- [HTML to Markdown](https://nuxtseo.com/tools/html-to-markdown)
- [XML Sitemap Validator](https://nuxtseo.com/tools/xml-sitemap-validator)
- [Schema.org Validator](https://nuxtseo.com/tools/schema-validator)
- [Keyword Research Pro](https://nuxtseo.com/tools/keyword-research)
- [SERP Analyzer Pro](https://nuxtseo.com/tools/serp-analyzer)
- [Domain Rankings Pro](https://nuxtseo.com/tools/domain-rankings)

Copyright © 2023-2026 Harlan Wilton - [MIT License](https://github.com/harlan-zw/nuxt-seo/blob/main/license) · [mdream](https://mdream.dev)