Nuxt 3: SSR and Static Generation Complete Guide
Master SSR and static generation with Nuxt 3. From useFetch to route rules, learn how to optimize performance for your Vue.js applications.

Nuxt 3 transforms how Vue.js applications are built by offering multiple rendering modes suited to different use cases. From Server-Side Rendering (SSR) to static generation and hybrid rendering, the framework provides remarkable flexibility for optimizing performance and SEO.
This tutorial assumes basic knowledge of Vue 3 and the Composition API. Familiarity with server-side rendering concepts is helpful but not required, as fundamentals are explained throughout the guide.
Understanding Nuxt 3 Rendering Modes
Before diving into code, understanding the differences between available rendering modes is essential. Each mode addresses specific needs in terms of performance, SEO, and user experience.
SSR (Server-Side Rendering) generates HTML on the server for each request. Static generation (SSG) pre-generates all pages at build time. Hybrid mode allows combining these approaches on a per-page basis.
// Configuration for different rendering modes
export default defineNuxtConfig({
// SSR enabled by default (recommended for SEO)
ssr: true,
// Static generation: pre-renders all pages
// Use 'npm run generate' to build
// target: 'static', // Nuxt 2 syntax
// Hybrid mode: configurable per route
routeRules: {
// Homepage: pre-rendered and cached
'/': { prerender: true },
// Blog: static generation
'/blog/**': { prerender: true },
// Dashboard: client-side rendering only
'/dashboard/**': { ssr: false },
// API: no pre-rendering
'/api/**': { prerender: false }
}
})This configuration demonstrates the power of hybrid mode: each section of the application uses the rendering mode best suited to its needs.
Data Fetching with useFetch and useAsyncData
Nuxt 3 provides two main composables for fetching data isomorphically. These composables work both server-side and client-side, with automatic hydration management.
useFetch is a wrapper around useAsyncData that simplifies HTTP calls. useAsyncData offers more control for advanced use cases.
<script setup lang="ts">
// pages/blog/[slug].vue
// Article detail page with useFetch
// Get route parameter
const route = useRoute()
// useFetch: automatic data fetching
// Data is fetched server-side then hydrated on client
const { data: article, pending, error } = await useFetch(
`/api/articles/${route.params.slug}`,
{
// Unique key for caching and deduplication
key: `article-${route.params.slug}`,
// Transform data if needed
transform: (response) => response.data,
// Cache options
getCachedData: (key) => {
// Check if data is cached
const nuxtApp = useNuxtApp()
return nuxtApp.payload.data[key]
}
}
)
// Error handling with navigation
if (error.value) {
throw createError({
statusCode: 404,
message: 'Article not found'
})
}
</script>
<template>
<div>
<div v-if="pending" class="loading">
Loading article...
</div>
<article v-else-if="article">
<h1>{{ article.title }}</h1>
<div v-html="article.content" />
</article>
</div>
</template>For cases requiring more control, useAsyncData allows executing any asynchronous function.
<script setup lang="ts">
// pages/products/index.vue
// Product list with useAsyncData and filters
const route = useRoute()
// useAsyncData: full control over fetching logic
const { data: products, refresh } = await useAsyncData(
'products-list',
async () => {
// Fetch from multiple sources if needed
const [productsResponse, categoriesResponse] = await Promise.all([
$fetch('/api/products', {
query: {
category: route.query.category,
sort: route.query.sort || 'date'
}
}),
$fetch('/api/categories')
])
// Combine and transform data
return {
products: productsResponse.data,
categories: categoriesResponse.data,
total: productsResponse.meta.total
}
},
{
// Refresh when query params change
watch: [() => route.query]
}
)
// Manual refresh function
const updateFilters = async (newCategory: string) => {
await navigateTo({
query: { ...route.query, category: newCategory }
})
}
</script>These composables prevent double-fetching: data retrieved on the server is serialized into the HTML payload and reused during client-side hydration.
Customizing SSR with Server Hooks
Nuxt 3's SSR can be customized through server hooks. These hooks allow intervention at different stages of the rendering cycle to modify default behavior.
// Server plugin to customize SSR rendering
export default defineNitroPlugin((nitroApp) => {
// Hook executed before rendering each page
nitroApp.hooks.hook('render:html', (html, { event }) => {
// Inject scripts or metadata
html.head.push(`
<script>
// Analytics or global configuration
window.__APP_CONFIG__ = {
environment: '${process.env.NODE_ENV}',
apiUrl: '${process.env.API_URL}'
}
</script>
`)
})
// Hook for render cache management
nitroApp.hooks.hook('render:response', (response, { event }) => {
// Add custom cache headers
const path = event.path
if (path.startsWith('/blog/')) {
// Long cache for blog articles
response.headers['Cache-Control'] = 'public, max-age=3600, s-maxage=86400'
} else if (path.startsWith('/api/')) {
// No cache for APIs
response.headers['Cache-Control'] = 'no-store'
}
})
})The render:response hook is ideal for implementing HTTP cache strategies. Combining SSR with a CDN that respects Cache-Control headers enables serving pre-rendered pages while maintaining the ability to invalidate them.
Static Generation with nuxt generate
Static generation pre-builds all pages at build time. This approach is ideal for sites with stable content like blogs, documentation, or marketing sites.
For dynamic routes, Nuxt needs to know all URLs to generate. The prerender:routes hook allows defining these routes programmatically.
// Complete configuration for static generation
export default defineNuxtConfig({
// Enable static generation
nitro: {
prerender: {
// Enable automatic link crawling
crawlLinks: true,
// Routes to always include
routes: ['/', '/about', '/contact'],
// Ignore certain routes
ignore: ['/admin', '/api']
}
},
hooks: {
// Hook to generate dynamic routes
async 'prerender:routes'(ctx) {
// Fetch articles from API or DB
const articles = await fetch('https://api.example.com/articles')
.then(res => res.json())
// Add article routes
for (const article of articles) {
ctx.routes.add(`/blog/${article.slug}`)
}
// Fetch categories
const categories = await fetch('https://api.example.com/categories')
.then(res => res.json())
for (const category of categories) {
ctx.routes.add(`/category/${category.slug}`)
}
}
}
})For projects with many pages, the automatic crawler may be insufficient. Here's a more robust approach with a separate configuration file.
// Utility to generate dynamic route list
import { prisma } from './prisma'
export async function getAllStaticRoutes(): Promise<string[]> {
const routes: string[] = []
// Blog articles
const articles = await prisma.article.findMany({
where: { published: true },
select: { slug: true, category: { select: { slug: true } } }
})
for (const article of articles) {
routes.push(`/blog/${article.category.slug}/${article.slug}`)
}
// Product pages
const products = await prisma.product.findMany({
where: { active: true },
select: { slug: true }
})
for (const product of products) {
routes.push(`/products/${product.slug}`)
}
// Tag pages
const tags = await prisma.tag.findMany({
select: { slug: true }
})
for (const tag of tags) {
routes.push(`/tags/${tag.slug}`)
}
return routes
}Ready to ace your Vue.js / Nuxt.js interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Hybrid Rendering with routeRules
Hybrid rendering is Nuxt 3's flagship feature. It allows defining different rendering rules per route, combining the best of SSR and SSG.
// Advanced hybrid rendering configuration
export default defineNuxtConfig({
routeRules: {
// Marketing pages: pre-rendered and cached long-term
'/': { prerender: true },
'/pricing': { prerender: true },
'/features/**': { prerender: true },
// Blog: ISR (Incremental Static Regeneration)
// Revalidation every hour
'/blog/**': {
isr: 3600,
prerender: true
},
// Documentation: CDN cache with revalidation
'/docs/**': {
swr: 86400, // Stale-while-revalidate
prerender: true
},
// E-commerce: SSR with short cache
'/products/**': {
ssr: true,
cache: {
maxAge: 60,
staleMaxAge: 300
}
},
// Cart and checkout: client-side only
'/cart': { ssr: false },
'/checkout/**': { ssr: false },
// Dashboard: SPA mode
'/dashboard/**': {
ssr: false,
// Disable prerendering
prerender: false
},
// API routes: no cache by default
'/api/**': {
cors: true,
headers: {
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE'
}
}
}
})This configuration illustrates a typical modern application architecture: public pages are optimized for SEO with SSG, while interactive sections use client rendering.
Performance Optimization with Data Caching
Beyond page caching, Nuxt 3 allows caching fetched data. This strategy reduces API load and improves response times.
// API endpoint with data caching
import { getArticleBySlug } from '~/server/utils/articles'
export default defineCachedEventHandler(
async (event) => {
const slug = getRouterParam(event, 'slug')
if (!slug) {
throw createError({
statusCode: 400,
message: 'Missing slug'
})
}
const article = await getArticleBySlug(slug)
if (!article) {
throw createError({
statusCode: 404,
message: 'Article not found'
})
}
return article
},
{
// Cache key based on slug
getKey: (event) => `article-${getRouterParam(event, 'slug')}`,
// Cache duration: 1 hour
maxAge: 3600,
// Stale-while-revalidate: serve stale cache during update
staleMaxAge: 7200,
// Tag-based invalidation
tags: ['articles']
}
)To invalidate cache when content changes, Nuxt provides a tag system.
// Article update with cache invalidation
import { updateArticle } from '~/server/utils/articles'
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
const body = await readBody(event)
// Update the article
const article = await updateArticle(slug, body)
// Invalidate cache for this article
await useStorage('cache').removeItem(`nitro:handlers:article-${slug}`)
// Or tag-based invalidation (all articles)
// await useStorage('cache').clear('articles')
return article
})In production with multiple instances, in-memory cache is insufficient. Configuring Redis or another distributed system via Nitro configuration is recommended to ensure consistency across instances.
SEO and Metadata Management
SSR enables SEO optimization by generating metadata server-side. Nuxt 3 offers several approaches for managing meta tags dynamically.
<script setup lang="ts">
// pages/blog/[slug].vue
// Blog page with optimized SEO
const route = useRoute()
const { data: article } = await useFetch(`/api/articles/${route.params.slug}`)
// Dynamic SEO configuration based on article
useSeoMeta({
title: article.value?.title,
description: article.value?.excerpt,
ogTitle: article.value?.title,
ogDescription: article.value?.excerpt,
ogImage: article.value?.coverImage,
ogType: 'article',
twitterCard: 'summary_large_image',
twitterTitle: article.value?.title,
twitterDescription: article.value?.excerpt,
twitterImage: article.value?.coverImage
})
// Structured data for Google
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.value?.title,
description: article.value?.excerpt,
image: article.value?.coverImage,
datePublished: article.value?.publishedAt,
dateModified: article.value?.updatedAt,
author: {
'@type': 'Organization',
name: 'SharpSkill'
}
})
}
]
})
</script>For static pages, metadata can be defined directly in the component.
<script setup lang="ts">
// pages/about.vue
// Static page with SEO
definePageMeta({
title: 'About'
})
useSeoMeta({
title: 'About SharpSkill | Tech Interview Preparation',
description: 'Discover SharpSkill, the tech interview preparation platform. Our mission: helping developers succeed in their technical interviews.',
ogTitle: 'About SharpSkill',
ogDescription: 'The tech interview preparation platform',
ogImage: '/images/og-about.webp'
})
</script>Deployment and Production Considerations
The deployment choice depends on the rendering mode used. Here are the main options and their configurations.
// Configuration for different deployment environments
export default defineNuxtConfig({
nitro: {
// Preset based on target platform
// preset: 'vercel', // Vercel
// preset: 'netlify', // Netlify
// preset: 'cloudflare-pages', // Cloudflare
// preset: 'node-server', // Classic Node.js
// Configuration for Node.js in production
preset: 'node-server',
// Response compression
compressPublicAssets: true,
// Cache storage configuration
storage: {
cache: {
driver: 'redis',
url: process.env.REDIS_URL
}
}
},
// Runtime environment variables
runtimeConfig: {
// Secrets (not exposed to client)
apiSecret: process.env.API_SECRET,
// Public configuration
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
}
}
})For static deployment, the npm run generate command creates an .output/public folder ready to deploy on any static file host.
# Static generation
npm run generate
# The .output/public content can be deployed to:
# - Vercel (automatic detection)
# - Netlify (automatic configuration)
# - GitHub Pages
# - S3 + CloudFront
# - Any CDN or static file serverStart practicing!
Test your knowledge with our interview simulators and technical tests.
Conclusion
Nuxt 3 offers exceptional flexibility for rendering Vue.js applications. The choice between SSR, SSG, and hybrid rendering depends on each project's specific needs.
Key takeaways:
✅ SSR: ideal for dynamic content requiring good SEO (e-commerce, news sites)
✅ SSG: perfect for stable content (blogs, documentation, marketing sites)
✅ Hybrid: the best approach for complex applications with varied needs
✅ useFetch/useAsyncData: automatic hydration and cache management
✅ routeRules: fine-grained configuration for each route's behavior
✅ Caching: multiple strategies to optimize production performance
Combining hybrid rendering with a well-thought-out caching strategy enables building performant applications optimized for SEO while maintaining the interactivity of Single Page Applications.
Tags
Share
Related articles

Essential Vue.js Interview Questions: 25 Questions to Land the Job
Prepare for Vue.js interviews with these 25 essential questions. From reactivity to composables, master the key concepts to ace your next interview.

Vue 3 Composition API: Complete Guide to Mastering Reactivity
Master Vue 3 Composition API with this practical guide. Learn ref, reactive, computed, watch, and composables to build performant Vue applications.

Angular 19 Interview Questions: Signals, SSR and Must-Know Concepts
The most common Angular 19 interview questions: Signals, incremental hydration, zoneless change detection, and new reactive APIs with code examples and expected answers.