404 Not Found

404 Not Found


nginx

Parallel Routes & Intercepting Routes

Parallel routing is like multiple monitors—each screen operates independently but is interconnected; intercept routing is like a package pickup point—it intercepts packages along the way, processes them, and then continues delivery.

1. What You'll Learn


2. A Product Manager’s True Story

(1) Pain Point: Poor pop-up experience in the feed

Alice is developing an image browsing feature for a content platform and has encountered some user experience issues:

"When a user tapped on an image in the Feed, we displayed a full-screen modal, and the URL changed to /photos/123. As a result, when the user tried to return to the Feed by pressing the browser's back button, they were logged out of the entire app. The user complained, 'I just wanted to look at a picture—why can't I go back to the home page?'"

Issues Analyzed by Alice:

Issue Impact User Rating
Modal cannot be refreshed independently Refreshing the modal causes the entire page to refresh 2/5
Back button exits the app User flow disruption 1.5/5
Sidebar and main content are out of sync Navigation is confusing 2.5/5
Team View and Dashboard Are Separate Low Management Efficiency 3/5

(2) The Parallel + Intercepting Routes Solution

Use @modal to display a pop-up window for parallel routing + (.)photo to intercept image navigation in the feed stream.

TEXT
src/app/
├── layout.tsx              # Main Layout:@children + @modal
└── (feed)/
    ├── layout.tsx          # Feed Layout:@children + @sidebar
    ├── page.tsx            # Feed Stream Home Page
    └── photos/
        ├── [id]/
        │   └── page.tsx    # Full page: /photos/123
        └── (.)[id]/
            └── page.tsx    # Intercept: open as a modal in the Feed

(3) Revenue

Dimension Before (Standard Routing) After (Parallel + Intercepting)
Image Browsing Experience Full-page redirect, interrupts browsing Pop-up preview, stays on the current page
Back Button Behavior Exit the app Close the modal and return to the Feed
Page can be refreshed Not supported Refreshes to display the entire page; renders normally
Sidebar is independent Not independent @sidebar Renders independently, without affecting the main content

3. Parallel Routes

(1) Concepts and Usage

100%
graph TB
    subgraph "URL: /dashboard"
        A[layout.tsx] --> B[children<br/>Main Content]
        A --> C[@modal<br/>Pop-up Slot]
        A --> D[@sidebar<br/>Sidebar Slot]
        A --> E[@team<br/>Team Slots]
    end

    subgraph "Rendering Results"
        F[Main Content Area] & G[Pop-up Area] & H[Sidebar Area] & I[Team Area]
    end

    style A fill:#cce5ff
    style B fill:#d4edda
Slot Name Directory Prefix URL Impact Use Case
@children None (default) Standard URL Main page content
@modal @modal/ None Pop-ups, dialog boxes
@sidebar @sidebar/ None Sidebar Panel
@team @team/ None Team View

▶ Example: Basic Parallel Routes

Output:

TEXT
Diagram of route structure: file system paths map to URL paths.
TSX
// ============================================
// Parallel Routes Basics:Dashboard Multi-slot layout
// ============================================

// src/app/(dashboard)/layout.tsx — Parallel Routing Layout
export default function DashboardLayout({
  children,
  modal,
  sidebar,
  team,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
  sidebar: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <div className="flex h-screen">
      {/* Main Content Area */}
      <main className="flex-1 p-8 overflow-auto">
        {children}
      </main>

      {/* Sidebar Slot(Independent Parallel Rendering) */}
      <aside className="w-72 bg-gray-50 p-4 border-l">
        {sidebar}
      </aside>

      {/* Team Slots(Independent Parallel Rendering) */}
      <aside className="w-64 bg-gray-900 text-white p-4">
        {team}
      </aside>

      {/* Modal Slot (Conditional Rendering) */}
      {modal}
    </div>
  );
}

Output:

TEXT
Renders: Dashboard layout with dark sidebar (Dashboard/Projects/Settings links) + main content area.
TSX
// src/app/(dashboard)/@sidebar/default.tsx — Default State of the Sidebar
export default function SidebarDefault() {
  return (
    <div>
      <h3 className="font-bold text-lg mb-4">Sidebar</h3>
      <div className="space-y-2">
        <div className="p-3 bg-white rounded shadow-sm">
          <p className="font-medium">Recent Activity</p>
          <p className="text-sm text-gray-500">No recent activity</p>
        </div>
        <div className="p-3 bg-white rounded shadow-sm">
          <p className="font-medium">Notifications</p>
          <p className="text-sm text-gray-500">3 unread</p>
        </div>
      </div>
    </div>
  );
}
TSX
// src/app/(dashboard)/@team/default.tsx — Default Team Slot Status
export default function TeamDefault() {
  return (
    <div className="p-4">
      <h3 className="font-bold mb-4">Team</h3>
      <div className="space-y-3">
        {["Alice", "Bob", "Charlie", "Diana"].map(name => (
          <div key={name} className="flex items-center gap-2">
            <div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm">
              {name[0]}
            </div>
            <span className="text-sm">{name}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

Output:

TEXT
Visit /dashboard:
┌──────────────────────┬──────────┬──────────┐
│                      │          │          │
│   Main Content Area           │ Sidebar   │  Team    │
│   Dashboard          │ Recent   │  Alice   │
│   Welcome back!      │ Activity │  Bob     │
│                      │ Notif(3) │  Charlie │
│                      │          │  Diana   │
└──────────────────────┴──────────┴──────────┘

Three regions rendered independently,Do not affect each other

(2) default.tsx — Required default state

Each @slot directory must contain default.tsx, which is displayed when no matching route is found.

100%
graph TB
    A[User Navigation] --> B{Current URL<br/>matches slot route?}
    B -->|Matches| C[Show slot's page.tsx]
    B -->|Mismatch| D[Show slot's default.tsx]

    style C fill:#d4edda
    style D fill:#f8d7da

▶ Example: The Importance of default.tsx

Output:

TEXT
Diagram: User Navigation; Show slot's page.tsx; Show slot's default.tsx.
TSX
// ============================================
// None default.tsx It will happen 404
// Each @slot All of them must be present default.tsx
// ============================================

// src/app/@modal/default.tsx — Modal Hidden by default
export default function ModalDefault() {
  return null; // None Modal It does not exaggerate anything
}

// src/app/(dashboard)/@sidebar/default.tsx
export default function SidebarDefault() {
  return (
    <div className="p-4">
      <h3 className="font-bold text-sm text-gray-500 uppercase">
        Quick Links
      </h3>
      <nav className="mt-3 space-y-2">
        <a href="/dashboard" className="block text-blue-600">Dashboard</a>
        <a href="/dashboard/projects" className="block text-blue-600">Projects</a>
        <a href="/dashboard/settings" className="block text-blue-600">Settings</a>
      </nav>
    </div>
  );
}

Output:

TEXT
Includes a sidebar.

Output:

TEXT
Visit /dashboard:
- @modal No matching route found → Show ModalDefault (= null,Do not render)
- @sidebar No matching route found → Show SidebarDefault(Quick Links Navigation)
- @team No matching route found → Show TeamDefault(List of Team Members)

Visit /dashboard/photos/1(Assumption @modal Matches found):
- @modal matches → Display the pop-up window content
- Other slot Display each one's default.tsx

4. Intercepting Routes

(1) Interception Matching Rules

100%
graph TB
    A[Currently in Feed stream] --> B{Click the image link}
    B --> C[Intercept (.)photo]
    C --> D[Display a pop-up in the Feed]
    D --> E[The user refreshes the page]
    E --> F[Bypass Block<br/>Show Entire Page]

    style C fill:#cce5ff
    style D fill:#d4edda
    style F fill:#f8d7da
Syntax Matching Level Example
(.) Same Class feed/photos/(.)[id] Intercept feed/photos/[id]
(..) Back feed/(..)photos/[id] Intercept photos/[id]
(..)(..) Two levels up feed/(..)(..)photos/[id] Intercept at the root level photos/[id]
(...) Root level feed/(...)photos/[id] Intercept app/photos/[id]

▶ Example: Blocking Image Browsing

Output:

TEXT
Diagram: Currently in Feed stream; Intercept (.)photo; Display a pop-up in the Feed; The user refreshes the page; Bypass Block Show Entire Page.
TSX
// ============================================
// Intercepting Route + Parallel Route
// Feed Click the image below → Display a pop-up window on the current page
// ============================================

// Directory Structure:
// app/
//   layout.tsx                    # Root Layout (with @modal)
//   (feed)/
//     page.tsx                    # Feed stream
//     photos/
//       [id]/page.tsx             # Full page: /photos/1
//   @modal/
//     default.tsx                 # No modal
//     (.)photos/
//       [id]/page.tsx             # Intercept: display a popup in the Feed

// src/app/layout.tsx — Root Layout (with modal slot)
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        {modal}
      </body>
    </html>
  );
}

// src/app/(feed)/page.tsx — Feed Streaming Page
import Link from "next/link";

export default function FeedPage() {
  const photos = Array.from({ length: 12 }, (_, i) => ({
    id: i + 1,
    url: `https://picsum.photos/seed/${i + 1}/300/300`,
    title: `Photo ${i + 1}`,
  }));

  return (
    <div className="max-w-4xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6">Photo Feed</h1>
      <div className="grid grid-cols-3 gap-4">
        {photos.map(photo => (
          <Link
            key={photo.id}
            href={`/photos/${photo.id}`}
            className="block overflow-hidden rounded-lg hover:opacity-90 transition-opacity"
          >
            <img
              src={photo.url}
              alt={photo.title}
              className="w-full h-64 object-cover"
            />
            <p className="mt-2 text-sm font-medium text-center">{photo.title}</p>
          </Link>
        ))}
      </div>
    </div>
  );
}

Output:

TEXT
Renders a dynamic list of items using .map().
Visible content: Photo Feed
TSX
// ============================================
// Intercept Routes: Open a modal popup in the Feed
// Documents:src/app/@modal/(.)photos/[id]/page.tsx
// ============================================

'use client';

import { useRouter } from "next/navigation";

export default function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const router = useRouter();
  const { id } = params;

  return (
    // Background Mask
    <div
      className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"
      onClick={() => router.back()}
    >
      {/* Modal Content — Click the background to close */}
      <div
        className="bg-white rounded-2xl overflow-hidden max-w-2xl w-full mx-4"
        onClick={e => e.stopPropagation()}
      >
        <img
          src={`https://picsum.photos/seed/${id}/800/600`}
          alt={`Photo ${id}`}
          className="w-full h-auto"
        />
        <div className="p-6">
          <div className="flex items-center justify-between">
            <div>
              <h2 className="text-xl font-bold">Photo #{id}</h2>
              <p className="text-gray-500 text-sm mt-1">
                Captured by photographer
              </p>
            </div>
            <button
              onClick={() => router.back()}
              className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center hover:bg-gray-200"
            >
              ✕
            </button>
          </div>
          <div className="flex gap-2 mt-4">
            <span className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm">
              Nature
            </span>
            <span className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm">
              Landscape
            </span>
            <span className="px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-sm">
              HD
            </span>
          </div>
        </div>
      </div>
    </div>
  );
}
TSX
// ============================================
// Full page: Display the full page when accessed independently
// Documents:src/app/photos/[id]/page.tsx
// ============================================

import Link from "next/link";

export default function PhotoPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = params;

  return (
    <div className="max-w-4xl mx-auto p-8">
      <Link
        href="/"
        className="text-blue-600 hover:underline mb-4 inline-block"
      >
        &larr; Back to Feed
      </Link>
      <img
        src={`https://picsum.photos/seed/${id}/1200/800`}
        alt={`Photo ${id}`}
        className="w-full rounded-lg"
      />
      <div className="mt-6">
        <h1 className="text-3xl font-bold">Photo #{id}</h1>
        <p className="text-gray-500 mt-2">
          Full page view of photo {id}. This page is directly accessible
          and works even without JavaScript.
        </p>
        <div className="flex gap-4 mt-6">
          <Link
            href={`/photos/${Number(id) - 1}`}
            className="px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200"
          >
            ← Previous
          </Link>
          <Link
            href={`/photos/${Number(id) + 1}`}
            className="px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200"
          >
            Next →
          </Link>
        </div>
      </div>
    </div>
  );
}

Output:

TEXT
Scene 1: Click an image from the Feed
- Currently in / Page(Photo Feed Grid)
- Click on the 1 Upload a picture
- Display a larger image in a pop-up window(The address bar changes to /photos/1)
- Click the background mask → router.back() → Back to Feed stream
- Click the browser's back button → Close Pop-up → Feed The flow remains unchanged

Scene 2:Direct Access /photos/1
- Skip Intercepted Routes
- Show Full Page (full-width large image + Previous/Next buttons)
- Can be refreshed directly、Share Link

Scene 3:Refresh the pop-up page
- Popup window in the Feed → Press F5 to refresh
- Interception rules are not taking effect(Not from Feed Navigation)
- Show full text /photos/1 Page

(2) Multi-level Interception

▶ Example: Multi-level Interception Routing

Output:

TEXT
The page renders as described above, with the UI updating based on the described behavior.
TSX
// ============================================
// Example of Multi-Level Interception Routing
// Directory Structure:
// app/
//   photos/
//     [id]/page.tsx                    → /photos/1(Full Page)
//   (feed)/
//     page.tsx                         → / (Feed stream)
//     categories/
//       [cat]/page.tsx                 → /categories/nature(Category Page)
//       (..)(..)photos/
//         [id]/page.tsx                → Intercept /photos/1 → Pop-up on the Categories Page
// ============================================

// Explanation of Matching Rules:
// (feed)/categories/[cat] levels = app/(feed)/categories/[cat]
// Objective: app/photos/[id] levels = app/photos/[id]
// You need to go up two levels to match → (..)(..)

// app/(feed)/categories/(..)(..)photos/[id]/page.tsx
'use client';
import { useRouter } from "next/navigation";

export default function CategoryPhotoModal({ params }) {
  const router = useRouter();

  return (
    <div
      className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center"
      onClick={() => router.back()}
    >
      <div onClick={e => e.stopPropagation()} className="max-w-lg">
        <img
          src={`https://picsum.photos/seed/${params.id}/600/400`}
          alt=""
          className="rounded-lg"
        />
        <button
          onClick={() => router.back()}
          className="mt-2 px-4 py-2 bg-white rounded"
        >
          Close
        </button>
      </div>
    </div>
  );
}

Output:

TEXT
CategoryPhotoModal renders its UI.

Output:

TEXT
From /categories/nature, click on the image:
1. Display a larger image in a pop-up window(Address bar /photos/1)
2. Click Close or background → router.back() → Back to /categories/nature
3. Direct Access /photos/1 → Show Full Page(No pop-ups)
4. The pop-up window on the category page is working properly

5. Modal + Parallel Route Combination Model

(1) Architecture Design

100%
graph TB
    subgraph "URL-Driven Modal Popup"
        A[User Actions] --> B{Navigate to /photos/1}
        B --> C[In the Feed?]
        C -->|Yes| D[@modal slot<br/>Match and Intercept Routes]
        C -->|No| E[Direct Access<br/>Show Full Page]
        D --> F[Pop-up Display<br/>Address Bar Update]
        F --> G[User Logout]
        G --> H[router.back()]
        H --> I[Back to Feed<br/>Modal Disappear]
    end

    style D fill:#cce5ff
    style E fill:#d4edda
    style G fill:#f8d7da
User Action URL Change Modal State Page State
Click the Feed Image //photos/1 Show Pop-up Keep Feed in the Background
Close Pop-up /photos/1/ Hide Feed Unchanged
Refresh /photos/1 /photos/1 No changes No pop-ups Show full page
Share /photos/1 Shareable No pop-ups Recipients see the full page

▶ Example: Complete Modal

Output:

TEXT
Diagram: User Actions; In the Feed?; @modal slot Match and Intercept Routes; Direct Access Show Full Page; Pop-up Display Address Bar Update; User Logout.
TSX
// ============================================
// Complete Modal + Parallel Route + Intercepting Route
// Implement a"Refreshable、Shareable、Can be undone"The pop-up system
// ============================================

// src/app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        {modal}
      </body>
    </html>
  );
}

// src/app/@modal/default.tsx
export default function Default() {
  return null;
}

// src/app/@modal/(.)photos/[id]/page.tsx — Block Pop-ups
'use client';
import { useRouter } from "next/navigation";

export default function PhotoModal({ params }) {
  const router = useRouter();

  return (
    <div
      className="fixed inset-0 bg-black/80 flex items-center justify-center z-50"
      onClick={() => router.back()}
    >
      <div
        className="bg-white rounded-xl max-w-3xl w-full mx-4 shadow-2xl"
        onClick={e => e.stopPropagation()}
      >
        <div className="flex justify-end p-2">
          <button
            onClick={() => router.back()}
            className="w-8 h-8 flex items-center justify-center hover:bg-gray-100 rounded-full"
          >
            ✕
          </button>
        </div>
        <img
          src={`https://picsum.photos/seed/${params.id}/800/600`}
          alt=""
          className="w-full"
        />
        <div className="p-6">
          <h2 className="text-2xl font-bold">Photo #{params.id}</h2>
          <div className="flex gap-4 mt-4">
            <a
              href={`/photos/${params.id}`}
              className="text-sm text-blue-600 hover:underline"
              onClick={() => router.push(`/photos/${params.id}`)}
            >
              Open in full page →
            </a>
          </div>
        </div>
      </div>
    </div>
  );
}

// src/app/(feed)/page.tsx — Feed stream
import Link from "next/link";

export default function Feed() {
  return (
    <div className="max-w-6xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6">Photo Gallery</h1>
      <div className="grid grid-cols-4 gap-4">
        {[1, 2, 3, 4, 5, 6, 7, 8].map(id => (
          <Link
            key={id}
            href={`/photos/${id}`}
            className="block group"
          >
            <div className="aspect-square bg-gray-100 rounded-lg overflow-hidden">
              <img
                src={`https://picsum.photos/seed/${id}/400/400`}
                alt=""
                className="w-full h-full object-cover group-hover:scale-105 transition-transform"
              />
            </div>
          </Link>
        ))}
      </div>
    </div>
  );
}

Output:

TEXT
Renders: Navigation component using useRouter for programmatic routing.

Output:

TEXT
User Flow:

1. Visit / → See 8 Image Grid
2. Click on the image #3 → Display a larger image in a pop-up window(URL: /photos/3)
3. Click the top-right corner ✕ → Close Pop-up(URL: /)
4. Click image #5 again → Popup (URL: /photos/5)
5. Click the browser's Back button → Close Pop-up(URL: /)
6. Press the Back button again → Exit the app(Normal Behavior)

Refresh Scene:
1. While the pop-up is open, press F5 → Refresh the entire page
2. Interception rules are not taking effect → Show /photos/3 Full Page
3. The page is functioning properly.,There is a navigation link to go back

Sharing Scenarios:
1. Copy /photos/3 URL Send to a friend
2. Friend, open this → View the full page(Not a pop-up)
3. Page SEO Normal,All content is indexable

6. Conditional Rendering of the Dashboard

(1) Render different views based on the role

100%
graph TB
    A[Dashboard Layout] --> B{User Roles}
    B -->|admin| C[@admin Panel]
    B -->|editor| D[@editor Panel]
    B -->|viewer| E[@viewer Panel]

    style A fill:#cce5ff
    style C fill:#d4edda
    style D fill:#d4edda
    style E fill:#d4edda

▶ Example: Role-Driven Dashboard

Output:

TEXT
Diagram of role-based access: admin (full), editor (read/write), viewer (read-only).
TSX
// ============================================
// Conditional Rendering:Displays differently depending on the character Dashboard
// ============================================

// src/app/(dashboard)/layout.tsx — Role Routing Layout
export default function DashboardLayout({
  children,
  admin,
  editor,
  viewer,
}: {
  children: React.ReactNode;
  admin: React.ReactNode;
  editor: React.ReactNode;
  viewer: React.ReactNode;
}) {
  // Simulate from Cookie/Session Get a Character
  const role = "admin";

  return (
    <div className="flex h-screen">
      {/* Main Content */}
      <main className="flex-1 p-8">
        {children}
      </main>

      {/* Render different slots based on the character */}
      <aside className="w-80 border-l p-4">
        {role === "admin" && admin}
        {role === "editor" && editor}
        {role === "viewer" && viewer}
      </aside>
    </div>
  );
}

Output:

TEXT
Renders the ▶ Example: Role-Driven Dashboard component UI as described in the section.
TSX
// src/app/(dashboard)/@admin/default.tsx — Administrator Panel
export default function AdminPanel() {
  return (
    <div className="space-y-4">
      <h3 className="font-bold text-lg">Admin Controls</h3>
      <div className="bg-red-50 border border-red-200 rounded-lg p-4">
        <p className="font-medium text-red-700">System Health</p>
        <div className="mt-2 space-y-2">
          <div className="flex justify-between text-sm">
            <span>CPU Usage</span>
            <span className="text-green-600">45%</span>
          </div>
          <div className="flex justify-between text-sm">
            <span>Memory</span>
            <span className="text-yellow-600">72%</span>
          </div>
          <div className="flex justify-between text-sm">
            <span>Active Users</span>
            <span className="text-blue-600">1,234</span>
          </div>
        </div>
      </div>
      <div className="bg-white rounded-lg border p-4">
        <p className="font-medium">Pending Approvals</p>
        <p className="text-2xl font-bold text-orange-600 mt-2">12</p>
      </div>
      <button className="w-full p-2 bg-blue-600 text-white rounded-lg">
        View All Settings
      </button>
    </div>
  );
}

// src/app/(dashboard)/@editor/default.tsx — Editor Panel
export default function EditorPanel() {
  return (
    <div className="space-y-4">
      <h3 className="font-bold text-lg">Editor Tools</h3>
      <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
        <p className="font-medium text-blue-700">Draft Count</p>
        <p className="text-3xl font-bold mt-2">8</p>
      </div>
      <div className="bg-white rounded-lg border p-4">
        <p className="font-medium">Recent Edits</p>
        <div className="mt-2 space-y-2 text-sm">
          <p>• Updated homepage hero</p>
          <p>• Fixed typo in about page</p>
          <p>• Added new blog post</p>
        </div>
      </div>
      <button className="w-full p-2 bg-green-600 text-white rounded-lg">
        Create New Post
      </button>
    </div>
  );
}

// src/app/(dashboard)/@viewer/default.tsx — Viewer Panel
export default function ViewerPanel() {
  return (
    <div className="space-y-4">
      <h3 className="font-bold text-lg">Overview</h3>
      <div className="bg-gray-50 border rounded-lg p-4">
        <p className="font-medium">Your Dashboard</p>
        <p className="text-sm text-gray-500 mt-2">
          You have read-only access. Contact admin for editing permissions.
        </p>
      </div>
      <div className="bg-white border rounded-lg p-4">
        <p className="font-medium">Quick Links</p>
        <div className="mt-2 space-y-2 text-sm">
          <a href="/docs" className="block text-blue-600">Documentation</a>
          <a href="/reports" className="block text-blue-600">Reports</a>
          <a href="/help" className="block text-blue-600">Help Center</a>
        </div>
      </div>
    </div>
  );
}

Output:

TEXT
Administrator(role = "admin"):
┌────────────────────────────┬──────────────────────┐
│                            │  Admin Controls       │
│    Dashboard               │  ┌────────────────┐  │
│    Welcome back, Alice!    │  │ CPU: 45% ✅    │  │
│                            │  │ Memory: 72% ⚠️  │  │
│    Project Summary         │  │ Users: 1,234   │  │
│    12 Active Projects      │  └────────────────┘  │
│    48 Pending Tasks        │  Pending Approvals    │
│    8 Team Members          │  12                   │
│                            │  [View All Settings]  │
└────────────────────────────┴──────────────────────┘

Editor(role = "editor"):
┌────────────────────────────┬──────────────────────┐
│                            │  Editor Tools         │
│    Dashboard               │  Draft Count: 8       │
│    Welcome back, Bob!      │  Recent Edits:        │
│                            │  • Updated homepage   │
│    My Projects             │  • Fixed typo         │
│    5 Active Projects       │  • Added blog post    │
│                            │  [Create New Post]    │
└────────────────────────────┴──────────────────────┘

Viewers(role = "viewer"):
┌────────────────────────────┬──────────────────────┐
│                            │  Overview             │
│    Dashboard               │  Read-only access     │
│    Welcome, Charlie!       │  Quick Links:         │
│                            │  • Documentation      │
│    Team Activity           │  • Reports            │
│    10 team members online  │  • Help Center        │
│                            │                      │
└────────────────────────────┴──────────────────────┘

7. Complete Example: Feed + Modal + Conditional Dashboard

TSX
// ============================================
// Comprehensive Example:Comprehensive Routing System for Content Platforms
// Covering Parallel Routes + Intercepting Routes + Conditions Dashboard
// ============================================

// src/app/layout.tsx — Root Layout (with modal slot)
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className="bg-gray-50">
        <header className="bg-white shadow-sm sticky top-0 z-40">
          <div className="max-w-6xl mx-auto px-4 py-3 flex justify-between">
            <a href="/" className="text-xl font-bold text-blue-600">PhotoVault</a>
            <nav className="flex gap-4">
              <a href="/" className="hover:text-blue-600">Feed</a>
              <a href="/dashboard" className="hover:text-blue-600">Dashboard</a>
            </nav>
          </div>
        </header>
        {children}
        {modal}
      </body>
    </html>
  );
}

// src/app/@modal/default.tsx
export default function Default() {
  return null;
}

// src/app/@modal/(.)photos/[id]/page.tsx — Block Pop-ups
'use client';
import { useRouter } from "next/navigation";

export default function PhotoModal({ params }) {
  const router = useRouter();

  return (
    <div
      className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"
      onClick={() => router.back()}
    >
      <div
        className="bg-white rounded-xl max-w-2xl w-full mx-4 overflow-hidden shadow-2xl"
        onClick={e => e.stopPropagation()}
      >
        <img
          src={`https://picsum.photos/seed/${params.id}/800/600`}
          alt=""
          className="w-full"
        />
        <div className="p-4 flex justify-between items-center">
          <div>
            <h2 className="font-bold">Photo #{params.id}</h2>
            <p className="text-sm text-gray-500">Click background to close</p>
          </div>
          <button
            onClick={() => router.push(`/photos/${params.id}`)}
            className="text-sm text-blue-600 hover:underline"
          >
            Open Full Page
          </button>
        </div>
      </div>
    </div>
  );
}

// src/app/(feed)/page.tsx — Feed stream
import Link from "next/link";

export default function FeedPage() {
  const photos = Array.from({ length: 12 }, (_, i) => ({
    id: i + 1,
    url: `https://picsum.photos/seed/${i + 1}/400/400`,
    title: `Photo ${i + 1}`,
  }));

  return (
    <div className="max-w-6xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6">Photo Feed</h1>
      <div className="grid grid-cols-4 gap-4">
        {photos.map(photo => (
          <Link
            key={photo.id}
            href={`/photos/${photo.id}`}
            className="block aspect-square rounded-lg overflow-hidden bg-gray-100"
          >
            <img
              src={photo.url}
              alt={photo.title}
              className="w-full h-full object-cover hover:scale-105 transition-transform"
            />
          </Link>
        ))}
      </div>
    </div>
  );
}

// src/app/photos/[id]/page.tsx — Full Image Page
export default async function PhotoPage({ params }) {
  const { id } = params;

  return (
    <div className="max-w-4xl mx-auto p-8">
      <a href="/" className="text-blue-600 hover:underline mb-4 inline-block">
        &larr; Back to Feed
      </a>
      <img
        src={`https://picsum.photos/seed/${id}/1200/800`}
        alt=""
        className="w-full rounded-lg shadow-lg"
      />
      <div className="mt-6">
        <h1 className="text-3xl font-bold">Photo #{id}</h1>
        <p className="text-gray-500 mt-2">
          This is the full-page view. Share this link directly with others.
        </p>
      </div>
    </div>
  );
}

// src/app/(dashboard)/layout.tsx — Dashboard Parallel Routing Layout
export default function DashboardLayout({
  children,
  admin,
}: {
  children: React.ReactNode;
  admin: React.ReactNode;
}) {
  const role = "admin";

  return (
    <div className="max-w-6xl mx-auto p-8 flex gap-8">
      <div className="flex-1">{children}</div>
      <aside className="w-80">
        {role === "admin" && admin}
      </aside>
    </div>
  );
}

// src/app/(dashboard)/@admin/default.tsx
export default function AdminPanel() {
  return (
    <div className="bg-white rounded-xl shadow-sm border p-6 space-y-4">
      <h3 className="font-bold text-lg">Admin Panel</h3>
      <div className="space-y-2">
        <div className="flex justify-between">
          <span>Total Photos</span>
          <span className="font-bold">1,234</span>
        </div>
        <div className="flex justify-between">
          <span>Daily Uploads</span>
          <span className="font-bold text-green-600">+48</span>
        </div>
        <div className="flex justify-between">
          <span>Storage Used</span>
          <span className="font-bold">237 GB</span>
        </div>
      </div>
      <button className="w-full p-2 bg-blue-600 text-white rounded-lg">
        Manage Gallery
      </button>
    </div>
  );
}

// src/app/(dashboard)/page.tsx — Dashboard Home
export default function DashboardPage() {
  return (
    <div>
      <h1 className="text-2xl font-bold">Dashboard</h1>
      <p className="text-gray-500 mt-2">Overview of your photo gallery</p>
      <div className="grid grid-cols-2 gap-4 mt-6">
        <div className="bg-white p-6 rounded-xl shadow-sm border">
          <p className="text-sm text-gray-500">This Week</p>
          <p className="text-3xl font-bold mt-1">342</p>
          <p className="text-sm text-green-600 mt-1">↑ 12% from last week</p>
        </div>
        <div className="bg-white p-6 rounded-xl shadow-sm border">
          <p className="text-sm text-gray-500">Total Views</p>
          <p className="text-3xl font-bold mt-1">89.4K</p>
          <p className="text-sm text-green-600 mt-1">↑ 8% from last month</p>
        </div>
      </div>
    </div>
  );
}

Expected Output:

TEXT
1. Visit / → 12 Image Grid
2. Click on the image → Display a larger image in a pop-up window(URL: /photos/5)
3. Click on the background → Close Pop-up,Back to Feed
4. Refresh → Show Full Text /photos/5 Page
5. Share /photos/5 → Friends saw the entire page
6. Visit /dashboard → Show Dashboard + Admin Panel
7. From the Dashboard, click on the image → Same popup experience (Intercept Routes)

