Retour au cours

frontend / vuejs

SSR & Nuxt (concepts)

Leçon 211 exercice

Explication

Ce que vous allez apprendre

  • Expliquer la différence entre un rendu 100% client (SPA) et un rendu côté serveur (SSR)
  • Comprendre le mécanisme d'hydratation qui relie le HTML serveur au Vue côté client
  • Utiliser Nuxt 3 pour bénéficier du routing par fichiers et des API serveur intégrées
  • Choisir la bonne stratégie de rendu (SSR, SSG, SWR, client uniquement) selon chaque route
  • Éviter le piège classique du code qui accède à window/document côté serveur

Dans quel contexte ?

Une boutique en ligne construite en SPA Vue classique constate que ses fiches produits n'apparaissent quasiment jamais dans les résultats Google, malgré un excellent contenu : les robots d'indexation reçoivent un HTML quasi vide, le contenu réel n'apparaissant qu'après exécution du JavaScript. C'est exactement le problème que le rendu côté serveur (SSR) résout, en envoyant directement un HTML déjà rempli au navigateur — et donc aussi aux moteurs de recherche.

D'abord, comprendre la différence fondamentale entre SPA et SSR

Dans une application 100% client, le navigateur reçoit un HTML presque vide, télécharge le bundle JavaScript, puis Vue construit le DOM et le contenu devient enfin visible. En SSR, le serveur exécute lui-même les composants Vue pour générer un HTML complet, envoyé directement au navigateur — le contenu est visible immédiatement, avant même que le JavaScript ne soit chargé.

Prérequis

Cette leçon suppose une bonne maîtrise du cycle de vie des composants (onMounted notamment) et du routing avec Vue Router, vus dans les leçons précédentes.

Une fois le HTML reçu, il reste une étape essentielle : l'hydratation

Le HTML envoyé par le serveur est statique — aucun gestionnaire d'événement n'y est encore attaché. L'hydratation est le processus par lequel Vue, une fois chargé côté client, attache ses écouteurs d'événements sur ce HTML existant sans le reconstruire depuis zéro. C'est ce qui permet de cumuler affichage instantané et interactivité complète, sans double travail.

Stratégie de renduQuand l'utiliser
SSR classiqueContenu dynamique nécessitant SEO (fiches produits, articles)
SSG (prerender: true)Contenu qui change rarement (page d'accueil, pages statiques)
SWR (swr: 3600)Contenu mis à jour périodiquement, servi depuis un cache régénéré en arrière-plan
Client uniquement (ssr: false)Données privées sans besoin de SEO (tableau de bord utilisateur)

Il reste un problème pratique à connaître avant d'écrire du code Nuxt : le piège de window

Piège courant

Le code exécuté côté serveur n'a jamais accès à window, document ou localStorage — ces objets n'existent tout simplement pas dans l'environnement Node.js du serveur. Toute tentative directe d'y accéder fait planter le rendu serveur. La solution consiste à isoler ce code dans onMounted, qui ne s'exécute jamais côté serveur, uniquement côté client après le montage du composant.

Enfin, Nuxt 3 structure ce mécanisme dans un framework complet

Nuxt ajoute un routing par fichiers (pages/produits/[id].vue devient automatiquement /produits/:id), des composants et composables auto-importés sans écrire de import, et des endpoints API serveur colocalisés avec le frontend dans server/api/. useAsyncData va plus loin en évitant un double appel réseau : la donnée récupérée côté serveur est sérialisée et transmise directement au client lors de l'hydratation, sans nouvel appel HTTP.

Bonne pratique

Utilise routeRules dans nuxt.config.ts pour choisir la stratégie de rendu page par page plutôt que globalement. Une page d'accueil peut être pré-générée en statique (SSG) pendant qu'un tableau de bord privé reste en rendu client uniquement — Nuxt permet ce mélange sans compromis.

Après avoir compris le rendu côté serveur, la dernière leçon de ce parcours prend du recul pour aborder l'organisation d'une grosse application Vue à l'échelle d'une équipe entière.

Commandes & code

SSR & Nuxt (concepts)

Le rendu côté serveur (SSR) génère le HTML sur le serveur avant de l'envoyer au navigateur : meilleur SEO, meilleur temps d'affichage initial.

bash
Rendu client uniquement (SPA classique)          Rendu côté serveur (SSR)
──────────────────────────────────────           ──────────────────────────────────
1. Navigateur reçoit un HTML quasi vide           1. Serveur exécute le composant Vue
2. Télécharge le bundle JS                        2. Génère le HTML complet (contenu visible)
3. Vue s'exécute, construit le DOM                3. Navigateur reçoit un HTML déjà rempli
4. Contenu enfin visible                          4. "Hydratation" : Vue attache les listeners
                                                      sur le HTML existant (pas de reconstruction)
bash
# Nuxt 3 est le framework SSR/meta-framework officiel bâti sur Vue 3
npx nuxi@latest init mon-app-nuxt
cd mon-app-nuxt
npm run dev
bash
mon-app-nuxt/
├── pages/              # routing par fichiers -> pages/produits/[id].vue = /produits/:id
├── components/         # auto-importés, pas besoin d'écrire `import`
├── composables/        # auto-importés également
├── server/api/         # endpoints API serveur (ex: server/api/produits.get.ts)
├── layouts/            # gabarits partagés (default.vue, admin.vue...)
├── app.vue             # point d'entrée
└── nuxt.config.ts      # configuration centrale
vue
<!-- pages/produits/[id].vue — routing par fichier, useAsyncData exécuté CÔTÉ SERVEUR puis hydraté -->
<script setup>
const route = useRoute()

// useAsyncData : exécuté sur le serveur au premier rendu, le résultat est sérialisé
// et transmis au client (pas de double appel réseau lors de l'hydratation).
const { data: produit, pending, error } = await useAsyncData(
  `produit-${route.params.id}`,
  () => $fetch(`/api/produits/${route.params.id}`)
)

// useSeoMeta : injecte les balises meta CÔTÉ SERVEUR, essentiel pour le SEO et les aperçus sociaux
useSeoMeta({
  title: () => produit.value?.nom ?? 'Produit',
  description: () => produit.value?.description,
  ogImage: () => produit.value?.image,
})
</script>

<template>
  <p v-if="pending">Chargement...</p>
  <p v-else-if="error">Produit introuvable</p>
  <article v-else>
    <h1>{{ produit.nom }}</h1>
    <p>{{ produit.prix }} €</p>
  </article>
</template>
ts
// server/api/produits/[id].get.ts — endpoint API serveur, colocalisé avec le frontend
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  const produit = await recupererProduitDepuisBDD(id)
  if (!produit) {
    throw createError({ statusCode: 404, statusMessage: 'Produit introuvable' })
  }
  return produit
})
ts
// nuxt.config.ts — bascule entre stratégies de rendu, PAGE PAR PAGE
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },             // SSG : générée à la build, servie en statique
    '/produits/**': { swr: 3600 },        // stale-while-revalidate : cache 1h, régénérée en arrière-plan
    '/tableau-de-bord/**': { ssr: false }, // rendu 100% client (données privées, pas de SEO nécessaire)
    '/admin/**': { index: false },        // exclu de l'indexation moteurs de recherche
  },
})
js
// Piège SSR classique : le code exécuté côté serveur n'a PAS accès à window/document/localStorage
// Mauvais : plante au rendu serveur
// const largeur = window.innerWidth

// Bon : garder ce code pour le client uniquement
import { onMounted, ref } from 'vue'
const largeur = ref(0)
onMounted(() => { largeur.value = window.innerWidth }) // onMounted ne s'exécute JAMAIS côté serveur

Résumé

  • SSR = HTML généré côté serveur puis "hydraté" côté client : meilleur SEO et affichage initial plus rapide.
  • Nuxt 3 fournit routing par fichiers, auto-imports, API serveur intégrée et stratégies de rendu par route.
  • useAsyncData/useFetch évitent le double appel réseau (serveur puis client) lors de l'hydratation.
  • Le code touchant window/document doit être isolé dans onMounted ou un flag process.client/import.meta.client.

Exercices pratiques

1 disponible
1

Mission : un déploiement Nuxt qui plante uniquement en production

Objectif : Diagnostiquer un accès direct à window dans un composant SSR, puis choisir une stratégie de rendu adaptée pour chaque page de l'application.

Contexte

Une page pages/produits/[id].vue fonctionne parfaitement en développement, mais le build de production Nuxt échoue avec une erreur "window is not defined". Le composant contient, directement au premier niveau de <script setup> (pas dans onMounted), la ligne const largeurEcran = window.innerWidth utilisée pour adapter la mise en page. Par ailleurs, l'équipe hésite encore sur la stratégie de rendu à appliquer à trois pages : la page d'accueil (contenu quasi statique), les fiches produits (contenu qui change souvent, doit être indexé par Google), et le tableau de bord privé de l'utilisateur connecté.

Résoudre l’exercice →