Une application monopage (SPA) repose sur un routeur côté client qui intercepte les changements d'URL et met à jour le DOM sans rechargement. Mais Nginx, par défaut, traite chaque requête HTTP comme une demande de fichier statique. Quand l'utilisateur visite directement /dashboard/settings ou recharge la page, le serveur cherche un fichier physique à ce chemin, ne le trouve pas, et renvoie une erreur 404. C'est le piège classique : votre SPA fonctionne parfaitement sur localhost , mais casse dès le déploiement en production.
Le problème : Nginx cherche des fichiers, pas des routes
La différence entre un serveur traditionnel et une SPA est fondamentale. Apache ou Nginx sont habitués à mapper les requêtes HTTP à des fichiers physiques : /api/users → /var/www/api/users.php , /images/logo.png → /var/www/images/logo.png . Avec une SPA, il n'existe qu'un seul fichier HTML — généralement index.html — qui contient votre application compilée (bundle JavaScript). Toutes les routes internes sont gérées par le routeur JavaScript du navigateur, pas par le serveur. Quand Nginx reçoit une requête pour /products/123 , il cherche un dossier ou fichier products/123 sur le disque, ne le trouve pas, et retourne 404. Pire encore, si l'utilisateur actualise la page, il perd complètement l'accès à son contenu, créant une expérience frustrante et dommageable pour le SEO (bien que les crawlers modernes exécutent JavaScript, la latence reste pénalisante).
La solution : try_files avec fallback sur index.html
La configuration Nginx standard pour servir une SPA utilise la directive try_files . Elle teste l'existence d'une ressource dans cet ordre : d'abord le fichier demandé, puis le dossier, puis enfin un fichier par défaut. Voici la configuration minimale fonctionnelle :
server {
listen 80;
server_name example.com;
root /var/www/dist;
location / {
try_files $uri $uri/ /index.html;
}
} Cette directive signifie : « Si le fichier $uri existe, le servir. Sinon, chercher le dossier $uri/ . Sinon, servir /index.html sans modifier l'URL du navigateur ». Cette dernière étape est cruciale. Sans elle, le navigateur verrait l'URL changer, ce qui casse la navigation SPA. Avec try_files , l'URL reste /products/123 , mais le serveur envoie silencieusement le contenu de index.html . Votre routeur Angular, React ou Vue reçoit cet HTML, charge le JavaScript, et reconstruit l'interface correcte basée sur l'URL actuelle.
Cas d'usage réel : Angular avec Ionic
Considérez une application Ionic/Angular avec ces routes : /home , /products , /products/:id , /settings . Après ng build --prod ou ionic build --prod , vous obtenez un dossier dist/ contenant index.html , des fichiers CSS et JS compilés, et des assets. Avec la configuration try_files ci-dessus, tous les scénarios fonctionnent : visiter /products directement, recharger la page, partager un lien /products/42 , ou naviguer via le routeur Angular. Aucun 404. Chaque requête revient à index.html , qui charge votre bundle JS, qui parse l'URL, initialise les composants appropriés et affiche le contenu.
Pièges courants et solutions
Un piège très fréquent : oublier d'exclure les routes API réelles du fallback sur index.html . Si votre backend tourne sur le même serveur ou est routé par Nginx (par exemple /api/ pointe vers un upstream), vous devez explicitement exclure ces chemins. Par exemple :
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:3000;
} Autre piège : servir des fichiers statiques versionnés (comme main.abc123def.js ) avec un long cache, puis redéployer sans mettre à jour le hash. Nginx lui-même n'en est pas responsable, mais configurez des en-têtes de cache appropriés pour que les navigateurs ne restent pas bloqués sur une ancienne version. Ajouter Cache-Control: no-cache sur index.html et Cache-Control: immutable, max-age=31536000 sur les fichiers versionnés résout ce problème.
Optimisations pratiques pour la production
En production, quelques ajustements simples améliorent la robustesse. Compressez les fichiers statiques avec gzip pour réduire la bande passante : gzip on; gzip_types text/plain text/css application/json application/javascript; . Configurez un cache agressif sur les fichiers de contenu statique versionnés et un cache minimal ou nul sur index.html . Utilisez add_header Cache-Control "public, max-age=31536000, immutable"; dans un bloc location ~ pour les assets, et add_header Cache-Control "no-cache, no-store, must-revalidate"; pour index.html . Vous pouvez aussi ajouter des en-têtes de sécurité comme X-Frame-Options: SAMEORIGIN et X-Content-Type-Options: nosniff pour prévenir certaines attaques.
Conclusion opérationnelle
Servir une SPA sans erreurs 404 sur Nginx ne demande qu'une ligne de configuration intelligente : try_files $uri $uri/ /index.html; . Cette directive simple redirigeant silencieusement toutes les routes inconnues vers votre HTML principal libère votre routeur côté client pour fonctionner librement. Déployez cette configuration, testez le rechargement de pages sur différentes routes, et vérifiez que vos appels API restent intacts. C'est une des modifications les plus impactantes et les moins invasives pour stabiliser une SPA en production. Une fois en place, vous pouvez vous concentrer sur l'optimisation réelle : compression, cache, et performance du bundle JavaScript.