❓ FAQ

Q Does every @slot need a default.tsx file?
A Yes. If there is no default.tsx file, Next.js will return a 404 error when the current URL does not match a route for that slot. Even if your modal slot isn’t displayed most of the time, you still need export default function Default() { return null; }.
Q Can slots in Parallel Routes communicate with each other?
A They cannot communicate directly. Each slot is an independent Server Component and cannot share state. If communication is needed, you can pass values via URL parameters (searchParams) or use a Context Provider in the root layout.
Q How is the level of (..) for Intercepting Routes calculated?
A (..) is calculated based on the actual file system hierarchy, but Route Groups (group) do not consume levels. For example, app/(feed)/photos/(.)[id]/page.tsx intercepts app/(feed)/photos/[id]/page.tsx because (feed) is not counted.
Q Do Parallel Routes affect page performance?
A There is a slight impact because each slot is an independent server component that needs to be rendered separately. However, Next.js automatically parallelizes these requests, making it faster than serial rendering. Recommendation: Do not use more than 3–4 slots, and avoid unnecessary slots.
Q Should conditional rendering use Parallel Routes or be handled client-side?
A We recommend Parallel Routes. Although client-side conditional rendering (if (role === 'admin')) is also possible, Parallel Routes keeps each character’s view file independent, type-safe, and easier to test and separate.

📖 Summary


📝 Exercises

  1. Basic Exercise (⭐): Create a @modal slot in your project and implement the basic functionality of "click the button → display a text snippet in a popup" (use default.tsx to return null).

  2. Advanced Exercise (⭐⭐): Create an image browsing system: The Feed page (/) displays a grid of images; clicking an image opens a pop-up window ((.)photos/[id]); and directly accessing /photos/1 displays the full page. Verify the behavior of the back button and the refresh function.

  3. Challenge (⭐⭐⭐): Implement a role-driven dashboard system: Create three slots—@admin, @editor, and @viewer—so that each role displays different dashboard content (administrators see system monitoring, editors see draft statistics, and viewers see read-only prompts). Verify the conditional rendering by switching the role variable.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