Rendering Modes
TL;DR
Nuxt supports hybrid rendering: mix SSR, SPA, prerendered, and ISR routes in a single app via routeRules. Each route gets the optimal rendering strategy β static pages are prerendered at build, dashboards are SPA, product pages use ISR for cached freshness. Configure in nuxt.config.ts, deploy to any target.
Mental Model
Think of routeRules as a per-route rendering policy:
routeRules: {
'/': { prerender: true }, // Static HTML at build time
'/about': { prerender: true }, // Static HTML at build time
'/blog/**': { isr: 3600 }, // Cached SSR, revalidate every hour
'/app/**': { ssr: false }, // SPA mode (no server rendering)
'/api/**': { cors: true }, // API: add CORS headers
'/admin/**': { ssr: false, appMiddleware: 'auth' },
}
One app. Multiple rendering strategies. Zero compromise. The marketing team gets SEO-friendly static pages. The dashboard team gets instant client-only navigation. The blog gets fast cached pages that stay fresh.
Universal Rendering (Default)
Nuxtβs default: server renders HTML on the first request, client hydrates and takes over for subsequent navigations.
First visit: Server β HTML (with data) β Client hydrates β Interactive
Navigation: Client-side β JS renders β No server round-trip
This gives you:
- Fast First Contentful Paint (HTML arrives immediately)
- SEO (crawlers see fully rendered content)
- Full interactivity after hydration
- Client-side navigation for subsequent pages
You donβt need to configure anything β this is active by default.
routeRules Configuration
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Prerender: generate static HTML at build time
'/': { prerender: true },
'/about': { prerender: true },
'/pricing': { prerender: true },
// ISR: cached SSR with background revalidation
'/blog/**': { isr: 3600 }, // Cache 1 hour
'/products/**': { isr: 600 }, // Cache 10 minutes
// SWR: stale-while-revalidate (similar to ISR)
'/docs/**': { swr: 86400 }, // Serve stale for 24h while refreshing
// SPA: client-only rendering (no SSR)
'/app/**': { ssr: false },
'/admin/**': { ssr: false },
// Headers: custom response headers
'/api/**': {
cors: true,
headers: { 'Cache-Control': 'max-age=60' },
},
// Redirects
'/old-path': { redirect: '/new-path' },
'/legacy/**': { redirect: { to: '/modern/**', statusCode: 301 } },
},
})
Pattern matching:
/pathβ exact match/path/**β matches all nested paths (recursive)/path/*β matches one segment only
Prerendering
Static HTML generated at build time. No server needed to serve these pages:
routeRules: {
'/': { prerender: true },
'/about': { prerender: true },
'/blog/my-post': { prerender: true },
}
Automatic crawling: During nuxt generate, Nuxt crawls links from prerendered pages and prerenders those too. Starting from /, it finds links to /about, /blog, etc. and generates them all.
Manual route list:
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
prerender: {
routes: ['/sitemap.xml', '/feed.rss'],
crawlLinks: true, // Follow links from prerendered pages
},
},
})
nuxi generate vs nuxi build:
nuxi buildβ builds the app (SSR server + client). Prerendered routes are generated during build.nuxi generateβ same as build but prerendering is the primary goal. Setsnitro.static: truefor routes withoutssr: false.
Use nuxi generate for fully static sites. Use nuxi build for hybrid apps (mix of static + server-rendered routes).
ISR (Incremental Static Regeneration)
The best of both worlds: serve cached HTML instantly, revalidate in the background:
routeRules: {
'/blog/**': { isr: 3600 }, // Cache for 1 hour (3600 seconds)
}
How ISR works:
Request 1 (cold):
β Server renders β Caches HTML β Returns to client
Request 2 (within 3600s):
β Returns cached HTML instantly (no rendering)
Request 3 (after 3600s):
β Returns STALE cached HTML instantly
β Triggers background re-render
β Next request gets fresh cached version
This means:
- First visitor gets SSR (slightly slower)
- All subsequent visitors get cached response (fast, like static)
- After TTL expires, the cache is refreshed in the background
- No visitor ever waits for a fresh render (stale served while regenerating)
ISR requires a running server. You canβt use ISR with pure static hosting (Netlify static, S3). You need a deployment with a server runtime (Node.js, Edge, serverless).
SWR (Stale-While-Revalidate)
Similar to ISR but with different caching semantics:
routeRules: {
'/docs/**': { swr: true }, // Cache indefinitely, revalidate on every request
'/news/**': { swr: 600 }, // Cache 10 minutes, then revalidate
}
swr: trueβ always serve cached, revalidate every time (eventual consistency)swr: <seconds>β serve cached for N seconds, then trigger revalidation
The difference from ISR is subtle: ISR has a hard TTL after which it must revalidate. SWR with true always revalidates but never blocks on it.
SPA Mode (ssr: false)
Disable server-side rendering for specific routes:
routeRules: {
'/app/**': { ssr: false }, // Entire app section is SPA
'/admin/**': { ssr: false }, // Admin panel is SPA
}
What this means:
- Server sends an empty HTML shell (no pre-rendered content)
- Client JavaScript renders the entire page
- No SEO for these pages (crawlers see empty HTML)
- Faster server response (no rendering work)
- Good for authenticated sections, dashboards, internal tools
Global SPA mode (disable SSR entirely):
// nuxt.config.ts
export default defineNuxtConfig({
ssr: false, // Entire app is SPA
})
Rendering Decision Framework
Use this flowchart:
Does the page need SEO?
βββ No β ssr: false (SPA)
β Good for: admin panels, dashboards, internal tools
βββ Yes
βββ Is content truly static (changes only on deploy)?
β βββ Yes β prerender: true
β Good for: marketing pages, about, pricing, docs
βββ No
βββ Is real-time freshness critical?
β βββ Yes β default SSR (no rule needed)
β Good for: personalized content, real-time data
βββ No
βββ How often does content change?
βββ Hourly β isr: 3600
βββ Daily β isr: 86400
βββ Weekly β isr: 604800 (or just prerender)
Good for: blog posts, product pages, news articles
Route-Level Cache Headers
Control CDN and browser caching:
routeRules: {
// API responses: cache for 60 seconds
'/api/products/**': {
headers: { 'Cache-Control': 'public, max-age=60, s-maxage=600' },
},
// Static assets: cache aggressively
'/_nuxt/**': {
headers: { 'Cache-Control': 'public, max-age=31536000, immutable' },
},
// No-cache for dynamic pages
'/dashboard/**': {
headers: { 'Cache-Control': 'no-store' },
ssr: false,
},
}
CORS Configuration
routeRules: {
'/api/**': {
cors: true, // Adds Access-Control-Allow-Origin: *
headers: {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
},
}
Verifying Rendering Modes
After deploying, verify each mode works:
# Prerendered: check for static HTML file in output
ls .output/public/about/index.html
# SSR: check response headers (no cache, full HTML)
curl -I http://localhost:3000/dashboard
# Content-Type: text/html, no strong caching
# ISR: check X-Nitro-Prerender header
curl -I http://localhost:3000/blog/my-post
# Look for cache-related headers
# SPA: view source shows empty body
curl http://localhost:3000/app/
# HTML body is minimal (just the mount point)
Walkthrough
- Set up routeRules with mixed modes:
routeRules: { '/': { prerender: true }, '/about': { prerender: true }, '/blog/**': { isr: 3600 }, '/app/**': { ssr: false }, } - Build and inspect output:
npx nuxi build ls .output/public/ # See prerendered HTML files - Run preview and test:
npx nuxi preview # Visit / β view source shows full HTML (prerendered) # Visit /app/ β view source shows empty shell (SPA) # Visit /blog/post β first load is SSR, subsequent are cached -
Check response headers for each route β verify caching behavior.
- Add a new blog post β verify ISR serves stale then refreshes.
Gotchas
ssr: falseis NOT the same as<ClientOnly>.ssr: falseon a route means the ENTIRE page is client-rendered (empty HTML shell).<ClientOnly>wraps a specific component within an otherwise SSR-rendered page.- ISR requires a server runtime. Pure static hosting (S3, GitHub Pages) canβt do ISR. You need Node.js, edge functions, or serverless with caching.
- Prerendered pages are build-time snapshots. They donβt reflect runtime data changes. If your blog post database updates, prerendered pages show stale content until the next build. Use ISR if freshness matters.
routeRulespatterns are glob-based./blog/**matches/blog/a/b/c(recursive)./blog/*matches only/blog/a(one level). Be specific.- Hybrid mode still needs a server. If ANY route uses SSR or ISR, you need a server runtime. Only a fully prerendered app (
nuxi generatewith all routes static) can deploy without a server. - SPA routes have no SEO. Search engines see an empty HTML shell. Donβt use
ssr: falsefor public-facing pages that need indexing. - ISR cache is per-instance. If you have multiple server instances (horizontal scaling), each has its own cache. Use a shared cache (Redis, CDN) for consistent behavior across instances.
nuxi generateprerendering follows links. If a prerendered page links to a dynamic page, Nuxt will try to prerender it too. Usenitro.prerender.ignoreto exclude patterns you donβt want prerendered.- First ISR request is slow. The first visitor to an uncached ISR page gets the full SSR response time. Subsequent visitors get the fast cached version. Warm your cache with a crawler if this matters.
Exercise
Configure a hybrid app:
- Prerender:
/,/about,/pricingβ verify HTML files exist in.output/public/ - ISR:
/blog/**with 1-hour cache β build, visit a blog page, refresh β second load should be from cache - SPA:
/app/**β verify empty HTML shell in view-source - Headers:
/api/**β add CORS +Cache-Control: max-age=60 - Redirect:
/old-blog/**β/blog/**with 301 status - Verify: use
curl -Ito check response headers for each route category. Confirm prerendered routes serve instantly, ISR routes show caching headers, and SPA routes have no pre-rendered content.