AiTechWorlds
AiTechWorlds
Two powerful App Router features that most tutorials skip: route groups for organizing routes without affecting URLs, and parallel routes for showing multiple pages simultaneously in one layout.
A folder wrapped in parentheses (name) creates a route group. The folder name is excluded from the URL path.
src/app/
├── (marketing)/ ← Group name is invisible in URLs
│ ├── layout.tsx → Applies to / about/ pricing/ only
│ ├── page.tsx → URL: /
│ ├── about/
│ │ └── page.tsx → URL: /about
│ └── pricing/
│ └── page.tsx → URL: /pricing
├── (auth)/ ← Another group
│ ├── layout.tsx → Minimal layout for auth pages
│ ├── login/
│ │ └── page.tsx → URL: /login
│ └── register/
│ └── page.tsx → URL: /register
└── (dashboard)/ ← Protected section
├── layout.tsx → Authenticated layout with sidebar
├── dashboard/
│ └── page.tsx → URL: /dashboard
└── settings/
└── page.tsx → URL: /settings
The marketing pages get a public layout with a header and footer. Auth pages get a minimal layout — just the form, centered. Dashboard pages get a sidebar layout with an auth check. All share the same root layout.tsx.
Without route groups you'd have to put all these layouts inside nested folders, affecting the URL structure.
Route groups let you apply different root layouts to different top-level sections:
// src/app/(marketing)/layout.tsx
export default function MarketingLayout({ children }: { children: React.ReactNode }) {
return (
<>
<MarketingNav />
{children}
<MarketingFooter />
</>
);
}
// src/app/(auth)/layout.tsx
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="bg-white rounded-2xl shadow-lg p-8 w-full max-w-md">
{children}
</div>
</div>
);
}
// src/app/(dashboard)/layout.tsx
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const user = await getSession();
if (!user) redirect("/login");
return (
<div className="flex">
<Sidebar user={user} />
<main className="flex-1">{children}</main>
</div>
);
}
Parallel routes show multiple pages in the same layout simultaneously using named slots. A folder named @slotName defines a slot:
src/app/
├── layout.tsx
├── page.tsx
├── @modal/
│ ├── default.tsx → Shown when no modal route matches (usually null)
│ └── (.)photos/[id]/
│ └── page.tsx → Intercepted photo route rendered as a slot
└── photos/
├── page.tsx → /photos (grid)
└── [id]/
└── page.tsx → /photos/123 (full page)
The layout receives all slots as props:
// src/app/layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html>
<body>
{children}
{modal} {/* Renders on top when a modal route is active */}
</body>
</html>
);
}
// src/app/@modal/default.tsx — shown when no modal is active
export default function ModalDefault() {
return null;
}
The (.), (..), (..)(..) prefix intercepts navigation to another route and shows it in the current context:
(.) → Intercept routes at the same level
(..) → Intercept routes one level up
(..)(...) → Two levels up
(...) → From the root
// src/app/@modal/(.)photos/[id]/page.tsx
// Intercepts navigation to /photos/[id] and shows it as a modal
import { Modal } from "@/components/Modal";
import PhotoDetail from "@/components/PhotoDetail";
export default async function PhotoModal({ params }: { params: { id: string } }) {
const photo = await getPhoto(params.id);
return (
<Modal>
<PhotoDetail photo={photo} />
</Modal>
);
}
// src/components/Modal.tsx
"use client";
import { useRouter } from "next/navigation";
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter();
return (
<div
className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4"
onClick={() => router.back()}
>
<div
className="bg-white rounded-2xl max-w-2xl w-full max-h-[90vh] overflow-auto"
onClick={e => e.stopPropagation()}
>
{children}
</div>
</div>
);
}
Now:
router.push("/photos/123") → Modal opens (intercepted)/photos/123 directly → Full-page photo view (not intercepted)/photos/123 → Full-page view loadsThis is exactly how Instagram, Pinterest, and most image grid apps work. With Next.js, it's just a few files.
Modal with URL — Any dialog that should be shareable/bookmarkable:
Split view — Two panels with independent navigation:
Tab persistence — Tabs that each maintain their own URL and state:
src/app/
├── @inbox/
│ ├── default.tsx
│ └── page.tsx → /inbox content
└── @notifications/
├── default.tsx
└── page.tsx → /notifications content
Both slots render simultaneously in the layout. Each has its own URL state. Refreshing the page restores both.
Next lesson: When to use Client Components — the decision framework for Server vs Client.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises