Design wechseln

Next.js Advanced Routing: Route Groups, Nested Layouts, Parallel Routes und Intercepting Routes

Easton editorial illustration: server-client bridge

Letzte Woche habe ich ein zwei Jahre altes Next.js-E-Commerce-Projekt übernommen. Beim Öffnen von app starrte ich auf über 60 Ordner – about, products, admin-users, marketing-campaign, shop-cart … alles flach nebeneinander. Eine benutzerbezogene Seite finden? Durch Marketing-Ordner wühlen. Noch frustrierender: drei Leute ändern gleichzeitig Routen – mindestens zwei Git-Konflikte pro Tag; im Code-Review brauchte es oft eine halbe Stunde nur, um die Dateistruktur zu verstehen.

Dann fiel mir ein: Next.js hat Route Groups, Parallel Routes und mehr – seit Next.js 13 dokumentiert, im Projekt aber ungenutzt. Nicht, weil die Tools fehlen, sondern weil unklar war, wann welches Feature passt.

Drei Stunden Dokumentation, zwei Tage Refactoring: die Struktur war aufgeräumt, im Team-Chat kein „wieder Konflikt“. Die vier Features lösen konkrete Probleme: Route Groups ordnen Verzeichnisse, Nested Layouts nutzen Struktur wieder, Parallel Routes zeigen mehrere Bereiche gleichzeitig, Intercepting Routes ermöglichen elegante Modals.

Wenn Ihr Next.js-Projekt wächst, Dateien unübersichtlich werden und das Team ständig kollidiert – nach diesem Artikel wissen Sie, welches Feature wann wie einzusetzen ist und welche Fallen Sie vermeiden sollten.

Route Groups – aufgeräumte Verzeichnisse

Was sind Route Groups?

Kurz: Ordner in Klammern, z. B. (marketing) oder (shop). Next.js setzt den Klammernamen nicht in die URL.

Ohne reales Szenario wirkt das banal. Stellen Sie sich vor: Marketing (Start, Über uns), Shop (Produkte, Warenkorb), Admin (Bestellungen, Nutzer). Klassisch: alles unter app oder erzwungene URL-Präfixe wie /marketing/about – unattraktiv.

Route Groups trennen im Dateisystem, ohne die sichtbare URL zu verändern.

Drei praktische Einsatzzwecke

1. Nach Team oder Funktion ordnen

60 flache Ordner werden drei Gruppen:

app/
├── (marketing)/    # Marketing-Team
│   ├── page.js     # Start → yoursite.com/
│   ├── about/      # → yoursite.com/about
│   └── pricing/    # → yoursite.com/pricing
├── (shop)/         # Frontend-Team
│   ├── products/   # → yoursite.com/products
│   └── cart/       # → yoursite.com/cart
└── (dashboard)/    # Backend-Team
    ├── orders/     # → yoursite.com/orders
    └── users/      # → yoursite.com/users

URLs bleiben sauber; die Struktur ist auf einen Blick klar.

2. Unterschiedliche Root-Layouts pro Bereich

Marketing und Admin brauchen verschiedene Navigation – beide starten aber unter /about bzw. /orders. Lösung: pro Route Group ein layout.js.

app/
├── (marketing)/
│   ├── layout.js        # Marketing: Top-Nav + Hero
│   └── ...
├── (shop)/
│   ├── layout.js        # Shop: Warenkorb-Icon + Kategorien
│   └── ...
└── (dashboard)/
    ├── layout.js        # Admin: Sidebar + Auth-Check
    └── ...

Drei Layouts, ein Projekt – ohne Subdomains oder mehrere Next.js-Instanzen.

3. Layout selektiv teilen

Manche Seiten teilen ein Layout, andere nicht. Beispiel Blog: Artikel mit linker Navigation, Startseite ohne.

app/
├── blog/
│   ├── page.js         # Blog-Start ohne Sidebar
│   └── (articles)/     # Gruppe mit Sidebar-Layout
│       ├── layout.js
│       ├── [slug]/     # → /blog/xxx
│       └── ...

(articles) ändert die URL nicht – /blog/my-first-post bleibt, nur diese Gruppe nutzt das Sidebar-Layout.

Refactoring-Beispiel

Vorher (Auszug):

app/
├── page.js
├── about/
├── pricing/
├── products/
├── products-detail/
├── cart/
├── checkout/
├── admin-orders/
├── admin-users/
├── admin-settings/
├── marketing-campaign/
├── ...(noch ~50)

Nachher:

app/
├── (marketing)/
│   ├── layout.js
│   ├── page.js
│   ├── about/
│   ├── pricing/
│   └── campaign/
├── (shop)/
│   ├── layout.js
│   ├── products/
│   ├── cart/
│   └── checkout/
└── (dashboard)/
    ├── layout.js
    ├── orders/
    ├── users/
    └── settings/

Konflikte im Review sanken um etwa 70 %, weil Teams nur „ihre“ Group anfassen.

Drei Fallstricke

1. URL-Konflikte

app/
├── (marketing)/
│   └── about/page.js   # → /about
└── (shop)/
    └── about/page.js   # → /about (Konflikt!)

Next.js meldet Error: Conflicting route. Lösung: Pfad umbenennen oder echte Verzeichnisebene ohne Klammern.

2. Vollständiger Reload zwischen Root-Layouts

Wechsel von (shop) nach (marketing) kann kurz „flackern“ – verschiedene Root-Layouts werden entladen und neu gemountet (full page load). Gemeinsame Teile in app/layout.js auslagern, Unterschiede nur in Group-Layouts.

3. Startseite bei mehreren Root-Layouts

Mit mehreren Groups und je eigenem layout.js gehört page.js der Startseite in eine Group (z. B. (marketing)/page.js), nicht nach app/page.js.

Kern: Route Groups organisieren Dateien, ändern URLs nicht und erlauben getrennte Root-Layouts. Ab ~20 Ordner unter app lohnt sich der Schritt.

Nested Layouts – flexible UI-Hierarchie

Welches Problem lösen sie?

Startseite: Top-Nav. Blog-Liste: Nav + linke Kategorien. Artikel: Nav + Kategorien + rechtes Inhaltsverzeichnis.

Ohne Nested Layouts bauen Sie das in jeder Seite manuell zusammen – eine Nav-Änderung betrifft viele Dateien.

Nested Layouts verschachteln Layouts wie Matroschka-Puppen – jede Ebene fügt UI hinzu, ohne äußere Ebenen zu wiederholen.

Funktionsweise

Jeder Ordner kann layout.js haben; Kinder erben und erweitern.

Beispiel Lernplattform:

app/
├── layout.js              # Root: Header + Footer
└── courses/
    ├── layout.js          # + linke Kurskategorien
    ├── page.js
    └── [id]/
        ├── layout.js      # + Fortschrittsleiste rechts
        └── page.js

Für /courses/123:

  1. app/layout.js (Header + Footer)
  2. courses/layout.js (Sidebar)
  3. courses/[id]/layout.js (Fortschritt)
  4. courses/[id]/page.js (Inhalt)

Nav-Änderung nur in app/layout.js – gilt überall.

Blog mit drei Ebenen

app/
├── layout.js                    # Header + Footer
└── blog/
    ├── layout.js                # + Sidebar
    ├── page.js
    └── [slug]/
        ├── layout.js            # + Inhaltsverzeichnis
        └── page.js

app/layout.js

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  )
}

app/blog/layout.js

export default function BlogLayout({ children }) {
  return (
    <div className="blog-container">
      <Sidebar />
      <main>{children}</main>
    </div>
  )
}

app/blog/[slug]/layout.js

export default function ArticleLayout({ children }) {
  return (
    <div className="article-container">
      {children}
      <TableOfContents />
    </div>
  )
}

Jedes Layout kümmert sich nur um seine Ebene – Next.js verschachtelt automatisch.

Mit Route Groups kombinieren

Route Groups trennen horizontal (Marketing / Shop / Admin), Nested Layouts stapeln vertikal.

app/
└── (shop)/
    ├── layout.js             # Shop-Root: Warenkorb + Kategorien
    ├── products/
    │   ├── layout.js         # + Filter-Sidebar
    │   ├── page.js
    │   └── [id]/
    │       ├── layout.js     # + Breadcrumb
    │       └── page.js
    └── cart/
        └── page.js
  • /products: Shop-Root + Filter
  • /products/123: alle drei Ebenen
  • /cart: nur Shop-Root

Keine if (detailPage)-Logik in einer Mega-Komponente.

Zwei Details

1. Layouts rendern beim Unterrouten-Wechsel nicht neu

Von /blog nach /blog/my-post: app/layout.js und blog/layout.js bleiben – Scroll und Eingaben in der Sidebar bleiben erhalten.

