Routage avancé Next.js : groupes de routes, layouts imbriqués, routes parallèles et routes interceptées

La semaine dernière, j’ai repris un projet e-commerce Next.js en production depuis deux ans. En ouvrant le dossier app, l’écran s’est rempli de plus de 60 dossiers côte à côte — about, products, admin-users, marketing-campaign, shop-cart… tout à plat. Vous cherchez une page liée aux utilisateurs ? Il faut la dénicher parmi des dizaines de pages marketing. Pire encore : trois personnes modifiaient les fichiers de routes en parallèle, avec au moins deux conflits Git par jour ; en code review, il fallait une demi-heure rien que pour comprendre les relations entre fichiers.
Assis devant cet enchevêtrement de dossiers, je me suis souvenu que Next.js propose des groupes de routes, des routes parallèles et d’autres fonctionnalités avancées. En consultant la doc officielle, j’ai découvert qu’elles existaient déjà depuis Next.js 13 — mais le projet ne les utilisait pas du tout. En clair : ce n’était pas un manque d’outils, mais l’absence de critères pour savoir quand utiliser quoi.
Ce soir-là, j’ai passé trois heures à étudier ces fonctionnalités, puis deux jours à refactoriser la structure. Résultat : une arborescence nette, plus personne ne criait « encore un conflit » dans le canal d’équipe. Surtout, j’ai enfin compris à quoi servent ces quatre concepts un peu déroutants : groupes de routes pour organiser les dossiers, layouts imbriqués pour réutiliser la structure, routes parallèles pour afficher plusieurs pages en même temps, routes interceptées pour des modales élégantes.
Si votre projet Next.js grossit, que les fichiers s’empilent et que la collaboration génère des conflits — après cette lecture, vous saurez quand utiliser chaque fonctionnalité, comment l’implémenter et quels pièges éviter.
Groupes de routes (Route Groups) — une arborescence bien ordonnée
Qu’est-ce qu’un groupe de routes ?
En bref : un dossier entre parenthèses, comme (marketing) ou (shop). Next.js n’ajoute pas ce nom à l’URL.
Peu utile en théorie ? Attendez d’avoir un vrai cas d’usage.
Imaginons un site e-commerce avec des pages marketing (accueil, à propos), des pages boutique (liste produits, panier) et un back-office (commandes, utilisateurs). L’approche classique : tout empiler à la racine de app, ou forcer des préfixes /marketing, /shop, /admin dans l’URL — qui voudrait visiter yoursite.com/marketing/about ?
Les groupes de routes résolvent ce dilemme : organiser les fichiers par zone sans modifier l’URL visible.
Trois usages concrets
Usage 1 : organiser par équipe ou fonction
Le bénéfice le plus direct. Transformer 60 dossiers plats en trois groupes :
app/
├── (marketing)/ # Équipe marketing
│ ├── page.js # Accueil → yoursite.com/
│ ├── about/ # À propos → yoursite.com/about
│ └── pricing/ # Tarifs → yoursite.com/pricing
├── (shop)/ # Équipe frontend
│ ├── products/ # Produits → yoursite.com/products
│ └── cart/ # Panier → yoursite.com/cart
└── (dashboard)/ # Équipe backend
├── orders/ # Commandes → yoursite.com/orders
└── users/ # Utilisateurs → yoursite.com/users
L’URL reste propre, la structure de fichiers est limpide. Un nouveau stagiaire comprend immédiatement quel dossier gère quoi.
Usage 2 : layouts racine différents par zone
C’est la force des groupes de routes. La barre de navigation marketing et celle du back-office peuvent-elles être identiques ? Non. Pourtant /about et /orders partent tous deux de la racine — comment leur appliquer des layouts différents ?
Réponse : chaque groupe peut avoir son propre layout.js.
app/
├── (marketing)/
│ ├── layout.js # Layout marketing : nav + grand visuel
│ └── ...
├── (shop)/
│ ├── layout.js # Layout boutique : icône panier + filtres
│ └── ...
└── (dashboard)/
├── layout.js # Layout back-office : sidebar + contrôle d'accès
└── ...
Trois layouts, sans interférence. Hero marketing, sidebar admin, compteur panier fixe — le tout dans un seul projet, sans sous-domaines ni instances Next.js multiples.
Usage 3 : partage sélectif de layouts
Parfois, « certaines pages partagent un layout, d’autres non ». Tous les articles de blog ont une navigation latérale, pas la page d’accueil du blog. Les groupes de routes le permettent facilement :
app/
├── blog/
│ ├── page.js # Accueil blog, sans nav latérale
│ └── (articles)/ # Groupe articles, nav latérale partagée
│ ├── layout.js # Layout avec nav latérale
│ ├── [slug]/ # Détail article → /blog/xxx
│ └── ...
(articles) n’affecte pas l’URL — le chemin reste /blog/my-first-post, mais seules les pages de ce groupe appliquent le layout avec navigation.
Cas réel : de la confusion à la clarté
Retour au projet e-commerce avec 60 dossiers. Avant/après refactorisation :
Avant (extrait) :
app/
├── page.js
├── about/
├── pricing/
├── products/
├── products-detail/
├── cart/
├── checkout/
├── admin-orders/
├── admin-users/
├── admin-settings/
├── marketing-campaign/
├── ...(50 de plus)
Chercher un fichier ? Ctrl+F. Savoir à quel module appartient une page ? Deviner d’après le nom.
Après :
app/
├── (marketing)/
│ ├── layout.js
│ ├── page.js
│ ├── about/
│ ├── pricing/
│ └── campaign/
├── (shop)/
│ ├── layout.js
│ ├── products/
│ ├── cart/
│ └── checkout/
└── (dashboard)/
├── layout.js
├── orders/
├── users/
└── settings/
Trois niveaux, limpides. Modifier le marketing ? (marketing). Ajouter une fonction admin ? (dashboard). En code review, chaque équipe ne regarde que son groupe — taux de conflits en baisse de 70 %.
Trois pièges à éviter
Piège 1 : conflit d’URL = erreur immédiate
Les groupes n’affectent pas l’URL. Que se passe-t-il si deux groupes ont la même route ?
app/
├── (marketing)/
│ └── about/page.js # → /about
└── (shop)/
└── about/page.js # → /about(conflit !)
Next.js renvoie : Error: Conflicting route. Solution : renommer le chemin ou ajouter un vrai segment (sans parenthèses) :
app/
├── (marketing)/
│ └── about/page.js # → /about
└── (shop)/
└── shop-info/page.js # → /shop-info(renommé)
Piège 2 : plusieurs layouts racine déclenchent un rechargement complet
En naviguant de (shop) vers (marketing), la page « clignote » — ce n’est pas un bug, c’est voulu.
Les layouts racine de groupes différents sont indépendants ; Next.js doit démonter l’ancien et monter le nouveau, ce qui implique un rechargement complet (full page load). C’est intentionnel : les layouts peuvent être totalement incompatibles.
Pour une transition fluide, évitez plusieurs layouts racine — remontez les parties communes dans app/layout.js, et ne mettez dans chaque groupe que les différences.
Piège 3 : emplacement de la page d’accueil avec plusieurs layouts racine
Si plusieurs groupes ont chacun un layout.js, la page d’accueil page.js doit être dans l’un d’eux, pas dans app/page.js. Sinon Next.js ne sait pas quel layout racine appliquer.
En pratique, placez l’accueil dans (marketing)/page.js.
En résumé : organiser les fichiers sans toucher à l’URL, et appliquer des layouts différents par zone. Inutile sur un petit projet ; dès que app dépasse une vingtaine de dossiers, testez les groupes de routes.
Layouts imbriqués (Nested Layouts) — réutiliser la structure des pages
Quel problème résolvent-ils ?
Cas fréquent : l’accueil a une nav en haut, la liste du blog nav + barre latérale de catégories, l’article nav + barre latérale + table des matières à droite.
Avec des composants classiques, vous assemblez tout manuellement sur chaque page. Changer le style de la nav ? Trois endroits à modifier.
Les layouts imbriqués Next.js résolvent cela — des layouts comme des poupées russes, le layout parent s’applique automatiquement à toutes les pages enfants. À chaque niveau, on ajoute des éléments UI sans réécrire les couches extérieures.
Comment ça fonctionne ?
Chaque dossier peut avoir son layout.js ; les sous-dossiers héritent du parent et ajoutent leur propre couche.
Exemple — plateforme de formation en ligne :
app/
├── layout.js # Racine : nav + Footer
└── courses/
├── layout.js # Cours : racine + catégories à gauche
├── page.js # Liste des cours
└── [id]/
├── layout.js # Détail : cours + barre de progression à droite
└── page.js # Cours spécifique
Pour /courses/123, l’ordre de rendu :
- Couche externe :
app/layout.js(nav + Footer) - Couche intermédiaire :
courses/layout.js(barre latérale catégories) - Couche interne :
courses/[id]/layout.js(barre de progression) - Contenu :
courses/[id]/page.js
Modifier la nav du haut ? Un seul fichier : app/layout.js.
Cas pratique : blog en trois niveaux
J’avais un blog technique avec ces besoins :
- Toutes les pages : nav (accueil, à propos, contact) + pied de page
- Pages blog : nav + filtre par catégorie à gauche
- Article : nav + catégories + ancres à droite
Avec des layouts imbriqués, c’est naturel :
app/
├── layout.js # Niveau 1 : layout global
│ └── <Header /><Footer />
└── blog/
├── layout.js # Niveau 2 : layout blog
│ └── <Sidebar />
├── page.js # Liste blog (hérite des deux niveaux)
└── [slug]/
├── layout.js # Niveau 3 : layout article
│ └── <TableOfContents />
└── page.js # Détail (hérite des trois niveaux)
Code simplifié :
app/layout.js (niveau 1)
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children} {/* Rendu du layout ou page enfant */}
<Footer />
</body>
</html>
)
}
app/blog/layout.js (niveau 2)
export default function BlogLayout({ children }) {
return (
<div className="blog-container">
<Sidebar />
<main>{children}</main> {/* Contenu plus interne */}
</div>
)
}
app/blog/[slug]/layout.js (niveau 3)
export default function ArticleLayout({ children }) {
return (
<div className="article-container">
{children}
<TableOfContents /> {/* Table des matières à droite */}
</div>
)
}
Chaque layout ne gère que son UI — Next.js les imbrique automatiquement.
Combiné avec les groupes de routes
Les groupes isolent horizontalement (marketing, boutique, back-office) ; les layouts s’empilent verticalement.
Pour la zone boutique :
app/
└── (shop)/
├── layout.js # Racine boutique : panier + nav catégories
├── products/
│ ├── layout.js # Liste : + filtres à gauche
│ ├── page.js # Page liste
│ └── [id]/
│ ├── layout.js # Détail : + fil d'Ariane
│ └── page.js # Page détail
└── cart/
└── page.js # Panier (hérite seulement du layout racine boutique)
- Liste
/products: layout boutique + filtres - Détail
/products/123: boutique + filtres + fil d’Ariane - Panier
/cart: layout boutique uniquement
Quelle UI pour quelle page ? Déterminée par la profondeur des dossiers, sans conditions du type « si détail, afficher le fil d’Ariane ».
Deux détails importants
Détail 1 : le layout ne se re-rend pas (performances)
De /blog à /blog/my-post, app/layout.js et app/blog/layout.js ne se re-rendent pas — état et scroll conservés ; seul le page.js interne se recharge.
Vous pouvez garder le scroll de la sidebar ou le contenu d’un champ de recherche dans le layout : tout reste intact entre les pages internes.
Détail 2 : le layout n’accède pas aux paramètres de route enfant
Pour app/products/[id]/page.js, le paramètre [id] n’est disponible que dans page.js via params, pas dans layout.js.
Si le layout doit afficher le titre du produit selon [id], passez les données via Context ou remontez-les.
L’essentiel : aligner l’organisation du code sur la hiérarchie visuelle de l’UI. Modifier un layout impacte toute la zone — sans copier-coller de composants.
Routes parallèles (Parallel Routes) — afficher plusieurs pages simultanément
Quand en a-t-on besoin ?
Scénario concret : tableau de bord admin avec trois modules :
- En haut à gauche : graphique des ventes
- En haut à droite : dernières commandes
- En bas : alertes stock
Données indépendantes, vitesses de chargement différentes. Les stats peuvent être lentes, les commandes instantanées, le stock vient d’une autre API.
Approche classique : trois appels API dans dashboard/page.js, trois composants. Une erreur fait tout planter. Un état de chargement par module ? Beaucoup de state à gérer.
Les routes parallèles permettent de rendre plusieurs fragments de page indépendants (slots officiels) sur une même page, chacun avec son loading, sa gestion d’erreur, voire sa navigation.
Syntaxe : le symbole @ pour les slots
Un dossier @folder devient un slot.
app/
└── dashboard/
├── layout.js # Reçoit trois slots en props
├── @sales/ # Slot 1 : ventes
│ └── page.js
├── @orders/ # Slot 2 : commandes
│ └── page.js
├── @inventory/ # Slot 3 : stock
│ └── page.js
└── page.js # Contenu principal (optionnel)
@sales, @orders, @inventory — un dossier commençant par @ est un slot de route parallèle.
Dans layout.js, Next.js passe ces slots en props :
export default function DashboardLayout({
children, // Contenu de page.js
sales, // @sales/page.js
orders, // @orders/page.js
inventory // @inventory/page.js
}) {
return (
<div className="dashboard">
<div className="widgets">
<div className="widget">{sales}</div>
<div className="widget">{orders}</div>
</div>
<div className="main">{children}</div>
<div className="alerts">{inventory}</div>
</div>
)
}
Trois slots comme trois sous-pages indépendantes, disposées librement dans le layout.
Cas pratique : tableau de bord admin
@sales/page.js (statistiques ventes)
async function getSalesData() {
const res = await fetch('https://api.example.com/sales')
return res.json()
}
export default async function SalesWidget() {
const data = await getSalesData() // Peut être lent
return (
<div>
<h3>Ventes du mois</h3>
<Chart data={data} />
</div>
)
}
@orders/page.js (liste commandes)
async function getRecentOrders() {
const res = await fetch('https://api.example.com/orders')
return res.json()
}
export default async function OrdersWidget() {
const orders = await getRecentOrders() // Peut être rapide
return (
<div>
<h3>Commandes récentes</h3>
<ul>
{orders.map(order => <li key={order.id}>{order.title}</li>)}
</ul>
</div>
)
}
@inventory/page.js (alertes stock)
export default function InventoryWidget() {
// Peut aussi être un composant client avec useEffect
return (
<div>
<h3>Alerte stock</h3>
<p>5 produits en rupture de stock</p>
</div>
)
}
Les trois slots se chargent en parallèle. Commandes rapides d’abord ; stats plus tard ; une erreur n’affecte qu’un slot.
loading.js et error.js indépendants
Chaque slot peut avoir son propre loading.js et error.js :
app/
└── dashboard/
├── @sales/
│ ├── page.js
│ ├── loading.js # Chargement module ventes
│ └── error.js # Erreurs module ventes
├── @orders/
│ ├── page.js
│ └── loading.js # Chargement module commandes
└── @inventory/
└── page.js
@sales/loading.js :
export default function SalesLoading() {
return <div>Chargement des données de ventes...</div>
}
@sales/error.js :
'use client'
export default function SalesError({ error, reset }) {
return (
<div>
<p>Échec du chargement des données de ventes</p>
<button onClick={reset}>Réessayer</button>
</div>
)
}
Effet : à l’ouverture, ventes en « chargement… », commandes peut-être déjà affichées, stock immédiat. Si l’API ventes échoue, seul ce module affiche l’erreur.
L’utilisateur ne fixe plus un écran blanc — il voit ce qui est prêt.
Point clé : le rôle de default.js
Sur /dashboard, puis navigation vers /dashboard/settings : les slots @sales, @orders ne correspondent plus. Que rendre ?
Par défaut, le slot conserve son contenu (mise à jour partielle type SPA). Pour le vider au changement de route, ajoutez default.js :
app/
└── dashboard/
├── @sales/
│ ├── page.js
│ └── default.js # Retourne null si pas de correspondance
└── ...
@sales/default.js :
export default function SalesDefault() {
return null // Rien à afficher si pas de correspondance
}
Sur /dashboard/settings, @sales rend default.js (rien).
Les routes parallèles servent surtout avec les routes interceptées pour les modales — voir chapitre suivant. Seules, elles excellent pour les tableaux de bord multi-modules.
Routes interceptées (Intercepting Routes) — modales élégantes
L’expérience Instagram pour les images
Sur Instagram ou des apps similaires : clic sur une image dans le fil → modale, URL /photo/abc123. Et :
- Retour arrière : modale fermée, fil intact (pas une page différente)
- Rafraîchissement : page détail complète, pas de modale
- Lien partagé : page complète pour le destinataire
Implémentation classique : état, parsing d’URL, manipulation de l’historique… pénible.
Les routes interceptées interceptent la navigation client pour afficher le contenu en modale sur la page courante ; accès direct ou rafraîchissement → page complète.
Syntaxe : les symboles (..)
(.)— route au même niveau(..)— route au niveau parent(..)(..)— deux niveaux au-dessus(...)— route depuis la racine
Exemple : liste /photos, clic vers /photos/123.
- Clic : modale sur la liste
- Accès direct ou rafraîchissement : page détail complète
app/
├── @modal/
│ ├── (.)photos/ # Intercepte photos au même niveau
│ │ └── [id]/
│ │ └── page.js # Contenu modale
│ └── default.js # Retourne null si pas de correspondance
├── layout.js # Reçoit le slot modal
├── page.js # Fil d'accueil
└── photos/
└── [id]/
└── page.js # Page détail complète
@modal/(.)photos/ signifie : intercepter photos au même niveau que @modal sous app/, d’où (.).
Cas pratique : modale style Instagram
- Grille d’images sur l’accueil
- Clic → modale, URL
/photos/123 - Rafraîchissement ou accès direct → page complète
- Retour arrière ferme la modale
Étape 1 : structure
app/
├── @modal/
│ ├── (.)photos/
│ │ └── [id]/
│ │ └── page.js # Composant modale
│ └── default.js
├── layout.js
├── page.js # Grille d'images
└── photos/
└── [id]/
└── page.js # Page image complète
Étape 2 : layout racine avec slot modal
app/layout.js :
export default function RootLayout({ children, modal }) {
return (
<html>
<body>
{children} {/* Contenu principal */}
{modal} {/* Slot modale */}
</body>
</html>
)
}
Étape 3 : grille sur l’accueil
app/page.js :
import Link from 'next/link'
const photos = [
{ id: '1', url: '/images/photo1.jpg' },
{ id: '2', url: '/images/photo2.jpg' },
// ...
]
export default function HomePage() {
return (
<div className="photo-grid">
{photos.map(photo => (
<Link key={photo.id} href={`/photos/${photo.id}`}>
<img src={photo.url} alt="" />
</Link>
))}
</div>
)
}
Étape 4 : route interceptée — composant modale
app/@modal/(.)photos/[id]/page.js :
'use client'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
export default function PhotoModal({ params }) {
const router = useRouter()
return (
<div className="modal-backdrop" onClick={() => router.back()}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<button onClick={() => router.back()}>Fermer</button>
<Image src={`/images/photo${params.id}.jpg`} fill />
</div>
</div>
)
}
router.back() ferme la modale — navigation client, retour au fil.
Étape 5 : page complète
app/photos/[id]/page.js :
import Image from 'next/image'
export default function PhotoPage({ params }) {
return (
<div className="photo-page">
<nav>Retour à l'accueil</nav>
<h1>Détail de l'image</h1>
<Image src={`/images/photo${params.id}.jpg`} width={800} height={600} />
<p>Description de l'image...</p>
</div>
)
}
Étape 6 : default.js pour fermer la modale
app/@modal/default.js :
export default function Default() {
return null // Pas de modale si route non correspondante
}
Effet obtenu
-
Clic sur l’accueil :
- Link navigue vers
/photos/1côté client - Next.js intercepte via
@modal/(.)photos/[id] - Modale superposée au fil, URL
/photos/1, pas de rechargement complet
- Link navigue vers
-
Retour arrière :
router.back()vers/- Slot
@modalrenddefault.js(null) - Modale fermée, fil intact
-
Rafraîchissement ou accès direct
/photos/1:- Pas d’interception (pas navigation client)
- Rendu de
app/photos/[id]/page.jscomplet
-
Lien partagé :
- Destinataire voit la page complète
URL partageable, retour ferme la modale, rafraîchissement affiche la page — les trois exigences sont couvertes.
Choisir le niveau d’interception
Position de l’interception vs route cible, sous app/@modal/ :
- Cible
app/photos/(même niveau) →(.)photos - Cible
app/shop/products/(enfant du parent) →(..) - Cible profonde quelconque →
(...)depuis la racine
Exemple :
app/
└── shop/
├── @modal/
│ └── (..)products/ # Intercepte products au niveau parent
│ └── [id]/
└── products/
└── [id]/
@modal est sous shop/, products aussi — (..) pour remonter d’un niveau.
Conseil : commencez par (...) ; affinez ensuite en (.) ou (..).
Trois pièges
Piège 1 : oublier default.js — modale qui ne se ferme pas
Sans default.js, le slot garde le contenu précédent. Ajoutez default.js retournant null.
Piège 2 : useRouter dans un Server Component
La modale utilise souvent useRouter().back() → Client Component. N’oubliez pas 'use client'.
Piège 3 : interception qui échoue avec imbrication profonde
Structure profonde (app/shop/(store)/products/[id]) : calculez bien le chemin. En cas de doute, (...) depuis la racine — moins élégant, mais sûr.
Routes interceptées + routes parallèles : URL partageable, page complète au rafraîchissement, retour ferme la modale, avancer la rouvre. Instagram, Twitter, Airbnb — vous pouvez faire pareil avec Next.js.
Mise en pratique — combiner les quatre techniques
Scénario réel : structure complète d’une plateforme e-commerce
Pris séparément, ces quatre fonctionnalités semblent utiles ; combinées, elles déploient tout leur potentiel. Voyons une plateforme e-commerce moyenne.
Analyse des besoins
Trois zones fonctionnelles (layouts distincts) :
- Marketing (
/,/about,/pricing) : grand visuel + nav épurée - Boutique (
/products,/cart) : icône panier fixe + nav catégories - Back-office (
/dashboard) : sidebar + contrôle d’accès
Besoins zone boutique :
- Liste : filtres à gauche
- Détail : fil d’Ariane
- Clic carte produit : aperçu modale sans quitter la liste
- Rafraîchissement ou URL directe : page détail complète
Tableau de bord :
- Trois modules indépendants : ventes, commandes, stock
- Chacun avec son loading et sa gestion d’erreur
Structure de dossiers complète
app/
├── layout.js # Layout racine global
│
├── (marketing)/ # Groupe : marketing
│ ├── layout.js # Layout marketing
│ ├── page.js # Accueil → /
│ ├── about/ # À propos → /about
│ └── pricing/ # Tarifs → /pricing
│
├── (shop)/ # Groupe : boutique
│ ├── layout.js # Layout boutique
│ ├── @modal/ # Route parallèle : slot modale
│ │ ├── (.)products/ # Route interceptée : aperçu produit
│ │ │ └── [id]/
│ │ │ └── page.js # Composant modale
│ │ └── default.js
│ │
│ ├── products/
│ │ ├── layout.js # Layout imbriqué : filtres
│ │ ├── page.js # Liste → /products
│ │ └── [id]/
│ │ ├── layout.js # Layout imbriqué : fil d'Ariane
│ │ └── page.js # Détail → /products/123
│ │
│ └── cart/
│ └── page.js # Panier → /cart
│
└── (dashboard)/ # Groupe : back-office
├── layout.js # Layout sidebar
├── @sales/ # Route parallèle : ventes
│ ├── page.js
│ └── loading.js
├── @orders/ # Route parallèle : commandes
│ ├── page.js
│ └── loading.js
├── @inventory/ # Route parallèle : stock
│ └── page.js
└── page.js # Tableau de bord → /dashboard
- Groupes de routes : trois zones isolées
- Layouts imbriqués : UI progressive en boutique
- Routes parallèles : modale boutique + modules back-office
- Routes interceptées : aperçu rapide produit
Implémentation clé
1. Layout boutique avec slot modale
app/(shop)/layout.js :
export default function ShopLayout({ children, modal }) {
return (
<div>
<nav>{/* Icône panier + nav catégories */}</nav>
{children}
{modal} {/* Modale superposée ici */}
</div>
)
}
2. Layout imbriqué liste produits
app/(shop)/products/layout.js :
export default function ProductsLayout({ children }) {
return (
<div className="products-container">
<aside>{/* Filtres à gauche */}</aside>
<main>{children}</main>
</div>
)
}
app/(shop)/products/page.js (liste) :
import Link from 'next/link'
export default function ProductsPage() {
return (
<div className="product-grid">
{products.map(p => (
<Link key={p.id} href={`/products/${p.id}`}>
<ProductCard product={p} />
</Link>
))}
</div>
)
}
Clic carte → navigation client → interception → modale.
3. Route interceptée — aperçu rapide
app/(shop)/@modal/(.)products/[id]/page.js :
'use client'
import { useRouter } from 'next/navigation'
export default function ProductModal({ params }) {
const router = useRouter()
return (
<div className="modal-backdrop" onClick={() => router.back()}>
<div className="modal">
<h2>Aperçu rapide du produit</h2>
<ProductPreview id={params.id} />
<Link href={`/products/${params.id}`} onClick={() => router.back()}>
Voir le détail complet
</Link>
</div>
</div>
)
}
4. Routes parallèles du tableau de bord
app/(dashboard)/layout.js :
export default function DashboardLayout({ children, sales, orders, inventory }) {
return (
<div className="dashboard">
<aside>{/* Nav sidebar */}</aside>
<main>
{children}
<div className="widgets">
<div className="widget">{sales}</div>
<div className="widget">{orders}</div>
<div className="widget">{inventory}</div>
</div>
</main>
</div>
)
}
Trois slots en parallèle — les rapides s’affichent d’abord.
Raisons de ces choix
Pourquoi des groupes de routes ?
- Nav marketing, boutique et back-office totalement différentes
- Trois équipes, trois groupes, moins de conflits
Pourquoi des layouts imbriqués ?
- Liste avec filtres, détail avec fil d’Ariane, nav boutique commune
- Hiérarchie UI = hiérarchie des dossiers
Pourquoi interception + parallèle pour la modale ?
- Aperçu rapide sans quitter la liste
- URL
/products/123pour partage et SEO - Rafraîchissement → page complète
Pourquoi routes parallèles au back-office ?
- Sources et vitesses différentes
- Chargement et erreurs isolés par module
Bénéfices concrets en équipe
Après refactorisation :
- Conflits -65 % : frontend sur
(shop), backend sur(dashboard) - Onboarding divisé par deux : l’arborescence parle d’elle-même
- Maintenance simplifiée : modifier la nav d’une zone = un seul
layout.js - Conversion +23 % sur l’aperçu rapide (les utilisateurs évitaient le retour arrière depuis la page détail)
Ces quatre fonctionnalités ne servent pas à « faire joli » : structure plus claire, collaboration fluide, meilleure UX. Petit projet : inutile de tout déployer. Dizaines de routes, plusieurs équipes, modales complexes — cette approche change la donne.
Conclusion
Retour au projet aux 60 dossiers en désordre — après refactorisation, le vrai gain n’est pas la prouesse technique, mais enfin pouvoir se concentrer sur le métier, sans perdre du temps à chercher des fichiers, résoudre des conflits ou maintenir des layouts.
Récapitulatif :
| Fonctionnalité | Rôle principal | Cas d’usage | Syntaxe clé |
|---|---|---|---|
| Groupes de routes | Organiser, isoler les layouts | Multi-zones, collaboration | (folderName) |
| Layouts imbriqués | Empiler l’UI | Nav multi-niveaux | layout.js à chaque niveau |
| Routes parallèles | Plusieurs fragments simultanés | Tableau de bord, modules | @folderName |
| Routes interceptées | Intercepter, modale | Modale type Instagram | (.) (..) (...) |
Mon conseil : n’utilisez pas tout d’un coup. Commencez par les groupes de routes ; layouts imbriqués pour l’UI multi-niveaux ; routes parallèles et interceptées pour tableaux de bord et modales.
Ces concepts semblent déroutants au début — ma première lecture de la doc officielle aussi. Après une utilisation, la logique devient naturelle : structure des dossiers = routes, profondeur des fichiers = hiérarchie UI, interception = expérience utilisateur.
Si votre projet Next.js s’encombre, prenez une demi-journée pour tester une refactorisation par groupes de routes. Dans trois mois, vous vous remercierez.
Processus complet de refactorisation du routage avancé Next.js
Refactoriser une arborescence confuse en architecture claire avec groupes de routes, layouts imbriqués, routes parallèles et routes interceptées
⏱️ Estimated time: 4 hr
- 1
Step 1: Analyser la structure actuelle du projet
Évaluer le nombre et la complexité des routes :
• Compter les dossiers sous app (au-delà de 20, privilégier les groupes de routes)
• Identifier les zones fonctionnelles (marketing, boutique, back-office, etc.)
• Repérer les groupes de pages nécessitant des layouts différents
• Noter les points de friction en collaboration d'équipe
Critères de décision :
• Plus de 20 dossiers : groupes de routes
• Navigation multi-niveaux : layouts imbriqués
• Affichage simultané de modules indépendants : routes parallèles
• Interaction modale : routes interceptées + routes parallèles - 2
Step 2: Créer des groupes de routes par zone fonctionnelle
Utiliser des parenthèses pour créer des groupes par fonction ou équipe :
• Groupe (marketing) : pages marketing (accueil, à propos, tarifs)
• Groupe (shop) : pages boutique (produits, panier)
• Groupe (dashboard) : pages d'administration (commandes, utilisateurs)
Points d'attention :
• Le nom du groupe n'affecte pas l'URL, mais une même URL ne peut pas apparaître dans plusieurs groupes
• Chaque groupe peut avoir son propre layout.js
• La page d'accueil page.js doit être dans un groupe (pas dans app/page.js) - 3
Step 3: Concevoir la hiérarchie des layouts imbriqués
Concevoir les layouts selon la hiérarchie UI :
• Niveau 1 : app/layout.js (layout global : Header + Footer)
• Niveau 2 : layout.js de zone (ex. blog/layout.js avec barre latérale)
• Niveau 3 : layout.js de détail (ex. blog/[slug]/layout.js avec table des matières)
Points clés :
• Chaque layout n'ajoute que les éléments UI propres à son niveau
• Les layouts enfants héritent automatiquement des parents
• Les layouts ne se re-rendent pas — bonnes performances - 4
Step 4: Implémenter les routes parallèles (si nécessaire)
Créer des slots avec le symbole @ :
• Slot @modal : pour les modales
• Slots @sales, @orders : pour les modules indépendants du tableau de bord
Recevoir les slots dans layout.js :
• export default function Layout({ children, modal, sales, orders })
• Rendu JSX : {modal} {sales} {orders}
Chaque slot peut avoir son propre loading.js et error.js - 5
Step 5: Implémenter les routes interceptées (si modale nécessaire)
Créer la structure de routes interceptées :
• Sous @modal : (.)photos/[id]/page.js (intercepter une route au même niveau)
• photos/[id]/page.js (page complète)
Syntaxe :
• (.) : intercepter une route au même niveau
• (..) : intercepter une route au niveau parent
• (...) : intercepter depuis la racine
Créer default.js retournant null pour permettre la fermeture de la modale - 6
Step 6: Tester et valider
Vérifier toutes les fonctionnalités :
• Groupes de routes : URL inchangée, structure de fichiers claire
• Layouts imbriqués : hiérarchie UI correcte, état conservé
• Routes parallèles : chargement indépendant des modules, isolation des erreurs
• Routes interceptées : modale à la navigation client, page complète au rafraîchissement
Contrôle des performances :
• Utiliser Next.js DevTools pour vérifier le nombre de rendus de layout
• Confirmer que le layout ne se re-rend pas lors du changement de sous-route
FAQ
Les groupes de routes affectent-ils l'URL ?
Quand utiliser les groupes de routes ?
Les layouts imbriqués impactent-ils les performances ?
Quelle différence entre routes parallèles et composants classiques ?
Comment choisir entre (.), (..) et (...) pour les routes interceptées ?
Pourquoi les routes interceptées nécessitent-elles default.js ?
Faut-il refactoriser les routes en migrant du Pages Router vers l'App Router ?
18 min de lecture · Publié le: 18 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide complet Next.js
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Next.js Pages Router vers App Router : guide pratique et pièges à éviter
Migration complète de Next.js Pages Router vers App Router ? Stratégie progressive, 7 pièges courants et retours d'expérience production pour passer à Next.js 14 en toute sécurité.
Partie 3 sur 51
Suivant
Next.js App Router en pratique : groupes de routes et layouts imbriqués pour les grands projets
Groupes de routes, layouts imbriqués, routes parallèles et routes interceptées : structure de répertoires claire, moins de conflits d’URL et meilleure collaboration sur les grands projets Next.js.
Partie 5 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire