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, but it's slower and less reliable than HTML. Prerendering removes the wait.
Use prerendering when:
Don't prerender when:
If your /about and /pricing pages never change, prerender them. If you're showing user-generated content or real-time data, you need SSR.
Build-time prerendering (vite-ssg, prerender-spa-plugin):
npm run buildOn-demand prerendering (Prerender.io, Rendertron):
Most Vue apps need build-time prerendering. On-demand is for SPAs with frequent content changes that can't use SSR.
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:
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, Vercel, Cloudflare Pages—anywhere that serves static files.
Limitations:
/user/:id unless you generate all IDs)prerender-spa-plugin uses Puppeteer to render pages. Works with webpack and Vue CLI, but hasn't been updated since 2019. Use vite-ssg for new projects.
If you're on Vue CLI:
npm install -D prerender-spa-plugin
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:
Prerender.io 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:
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:
When to use:
Don't use if:
Rendertron is Google's open-source prerendering service. You host it yourself.
docker run -p 3000:3000 rendertron/rendertron
Configure your server to proxy bot requests:
import express from 'express'
const app = express()
const RENDERTRON_URL = 'http://localhost:3000'
app.use((req, res, next) => {
const botPattern = /googlebot|bingbot|slackbot|twitterbot/i
if (botPattern.test(req.headers['user-agent'])) {
const url = `${RENDERTRON_URL}/render/${req.protocol}://${req.get('host')}${req.url}`
// Fetch from Rendertron, return HTML
return fetch(url).then(r => r.text()).then(html => res.send(html))
}
next()
})
Benefits:
Drawbacks:
Your Vue app needs to know when it's being prerendered to avoid browser-only APIs:
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:
localStorage during prerender (crashes build)http://localhost (fails in CI)data-server-rendered="true" on root elementAdd to your root element so Vue hydrates instead of re-rendering:
<div id="app" data-server-rendered="true"></div>
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:
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
3. Fetch as Googlebot
curl -A "Mozilla/5.0 (compatible; Googlebot/2.1)" https://yoursite.com/about
If you see empty content, prerendering failed.
Mistake 1: Blocking JavaScript in robots.txt
# ❌ Breaks prerendering detection
User-agent: *
Disallow: /*.js$
Google needs JavaScript to hydrate your app. Never block .js or .css from Googlebot. See our robots.txt guide.
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:
| Approach | Cost | Setup | Best For |
|---|---|---|---|
| vite-ssg | Free | Easy | Modern Vue apps, <1000 routes |
| prerender-spa-plugin | Free | Medium | Legacy Vue CLI projects |
| Prerender.io | $30+/mo | Easy | Frequent updates, can't use SSR |
| Rendertron | Hosting | Hard | High traffic, need control |
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 →
When should you use prerendering vs SSR?
Prerendering when content updates hourly - SSR is better for frequently updated contentSSR when routes are known at build time - Prerendering works well for static routes known at build timePrerendering for static content, SSR for dynamic content - Correct! Prerender static pages, use SSR for user-specific or frequently updated contentSPA SEO
Why Vue SPAs struggle with search engines and how to fix it. Learn when you need SSR, when prerendering works, and when SPA is fine as-is.
Dynamic Rendering
Dynamic rendering serves pre-rendered HTML to crawlers while users see JavaScript. Google no longer recommends this—use SSR or SSG instead.