2. Layout sieht keine dynamischen params der Kind-Route

params.id aus products/[id]/page.js ist in layout.js nicht verfügbar. Titel im Layout? Context oder Daten eine Ebene höher.

Kern: Verzeichnistiefe = UI-Tiefe – ein layout.js pro Ebene, eine Änderung für eine ganze Region.

Parallel Routes – mehrere Seiten gleichzeitig

Wann brauchen Sie sie?

Dashboard mit drei Modulen: Verkaufschart (langsam), Bestellliste (schnell), Lagerwarnung (andere API). Klassisch: drei Requests in einer page.js – ein Fehler killt alles, ein Loading-State für alle.

Parallel Routes rendern mehrere unabhängige Slots in einer Ansicht – je Slot eigenes loading.js und error.js, paralleles Laden.

Syntax: @ für Slots

app/
└── dashboard/
    ├── layout.js
    ├── @sales/
    │   └── page.js
    ├── @orders/
    │   └── page.js
    ├── @inventory/
    │   └── page.js
    └── page.js

Ordner mit @ sind Slots. Im Layout kommen sie als Props:

export default function DashboardLayout({
  children,
  sales,
  orders,
  inventory
}) {
  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>
  )
}

Dashboard-Beispiel

@sales/page.js

async function getSalesData() {
  const res = await fetch('https://api.example.com/sales')
  return res.json()
}

export default async function SalesWidget() {
  const data = await getSalesData()
  return (
    <div>
      <h3>Umsatz diesen Monat</h3>
      <Chart data={data} />
    </div>
  )
}

@orders/page.js und @inventory/page.js analog – Slots laden parallel. Schnelle Module erscheinen zuerst; ein Fehler betrifft nur einen Slot.

Eigenes loading.js und error.js

app/
└── dashboard/
    ├── @sales/
    │   ├── page.js
    │   ├── loading.js
    │   └── error.js
    └── ...

@sales/loading.js

export default function SalesLoading() {
  return <div>Verkaufsdaten werden geladen …</div>
}

@sales/error.js

'use client'

export default function SalesError({ error, reset }) {
  return (
    <div>
      <p>Verkaufsdaten konnten nicht geladen werden</p>
      <button onClick={reset}>Erneut versuchen</button>
    </div>
  )
}

default.js

Navigation von /dashboard nach /dashboard/settings: Slots passen nicht mehr – Standard: alter Slot-Inhalt bleibt. Mit default.js und return null verschwindet der Slot beim Wechsel.

Parallel Routes + Intercepting Routes = typisches Modal-Muster (nächstes Kapitel). Allein schon für Dashboards mit unabhängigen Modulen sehr nützlich.

Intercepting Routes – elegante Modals

Instagram-ähnliche Bildansicht

Feed → Bild klicken → Modal, URL /photo/abc123. Zurück schließt Modal, Refresh zeigt Vollseite, geteilte URL öffnet Vollseite.

Intercepting Routes fangen Client-Navigation ab und zeigen Zielinhalt als Modal; bei direktem Aufruf oder Refresh die normale Vollseite.

Syntax

  • (.) – gleichstufige Route
  • (..) – eine Ebene höher
  • (..)(..) – zwei Ebenen höher
  • (...) – von Root

Liste /photos, Klick auf /photos/123 → Modal bei Navigation, Vollseite bei Refresh:

app/
├── @modal/
│   ├── (.)photos/
│   │   └── [id]/
│   │       └── page.js
│   └── default.js
├── layout.js
├── page.js
└── photos/
    └── [id]/
        └── page.js

@modal/(.)photos/ fängt die gleichstufige photos-Route ab.

Schritt für Schritt

app/layout.js

export default function RootLayout({ children, modal }) {
  return (
    <html>
      <body>
        {children}
        {modal}
      </body>
    </html>
  )
}

app/page.js – Grid mit Links zu /photos/${id}.

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()}>Schließen</button>
        <Image src={`/images/photo${params.id}.jpg`} fill />
      </div>
    </div>
  )
}

app/photos/[id]/page.js – Vollseite mit Nav und Beschreibung.

app/@modal/default.js

export default function Default() {
  return null
}

Verhalten: Klick → Client-Nav → Intercept → Modal über Feed; Zurück → default.js → Modal weg; Refresh/Direktaufruf → photos/[id]/page.js ohne Modal.

Ebene wählen

Intercept unter app/@modal/:

  • Ziel app/photos/(.)photos
  • Ziel app/shop/products/ von shop/@modal(..)products
  • Unsicher → (...) von Root, später verfeinern

Fallstricke

  1. Ohne default.js bleibt Modal sichtbar.
  2. useRouter braucht 'use client' im Modal.
  3. Tiefe Verschachtelung: Pfad genau berechnen oder vorerst (...).

Intercepting + Parallel Routes: teilbare URL, Refresh = Vollseite, Zurück schließt Modal – wie bei Instagram, Twitter, Airbnb.

Kombiniert – alle vier Features

E-Commerce-Struktur

Drei Bereiche: Marketing (/, /about), Shop (/products, /cart), Dashboard (/dashboard).

Shop: Listen-Filter, Detail-Breadcrumb, Schnellvorschau-Modal (Intercept), Refresh = Voll-Detail.

Dashboard: @sales, @orders, @inventory parallel.

app/
├── layout.js
├── (marketing)/
│   ├── layout.js
│   ├── page.js
│   ├── about/
│   └── pricing/
├── (shop)/
│   ├── layout.js
│   ├── @modal/
│   │   ├── (.)products/
│   │   │   └── [id]/page.js
│   │   └── default.js
│   ├── products/
│   │   ├── layout.js
│   │   ├── page.js
│   │   └── [id]/
│   │       ├── layout.js
│   │       └── page.js
│   └── cart/page.js
└── (dashboard)/
    ├── layout.js
    ├── @sales/ ...
    ├── @orders/ ...
    ├── @inventory/ ...
    └── page.js

app/(shop)/layout.js{children} und {modal}.

app/(shop)/@modal/(.)products/[id]/page.js – Client-Modal mit router.back() und Link zur Voll-Detailseite.

app/(dashboard)/layout.js{sales}, {orders}, {inventory} parallel.

Warum so?

EntscheidungGrund
Route GroupsGetrennte Root-Layouts, weniger Team-Konflikte
Nested LayoutsUI-Stufen = Ordnerstufen
Intercept + ParallelVorschau ohne Listenverlust, URL trotzdem /products/123
Parallel im DashboardUnabhängige Lade- und Fehlerzustände

Nach dem Refactoring: ~65 % weniger Konflikte, schnelleres Onboarding, gezielte Layout-Änderungen, höhere Conversion bei Schnellvorschau (Nutzer mussten seltener zurücknavigieren).

Fazit

Vier Features im Überblick:

FeatureRolleSzenarioSyntax
Route GroupsDateien ordnen, Layouts trennenMulti-Bereich, Teams(name)
Nested LayoutsUI schichtenMehrstufige Navigationlayout.js pro Ebene
Parallel RoutesMehrere FragmenteDashboard, Module@slot
Intercepting RoutesNav abfangen, ModalFeed + Detail-URL(.) (..) (...)

Empfehlung: Nicht alles auf einmal. Zuerst Route Groups; Nested Layouts bei gestaffelter UI; Parallel und Intercept für Dashboard und Modals.

Am Anfang wirkt die Doku dicht – nach einmaligem Einsatz passt es: Verzeichnis = Route, Tiefe = UI, Intercept = UX.

Wenn app unübersichtlich wird: einen halben Tag in Route Groups investieren – in drei Monaten danken Sie sich oft selbst dafür.

Vollständiger Refactoring-Workflow für Next.js Advanced Routing

Von chaotischer Verzeichnisstruktur zu Route Groups, Nested Layouts, Parallel Routes und Intercepting Routes

