Changer le thème

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

Easton editorial illustration: server-client bridge

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 :

  1. Couche externe : app/layout.js (nav + Footer)
  2. Couche intermédiaire : courses/layout.js (barre latérale catégories)
  3. Couche interne : courses/[id]/layout.js (barre de progression)
  4. 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, @inventoryun 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

  1. Clic sur l’accueil :

    • Link navigue vers /photos/1 côté client
    • Next.js intercepte via @modal/(.)photos/[id]
    • Modale superposée au fil, URL /photos/1, pas de rechargement complet
  2. Retour arrière :

    • router.back() vers /
    • Slot @modal rend default.js (null)
    • Modale fermée, fil intact
  3. Rafraîchissement ou accès direct /photos/1 :

    • Pas d’interception (pas navigation client)
    • Rendu de app/photos/[id]/page.js complet
  4. 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/123 pour 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 principalCas d’usageSyntaxe clé
Groupes de routesOrganiser, isoler les layoutsMulti-zones, collaboration(folderName)
Layouts imbriquésEmpiler l’UINav multi-niveauxlayout.js à chaque niveau
Routes parallèlesPlusieurs fragments simultanésTableau de bord, modules@folderName
Routes interceptéesIntercepter, modaleModale 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. 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. 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. 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. 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. 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. 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 ?
Non. Les groupes de routes utilisent des parenthèses (ex. (marketing)) ; Next.js ignore le nom entre parenthèses et l'URL reste identique. Par exemple, (marketing)/about/page.js correspond toujours à /about, et non à /marketing/about.
Quand utiliser les groupes de routes ?
Lorsque le dossier app dépasse 20 dossiers, ou lorsque différentes zones fonctionnelles nécessitent des layouts racine distincts. Les groupes de routes conviennent particulièrement aux projets multi-équipes et réduisent significativement les conflits Git.
Les layouts imbriqués impactent-ils les performances ?
Non. Lors d'un changement de sous-route, les layouts imbriqués Next.js ne se re-rendent pas — seul le page.js le plus interne se recharge. Vous pouvez conserver un état dans le layout (position de défilement de la barre latérale, contenu du champ de recherche) : il reste intact entre les pages.
Quelle différence entre routes parallèles et composants classiques ?
Chaque slot de route parallèle est un fragment de page indépendant avec son propre loading.js et error.js, chargé en parallèle sans bloquer les autres. Avec des composants classiques, il faut attendre que toutes les données soient chargées ; une erreur dans un composant peut faire échouer toute la page.
Comment choisir entre (.), (..) et (...) pour les routes interceptées ?
(.) intercepte une route au même niveau (ex. @modal et photos sous app), (..) intercepte une route au niveau parent, (...) intercepte depuis la racine. En cas de doute, commencez par (...) depuis la racine ; ajustez ensuite selon la structure des dossiers.
Pourquoi les routes interceptées nécessitent-elles default.js ?
Sans default.js, lorsque la route ne correspond plus, le slot conserve son contenu précédent (la modale reste affichée). Il faut créer default.js retournant null pour fermer correctement la modale quand la route ne correspond plus.
Faut-il refactoriser les routes en migrant du Pages Router vers l'App Router ?
Pas forcément. Pour un petit projet (moins de 20 dossiers), une structure plate peut suffire. Pour un projet plus large ou des layouts et interactions complexes, les groupes de routes, layouts imbriqués et autres fonctionnalités avancées améliorent nettement la maintenabilité.

18 min de lecture · Publié le: 18 déc. 2025 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog