Nuxt 3 : SSR et génération statique, le guide complet
Maîtrisez le SSR et la génération statique avec Nuxt 3. De useFetch à generateRoutes, découvrez comment optimiser les performances de vos applications Vue.

Nuxt 3 transforme la façon de construire des applications Vue.js en proposant plusieurs modes de rendu adaptés à chaque cas d'usage. Du Server-Side Rendering (SSR) à la génération statique, en passant par le rendu hybride, le framework offre une flexibilité remarquable pour optimiser les performances et le SEO.
Ce tutoriel suppose une connaissance de base de Vue 3 et de la Composition API. Une familiarité avec les concepts de rendu côté serveur est un plus, mais les fondamentaux sont expliqués au fil du guide.
Comprendre les modes de rendu de Nuxt 3
Avant de plonger dans le code, il est essentiel de comprendre les différences entre les modes de rendu disponibles. Chaque mode répond à des besoins spécifiques en termes de performance, SEO et expérience utilisateur.
Le SSR (Server-Side Rendering) génère le HTML sur le serveur à chaque requête. La génération statique (SSG) pré-génère toutes les pages au moment du build. Le mode hybride permet de combiner ces approches page par page.
// Configuration des différents modes de rendu
export default defineNuxtConfig({
// SSR activé par défaut (recommandé pour le SEO)
ssr: true,
// Génération statique : pré-rend toutes les pages
// Utiliser 'npm run generate' pour builder
// target: 'static', // Nuxt 2 syntax
// Mode hybride : configurable par route
routeRules: {
// Page d'accueil : pré-rendue et mise en cache
'/': { prerender: true },
// Blog : génération statique
'/blog/**': { prerender: true },
// Dashboard : rendu côté client uniquement
'/dashboard/**': { ssr: false },
// API : pas de pré-rendu
'/api/**': { prerender: false }
}
})Cette configuration montre la puissance du mode hybride : chaque section de l'application utilise le mode de rendu le plus adapté à ses besoins.
Récupération de données avec useFetch et useAsyncData
Nuxt 3 propose deux composables principaux pour récupérer des données de manière isomorphique. Ces composables fonctionnent aussi bien côté serveur que côté client, avec une gestion automatique de l'hydratation.
useFetch est un wrapper autour de useAsyncData qui simplifie les appels HTTP. useAsyncData offre plus de contrôle pour les cas d'usage avancés.
<script setup lang="ts">
// pages/blog/[slug].vue
// Page de détail d'article avec useFetch
// Récupération du paramètre de route
const route = useRoute()
// useFetch : récupération automatique des données
// Les données sont fetched côté serveur puis hydratées côté client
const { data: article, pending, error } = await useFetch(
`/api/articles/${route.params.slug}`,
{
// Clé unique pour le cache et la déduplication
key: `article-${route.params.slug}`,
// Transformation des données si nécessaire
transform: (response) => response.data,
// Options de cache
getCachedData: (key) => {
// Vérifie si les données sont en cache
const nuxtApp = useNuxtApp()
return nuxtApp.payload.data[key]
}
}
)
// Gestion des erreurs avec navigation
if (error.value) {
throw createError({
statusCode: 404,
message: 'Article non trouvé'
})
}
</script>
<template>
<div>
<div v-if="pending" class="loading">
Chargement de l'article...
</div>
<article v-else-if="article">
<h1>{{ article.title }}</h1>
<div v-html="article.content" />
</article>
</div>
</template>Pour les cas nécessitant plus de contrôle, useAsyncData permet d'exécuter n'importe quelle fonction asynchrone.
<script setup lang="ts">
// pages/products/index.vue
// Liste de produits avec useAsyncData et filtres
const route = useRoute()
// useAsyncData : contrôle total sur la logique de récupération
const { data: products, refresh } = await useAsyncData(
'products-list',
async () => {
// Récupération depuis plusieurs sources si nécessaire
const [productsResponse, categoriesResponse] = await Promise.all([
$fetch('/api/products', {
query: {
category: route.query.category,
sort: route.query.sort || 'date'
}
}),
$fetch('/api/categories')
])
// Combinaison et transformation des données
return {
products: productsResponse.data,
categories: categoriesResponse.data,
total: productsResponse.meta.total
}
},
{
// Rafraîchir quand les query params changent
watch: [() => route.query]
}
)
// Fonction de rafraîchissement manuel
const updateFilters = async (newCategory: string) => {
await navigateTo({
query: { ...route.query, category: newCategory }
})
}
</script>Ces composables évitent le double-fetching : les données récupérées sur le serveur sont sérialisées dans le payload HTML et réutilisées lors de l'hydratation côté client.
Configuration du SSR avec les hooks serveur
Le SSR de Nuxt 3 peut être personnalisé grâce aux hooks serveur. Ces hooks permettent d'intervenir à différentes étapes du cycle de rendu pour modifier le comportement par défaut.
// Plugin serveur pour personnaliser le rendu SSR
export default defineNitroPlugin((nitroApp) => {
// Hook exécuté avant le rendu de chaque page
nitroApp.hooks.hook('render:html', (html, { event }) => {
// Injection de scripts ou de métadonnées
html.head.push(`
<script>
// Analytics ou configuration globale
window.__APP_CONFIG__ = {
environment: '${process.env.NODE_ENV}',
apiUrl: '${process.env.API_URL}'
}
</script>
`)
})
// Hook pour la gestion du cache de rendu
nitroApp.hooks.hook('render:response', (response, { event }) => {
// Ajout d'en-têtes de cache personnalisés
const path = event.path
if (path.startsWith('/blog/')) {
// Cache long pour les articles de blog
response.headers['Cache-Control'] = 'public, max-age=3600, s-maxage=86400'
} else if (path.startsWith('/api/')) {
// Pas de cache pour les API
response.headers['Cache-Control'] = 'no-store'
}
})
})Le hook render:response est idéal pour implémenter des stratégies de cache HTTP. Combiner le SSR avec un CDN qui respecte les headers Cache-Control permet de servir des pages pré-rendues tout en gardant la possibilité de les invalider.
Génération statique avec nuxt generate
La génération statique pré-construit toutes les pages au moment du build. Cette approche est idéale pour les sites à contenu stable comme les blogs, les documentations ou les sites vitrines.
Pour les routes dynamiques, Nuxt doit connaître toutes les URLs à générer. Le hook prerender:routes permet de définir ces routes programmatiquement.
// Configuration complète pour la génération statique
export default defineNuxtConfig({
// Activer la génération statique
nitro: {
prerender: {
// Activer le crawling automatique des liens
crawlLinks: true,
// Routes à toujours inclure
routes: ['/', '/about', '/contact'],
// Ignorer certaines routes
ignore: ['/admin', '/api']
}
},
hooks: {
// Hook pour générer les routes dynamiques
async 'prerender:routes'(ctx) {
// Récupération des articles depuis l'API ou la DB
const articles = await fetch('https://api.example.com/articles')
.then(res => res.json())
// Ajout des routes d'articles
for (const article of articles) {
ctx.routes.add(`/blog/${article.slug}`)
}
// Récupération des catégories
const categories = await fetch('https://api.example.com/categories')
.then(res => res.json())
for (const category of categories) {
ctx.routes.add(`/category/${category.slug}`)
}
}
}
})Pour les projets avec un grand nombre de pages, le crawler automatique peut être insuffisant. Voici une approche plus robuste avec un fichier de configuration séparé.
// Utilitaire pour générer la liste des routes dynamiques
import { prisma } from './prisma'
export async function getAllStaticRoutes(): Promise<string[]> {
const routes: string[] = []
// Articles de blog
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}`)
}
// Pages produits
const products = await prisma.product.findMany({
where: { active: true },
select: { slug: true }
})
for (const product of products) {
routes.push(`/products/${product.slug}`)
}
// Pages de tags
const tags = await prisma.tag.findMany({
select: { slug: true }
})
for (const tag of tags) {
routes.push(`/tags/${tag.slug}`)
}
return routes
}Prêt à réussir tes entretiens Vue.js / Nuxt.js ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Rendu hybride avec routeRules
Le rendu hybride est la fonctionnalité phare de Nuxt 3. Il permet de définir des règles de rendu différentes selon les routes, combinant le meilleur du SSR et du SSG.
// Configuration avancée du rendu hybride
export default defineNuxtConfig({
routeRules: {
// Pages marketing : pré-rendues et mises en cache longtemps
'/': { prerender: true },
'/pricing': { prerender: true },
'/features/**': { prerender: true },
// Blog : ISR (Incremental Static Regeneration)
// Revalidation toutes les heures
'/blog/**': {
isr: 3600,
prerender: true
},
// Documentation : CDN cache avec revalidation
'/docs/**': {
swr: 86400, // Stale-while-revalidate
prerender: true
},
// E-commerce : SSR avec cache court
'/products/**': {
ssr: true,
cache: {
maxAge: 60,
staleMaxAge: 300
}
},
// Panier et checkout : client-side uniquement
'/cart': { ssr: false },
'/checkout/**': { ssr: false },
// Dashboard : SPA mode
'/dashboard/**': {
ssr: false,
// Désactiver le prerendering
prerender: false
},
// API routes : pas de cache par défaut
'/api/**': {
cors: true,
headers: {
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE'
}
}
}
})Cette configuration illustre une architecture typique d'application moderne : les pages publiques sont optimisées pour le SEO avec SSG, tandis que les sections interactives utilisent le rendu client.
Optimisation des performances avec le cache de données
Au-delà du cache de pages, Nuxt 3 permet de mettre en cache les données récupérées. Cette stratégie réduit la charge sur les APIs et améliore les temps de réponse.
// Endpoint API avec mise en cache des données
import { getArticleBySlug } from '~/server/utils/articles'
export default defineCachedEventHandler(
async (event) => {
const slug = getRouterParam(event, 'slug')
if (!slug) {
throw createError({
statusCode: 400,
message: 'Slug manquant'
})
}
const article = await getArticleBySlug(slug)
if (!article) {
throw createError({
statusCode: 404,
message: 'Article non trouvé'
})
}
return article
},
{
// Clé de cache basée sur le slug
getKey: (event) => `article-${getRouterParam(event, 'slug')}`,
// Durée de vie du cache : 1 heure
maxAge: 3600,
// Stale-while-revalidate : servir le cache périmé pendant la mise à jour
staleMaxAge: 7200,
// Invalidation par tags
tags: ['articles']
}
)Pour invalider le cache quand le contenu change, Nuxt propose un système de tags.
// Mise à jour d'article avec invalidation du cache
import { updateArticle } from '~/server/utils/articles'
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
const body = await readBody(event)
// Mise à jour de l'article
const article = await updateArticle(slug, body)
// Invalidation du cache pour cet article
await useStorage('cache').removeItem(`nitro:handlers:article-${slug}`)
// Ou invalidation par tag (tous les articles)
// await useStorage('cache').clear('articles')
return article
})En production avec plusieurs instances, le cache en mémoire ne suffit pas. Il est recommandé de configurer un cache Redis ou un autre système distribué via la configuration Nitro pour garantir la cohérence.
Gestion du SEO et des métadonnées
Le SSR permet d'optimiser le SEO en générant les métadonnées côté serveur. Nuxt 3 propose plusieurs approches pour gérer les balises meta de manière dynamique.
<script setup lang="ts">
// pages/blog/[slug].vue
// Page de blog avec SEO optimisé
const route = useRoute()
const { data: article } = await useFetch(`/api/articles/${route.params.slug}`)
// Configuration SEO dynamique basée sur l'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
})
// Données structurées pour 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>Pour les pages statiques, les métadonnées peuvent être définies directement dans le composant.
<script setup lang="ts">
// pages/about.vue
// Page statique avec SEO
definePageMeta({
title: 'À propos'
})
useSeoMeta({
title: 'À propos de SharpSkill | Préparation aux entretiens tech',
description: 'Découvrez SharpSkill, la plateforme de préparation aux entretiens techniques. Notre mission : aider les développeurs à réussir leurs entretiens.',
ogTitle: 'À propos de SharpSkill',
ogDescription: 'La plateforme de préparation aux entretiens tech',
ogImage: '/images/og-about.webp'
})
</script>Déploiement et considérations de production
Le choix du mode de déploiement dépend du mode de rendu utilisé. Voici les options principales et leurs configurations.
// Configuration pour différents environnements de déploiement
export default defineNuxtConfig({
nitro: {
// Preset selon la plateforme cible
// preset: 'vercel', // Vercel
// preset: 'netlify', // Netlify
// preset: 'cloudflare-pages', // Cloudflare
// preset: 'node-server', // Node.js classique
// Configuration pour Node.js en production
preset: 'node-server',
// Compression des réponses
compressPublicAssets: true,
// Configuration du stockage de cache
storage: {
cache: {
driver: 'redis',
url: process.env.REDIS_URL
}
}
},
// Variables d'environnement runtime
runtimeConfig: {
// Secrets (non exposés au client)
apiSecret: process.env.API_SECRET,
// Configuration publique
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
}
}
})Pour le déploiement statique, la commande npm run generate crée un dossier .output/public prêt à être déployé sur n'importe quel hébergeur de fichiers statiques.
# Génération statique
npm run generate
# Le contenu de .output/public peut être déployé sur :
# - Vercel (détection automatique)
# - Netlify (configuration automatique)
# - GitHub Pages
# - S3 + CloudFront
# - Tout CDN ou serveur de fichiers statiquesPasse à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Conclusion
Nuxt 3 offre une flexibilité exceptionnelle pour le rendu des applications Vue.js. Le choix entre SSR, SSG et rendu hybride dépend des besoins spécifiques de chaque projet.
Points clés à retenir :
✅ SSR : idéal pour le contenu dynamique nécessitant un bon SEO (e-commerce, actualités)
✅ SSG : parfait pour le contenu stable (blogs, documentation, sites vitrines)
✅ Hybride : la meilleure approche pour les applications complexes avec des besoins variés
✅ useFetch/useAsyncData : gestion automatique de l'hydratation et du cache
✅ routeRules : configuration fine du comportement de chaque route
✅ Cache : stratégies multiples pour optimiser les performances en production
La combinaison du rendu hybride avec une stratégie de cache bien pensée permet de construire des applications performantes, optimisées pour le SEO, tout en conservant l'interactivité des Single Page Applications.
Tags
Partager
Articles similaires

Questions d'entretien Vue.js essentielles : 25 questions pour réussir
Préparez vos entretiens Vue.js avec ces 25 questions essentielles. De la réactivité aux composables, maîtrisez les concepts clés pour décrocher le poste.

Vue 3 Composition API : Guide complet pour maîtriser la réactivité
Découvrez Vue 3 Composition API avec ce guide pratique. Apprenez ref, reactive, computed, watch et les composables pour créer des applications Vue performantes.

Angular 19 en entretien : Signals, SSR et les questions incontournables
Les questions d'entretien Angular 19 les plus fréquentes : Signals, SSR avec hydratation incrémentale, détection de changement zoneless et nouvelles API réactives.