Skip to Content

Chapitre 10.4 — Statique & SPA

⏱️ TL;DR — C’est le cas le plus simple et le plus robuste de tout le cours : un site 100 % statique ou une SPA (React/Vue/Svelte buildés) n’a rien à faire tourner. Pas de Node, pas de PHP-FPM, pas de process qui vit. Tu builds (npm run build → un dossier dist/ ou out/), tu copies le résultat sur le VPS (rsync), et Nginx sert les fichiers directement depuis le disque. Deux subtilités seulement : le fallback SPA (try_files $uri $uri/ /index.html) pour que le routing côté client marche au rafraîchissement, et le cache — agressif et immutable sur les assets hashés, nul sur index.html. Résultat : rapide, quasi increvable, zéro RAM consommée par une app. Quand tu peux servir en statique, fais-le.

🎯 Objectifs

  • Distinguer site statique et SPA, et comprendre pourquoi aucun des deux n’a besoin d’un process qui tourne.
  • Builder et déployer le résultat sur le VPS avec rsync.
  • Écrire le server block Nginx avec le bon fallback SPA.
  • Régler le cache : immutable sur les assets hashés, jamais sur index.html.
  • Savoir exporter en statique un projet Next, Astro ou Vite.

Le cas le plus simple : rien à faire tourner

Un site statique, c’est des fichiers .html, .css, .js, des images — déjà prêts. Une SPA (Single Page Application, du React/Vue buildé), c’est pareil du point de vue du serveur : un index.html et un paquet de JS/CSS hashés que le navigateur exécute. Dans les deux cas, le serveur n’a rien à calculer : il envoie des fichiers. C’est exactement ce que Nginx fait le mieux.

Compare aux chapitres précédents : plus de proxy_pass, plus de fastcgi_pass. Nginx tout seul. C’est le déploiement le plus fiable qui soit — il n’y a aucun process applicatif qui puisse crasher, fuir de la mémoire ou tomber au reboot.

💡 Réflexe — Avant de déployer une app Node « juste pour servir du front », demande-toi : ce front a-t-il vraiment besoin d’un serveur qui tourne ? Si c’est un site vitrine, une doc, un blog généré, un dashboard SPA qui tape une API séparée… la réponse est souvent non. Sers-le en statique et économise un service, de la RAM et une source de pannes.

🔒 Sécurité — Un site statique a une surface d’attaque minuscule : pas d’exécution de code côté serveur, donc pas d’injection PHP, pas de faille applicative serveur, pas de dépendances runtime à patcher en urgence. La sécurité se joue côté build (dépendances npm) et côté headers HTTP (CSP, etc.), pas côté process. C’est un argument de poids pour le statique quand il est possible.

1. Builder

Le build transforme ton code source en fichiers prêts à servir. Le dossier de sortie dépend de l’outil : dist/ (Vite, la plupart), build/ (Create React App), out/ (export Next), _site/ ou public/ (générateurs de sites).

# Exemple typique (Vite / Vue / React) npm ci # install reproductible depuis le package-lock npm run build # génère le dossier de sortie, souvent dist/ ls dist/ # index.html + assets/ (JS et CSS hashés)

Les fichiers dans dist/assets/ portent un hash dans leur nom (par ex. index-a1b2c3d4.js). Ce détail est central pour la stratégie de cache, on y revient plus bas.

💡 Réflexe — Builde de préférence en CI (GitHub Actions, Partie 13), pas sur le VPS. Le serveur de prod n’a alors même pas besoin de Node installé : il reçoit un dist/ déjà construit. Moins d’outils sur le serveur = moins de surface d’attaque et de RAM. Pour ce chapitre, on builde en local pour la démonstration.

2. Déployer avec rsync

On copie le contenu du dossier de build vers la racine web du VPS. rsync est l’outil idéal : il ne transfère que ce qui a changé et peut supprimer côté serveur les fichiers qui n’existent plus côté build.

# Copie dist/ vers le VPS. Le / final sur dist/ = "le contenu de", pas le dossier rsync -avz --delete dist/ deploy@formacampus.fr:/var/www/vitrine/

Décorticage des options :

  • -a : mode archive (préserve les dates, récursif).
  • -v : verbeux (montre ce qui passe).
  • -z : compresse pendant le transfert (plus rapide sur le réseau).
  • --delete : supprime sur le serveur les fichiers absents du build local — indispensable pour ne pas laisser traîner d’anciens assets hashés.

⚠️ Piège — Le / final sur dist/. rsync -a dist/ .../vitrine/ copie le contenu de dist/ dans vitrine/. Sans le slash (rsync -a dist .../vitrine/), rsync crée vitrine/dist/ — ton site se retrouve un cran trop profond et Nginx sert du vide. Vérifie toujours le slash final.

3. Le server block Nginx

Voici le server block. Sa pièce maîtresse pour une SPA, c’est le fallback vers index.html.

server { listen 80; server_name vitrine.formacampus.fr; root /var/www/vitrine; index index.html; # Fallback SPA : fichier réel, sinon dossier, sinon index.html location / { try_files $uri $uri/ /index.html; } # Assets hashés : cache long et immutable location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } # index.html : JAMAIS en cache (c'est lui qui pointe les nouveaux assets) location = /index.html { add_header Cache-Control "no-cache, must-revalidate"; } }

Pourquoi le fallback try_files ... /index.html ?

Une SPA gère son routing côté client : quand tu cliques dans l’app, l’URL change (par ex. /tableau-de-bord/factures) sans appel serveur, c’est le JS qui affiche la bonne « page ». Mais si le visiteur rafraîchit sur cette URL, le navigateur demande /tableau-de-bord/factures à Nginx. Or ce fichier n’existe pas sur le disque — il n’y a que index.html.

try_files $uri $uri/ /index.html résout ça : Nginx tente le fichier, puis le dossier, et à défaut renvoie index.html. Le JS de la SPA se recharge, lit l’URL, et affiche la bonne vue. Sans ce fallback, tout rafraîchissement sur une route interne donne un 404.

⚠️ Piège — Oublier le fallback SPA. Symptôme classique : « ça marche quand je navigue dans l’app, mais F5 sur une page interne donne un 404 ». Ce n’est pas un bug de ton app : c’est Nginx qui cherche un fichier qui n’existe pas. La ligne try_files $uri $uri/ /index.html; est la réponse. (Attention : pour un site multi-pages statique — pas une SPA — on préfère try_files $uri $uri/ =404; pour renvoyer un vrai 404 sur les URLs inexistantes.)

4. Le cache : immutable sur les assets, rien sur index.html

C’est la partie qui fait la différence entre un site « qui marche » et un site rapide et fiable en déploiement. La clé, c’est le hash dans le nom des fichiers.

  • Les assets (index-a1b2c3d4.js) ont un nom unique par contenu : si le contenu change, le hash change, donc le nom change. On peut donc les mettre en cache pour un an avec immutable : le navigateur ne les redemandera jamais tant que le nom ne change pas. Zéro requête inutile.
  • index.html, lui, garde toujours le même nom mais change de contenu à chaque déploiement (il pointe les nouveaux noms d’assets). Il ne faut surtout pas le cacher, sinon le visiteur garde un vieux index.html qui référence des assets supprimés → page cassée.
index.html -> no-cache (toujours re-vérifié) assets/app-9f8e7d.js -> immutable (caché 1 an, jamais redemandé) assets/app-3c2b1a.css -> immutable

⚠️ Piège — Mettre index.html en cache long. Après un déploiement, les visiteurs qui ont l’ancien index.html en cache chargent des assets qui n’existent plus (tu les as supprimés avec --delete) : page blanche, erreurs 404 sur le JS. La règle est inversée entre les deux : index.html jamais caché, assets hashés très longtemps cachés. C’est contre-intuitif mais c’est exactement l’inverse du réflexe naïf « je cache tout ».

5. Exporter en statique : Next, Astro, Vite

Beaucoup de frameworks « à serveur » peuvent produire un export statique quand ton site n’a pas besoin de rendu serveur à la volée :

FrameworkCommande / réglageSortie
Vite (Vue/React/Svelte)npm run builddist/
Astronpm run build (sortie statique par défaut)dist/
Next.jsoutput: 'export' puis next buildout/
Create React Appnpm run buildbuild/

Pour Next.js, l’export statique convient si tu n’utilises pas les fonctionnalités serveur (rendu à la requête, routes API, getServerSideProps…). Si tu en as besoin, Next doit tourner comme un process — et là on retombe sur le déploiement Node de la Partie 9, pas sur ce chapitre.

💡 Réflexe — Pose-toi la question au niveau du projet : « ai-je besoin de code serveur à la requête ? ». Si non → export statique + Nginx (ce chapitre), le plus robuste. Si oui → un process qui tourne + reverse proxy (Parties 9 et 10.5). Beaucoup de sites « Next » n’utilisent aucune fonctionnalité serveur et gagneraient à être exportés en statique.

🧭 Sur FormaCampus — La documentation publique et la landing page marketing de FormaCampus sont des sites statiques (générés avec Astro), servis par Nginx sans aucun process sur docs.formacampus.fr et la racine du site vitrine. Déploiement : la CI builde, rsync --delete pousse le dist/ sur le VPS, Nginx sert avec cache immutable sur les assets hashés et no-cache sur les index.html. Ces sous-domaines n’ont jamais d’incident applicatif — il n’y a pas d’app à tomber. C’est le contraste avec l’API Symfony (10.2), qui, elle, doit tourner et être surveillée. Le front applicatif apprenant, en revanche, est une app Next qui tourne (Partie 9) car il a besoin de rendu serveur et d’auth.

📚 La doc — Pour le cache, la référence est la doc HTTP sur Cache-Control (MDN) et le module ngx_http_headers_module de Nginx pour add_header/expires. Pour l’export statique, la doc de ton framework (« Static Exports » chez Next, « Deploy » chez Astro/Vite) donne le réglage exact et à jour.

✏️ Exercices

Exercice 1 — Le F5 qui casse. Une SPA React déployée fonctionne quand on navigue via les liens, mais rafraîchir sur /profil renvoie un 404 Nginx. Quelle ligne manque au server block et pourquoi corrige-t-elle le problème ?

✅ Solution

Il manque le fallback SPA : try_files $uri $uri/ /index.html; dans location /. Au rafraîchissement sur /profil, le navigateur demande ce chemin à Nginx, qui n’a aucun fichier /profil sur le disque (seulement index.html). Le fallback renvoie index.html ; le JS de la SPA se recharge, lit l’URL et affiche la vue /profil. Sans lui, Nginx renvoie un 404 puisqu’il cherche un fichier inexistant.

Exercice 2 — La stratégie de cache. Explique pourquoi on cache assets/app-9f8e7d.js pendant un an en immutable mais jamais index.html. Que se passerait-il si on inversait ?

✅ Solution

Les assets portent un hash de contenu dans leur nom : un changement de contenu = un nouveau nom, donc l’ancien nom peut être caché indéfiniment sans risque (immutable). index.html garde toujours le même nom mais change à chaque déploiement (il pointe les nouveaux noms d’assets), donc on ne le cache pas. Si on inversait : un visiteur garderait un vieux index.html caché qui référence des assets supprimés par le --delete → 404 sur le JS/CSS, page cassée. La règle est volontairement inversée entre les deux.

Exercice 3 — Faut-il un process ? Pour chacun, statique (Nginx seul) ou app qui tourne (proxy) ? (a) Un dashboard React qui consomme une API REST séparée. (b) Un site Next.js avec des routes API et du rendu serveur par requête. (c) Une doc générée avec Astro.

✅ Solution

(a) Statique : le dashboard React est buildé en fichiers ; il tape l’API depuis le navigateur. Nginx sert le dist/, aucun process front nécessaire. (b) App qui tourne : les routes API et le rendu serveur par requête exigent que Next s’exécute — process + reverse proxy (Partie 9). (c) Statique : Astro génère des fichiers ; Nginx les sert. On ne déploie un process que quand il y a réellement du code serveur à la requête.

🧠 Quiz de révision

1. Pourquoi un site statique ou une SPA n’a-t-il besoin d’aucun process qui tourne sur le serveur ?

Parce que les fichiers sont déjà prêts après le build (.html, .js, .css, images). Le serveur n’a rien à calculer à la requête : il sert des fichiers, ce que Nginx fait seul. Pas de Node ni de PHP-FPM à faire tourner.

2. Que fait try_files $uri $uri/ /index.html et pour quel type d’appli ?

Pour une SPA : Nginx tente le fichier, puis le dossier, et à défaut renvoie index.html, pour que le routing côté client fonctionne même après un rafraîchissement sur une route interne. Sans lui, F5 sur /profil donne un 404.

3. Quelle option de rsync supprime côté serveur les fichiers absents du build, et pourquoi la veut-on ?

--delete. On la veut pour ne pas laisser traîner d’anciens assets hashés qui ne sont plus référencés : le serveur reflète exactement le dernier build, sans accumuler de fichiers obsolètes.

4. Pourquoi cache-t-on les assets hashés en immutable mais pas index.html ?

Les assets ont un hash dans leur nom : nouveau contenu = nouveau nom, donc l’ancien peut être caché un an sans risque. index.html garde le même nom mais change à chaque déploiement (il pointe les nouveaux assets) : le cacher servirait un HTML périmé référençant des fichiers supprimés → page cassée.

5. Quand un projet Next.js peut-il être exporté en statique, et quand doit-il tourner comme un process ?

Il peut être exporté (output: 'export'out/) s’il n’utilise aucune fonctionnalité serveur à la requête (routes API, rendu serveur par requête, getServerSideProps). S’il en a besoin, Next doit tourner comme un process derrière un reverse proxy (Partie 9).


Chapitre suivant : Python & le pattern génériquegunicorn/uvicorn derrière Nginx, et le modèle qui unifie toutes les stacks.

Last updated on