⏱️ Estimated time: 4 hr

  1. 1

    Step 1: Bestehende Projektstruktur analysieren

    Komplexität der aktuellen Routen bewerten:
    • Ordner unter app zählen (ab 20 empfiehlt sich Route Groups)
    • Funktionsbereiche erkennen (Marketing, Shop, Admin)
    • Seitengruppen mit unterschiedlichen Layouts identifizieren
    • Konfliktpunkte im Team dokumentieren

    Entscheidungskriterien:
    • Ordner > 20: Route Groups
    • Mehrstufige Navigation: Nested Layouts
    • Mehrere unabhängige Module gleichzeitig: Parallel Routes
    • Modal-Interaktion: Intercepting Routes + Parallel Routes
  2. 2

    Step 2: Route Groups für Funktionsbereiche anlegen

    Mit Klammern Route Groups erstellen:
    • (marketing): Marketing-Seiten (Start, Über uns, Preise)
    • (shop): Shop (Produkte, Warenkorb)
    • (dashboard): Admin (Bestellungen, Benutzer)

    Hinweise:
    • Group-Namen beeinflussen die URL nicht; dieselbe URL darf nicht in mehreren Groups vorkommen
    • Jede Route Group kann eigenes layout.js haben
    • page.js der Startseite muss in einer Route Group liegen (nicht app/page.js bei mehreren Root-Layouts)
  3. 3

    Step 3: Nested-Layout-Hierarchie entwerfen

    Nach UI-Hierarchie planen:
    • Ebene 1: app/layout.js (site-weit: Header + Footer)
    • Ebene 2: Bereichs-layout.js (z. B. blog/layout.js mit Sidebar)
    • Ebene 3: Detail-layout.js (z. B. blog/[slug]/layout.js mit Inhaltsverzeichnis)

    Umsetzung:
    • Jede Ebene nur eigene UI-Elemente
    • Kind-Layouts erben vom Eltern-Layout
    • layout wird beim Seitenwechsel nicht neu gerendert – gute Performance
  4. 4

    Step 4: Parallel Routes implementieren (falls nötig)

    Slots mit @ anlegen:
    • @modal: Modal-Slot
    • @sales, @orders: unabhängige Dashboard-Module

    In layout.js empfangen:
    • export default function Layout({ children, modal, sales, orders })
    • Im JSX: {modal} {sales} {orders}

    Jeder Slot kann eigenes loading.js und error.js haben
  5. 5

    Step 5: Intercepting Routes implementieren (für Modals)

    Intercepting-Struktur anlegen:
    • Unter @modal: (.)photos/[id]/page.js (gleichstufige Route abfangen)
    • Parallel: photos/[id]/page.js (Vollseite)

    Syntax:
    • (.): gleichstufig
    • (..): eine Ebene höher
    • (...): von Root

    default.js mit return null ist Pflicht, damit Modals schließen
  6. 6

    Step 6: Testen und validieren

    Funktionen prüfen:
    • Route Groups: URL unverändert, Struktur klar
    • Nested Layouts: UI-Hierarchie und State-Erhalt
    • Parallel Routes: unabhängiges Laden, Fehlerisolierung
    • Intercepting Routes: Modal bei Client-Navigation, Vollseite bei Refresh

    Performance:
    • Next.js DevTools für Layout-Render-Zähler
    • layout soll beim Unterrouten-Wechsel nicht neu rendern

FAQ

Beeinflussen Route Groups die URL?
Nein. Route Groups heißen (marketing) usw.; Next.js ignoriert den Klammernamen. Die URL von (marketing)/about/page.js bleibt /about, nicht /marketing/about.
Wann sollte ich Route Groups verwenden?
Wenn app mehr als 20 Ordner hat oder verschiedene Bereiche unterschiedliche Root-Layouts brauchen. Besonders bei Multi-Team-Projekten – Git-Konflikte sinken deutlich.
Beeinträchtigen Nested Layouts die Performance?
Nein. Beim Wechsel zwischen Unterrouten werden Nested Layouts nicht neu gerendert, nur die innerste page.js lädt neu. Status in Layouts (Sidebar-Scroll, Suchfeld) bleibt erhalten.
Was ist der Unterschied zwischen Parallel Routes und normalen Komponenten?
Jeder Parallel-Route-Slot ist ein unabhängiges Seitenfragment mit eigenem loading.js und error.js, paralleles Laden. Normale Komponenten warten auf alle Daten – ein Fehler blockiert die ganze Seite.
Wie wähle ich (.), (..) oder (...) bei Intercepting Routes?
(.) für gleichstufige Routen (z. B. @modal und photos unter app), (..) eine Ebene höher, (...) von Root. Im Zweifel mit (...) starten und danach anpassen.
Warum braucht Intercepting Routes default.js?
Ohne default.js bleibt der Slot-Inhalt bei Nicht-Match bestehen (Modal bleibt offen). default.js mit return null schließt das Modal korrekt.
Muss ich beim Wechsel von Pages Router zu App Router die Routen umbauen?
Nicht zwingend bei kleinen Projekten (< 20 Ordner). Bei größeren Projekten oder komplexen Layouts lohnen Route Groups und Nested Layouts – die Wartbarkeit steigt deutlich.

8 Min. Lesezeit · Veröffentlicht am: 18. Dez. 2025 · Aktualisiert am: 14. Juli 2026

Ähnliche Beiträge

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog