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.

Illustration montrant le rendu serveur et la génération statique avec Nuxt 3 et Vue.js

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.

Prérequis

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.

nuxt.config.tstypescript
// 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.

vue
<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.

vue
<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.

server/plugins/render-hooks.tstypescript
// 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'
    }
  })
})
Performance SSR

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.

nuxt.config.tstypescript
// 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é.

server/utils/generate-routes.tstypescript
// 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.

nuxt.config.tstypescript
// 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.

server/api/articles/[slug].get.tstypescript
// 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.

server/api/articles/[slug].put.tstypescript
// 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
})
Cache distribué

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.

vue
<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.

vue
<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.

nuxt.config.tstypescript
// 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.

bash
# 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 statiques

Passe à 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

#nuxt 3
#vue.js
#ssr
#génération statique
#performance web

Partager

Articles similaires