mirror of
https://github.com/greedychipmunk/agent-skills.git
synced 2026-09-14 20:46:42 +08:00
6.9 KiB
6.9 KiB
Next.js App Router Patterns
File Conventions
The App Router uses a file-system based router where folders define routes and special files define UI.
Special Files
| File | Purpose |
|---|---|
layout.tsx |
Shared UI for segment and children |
page.tsx |
Unique UI for a route (makes route accessible) |
loading.tsx |
Loading UI (wraps page in Suspense) |
error.tsx |
Error UI (wraps page in Error Boundary) |
not-found.tsx |
Not found UI |
template.tsx |
Re-rendered layout (no state preservation) |
default.tsx |
Fallback for parallel routes |
route.tsx |
API endpoint |
Layout Patterns
Root Layout (Required)
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
Nested Layout
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard">
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/settings">Settings</a>
</nav>
<main>{children}</main>
</div>
)
}
Layout with Metadata
// app/blog/layout.tsx
import type { Metadata } from "next"
export const metadata: Metadata = {
title: {
template: "%s | Blog",
default: "Blog",
},
}
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}
Loading States
Basic Loading UI
// app/dashboard/loading.tsx
export default function Loading() {
return <div className="skeleton">Loading...</div>
}
Loading with Suspense Boundaries
// app/dashboard/page.tsx
import { Suspense } from "react"
async function SlowComponent() {
const data = await fetch("/api/slow")
return <div>{data}</div>
}
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading stats...</div>}>
<SlowComponent />
</Suspense>
</div>
)
}
Error Handling
Error Boundary
// app/dashboard/error.tsx
"use client"
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
Global Error Handler
// app/global-error.tsx
"use client"
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}
Not Found Page
// app/not-found.tsx
import Link from "next/link"
export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find the requested resource</p>
<Link href="/">Return Home</Link>
</div>
)
}
Route Groups
Route groups allow organizing routes without affecting the URL structure.
Organizing by Feature
app/
├── (marketing)/
│ ├── about/page.tsx # /about
│ └── blog/page.tsx # /blog
├── (shop)/
│ ├── products/page.tsx # /products
│ └── cart/page.tsx # /cart
└── layout.tsx
Multiple Root Layouts
app/
├── (marketing)/
│ ├── layout.tsx # Marketing layout
│ └── page.tsx
├── (app)/
│ ├── layout.tsx # App layout
│ └── dashboard/page.tsx
Parallel Routes
Parallel routes allow rendering multiple pages in the same layout simultaneously.
Basic Parallel Routes
app/
├── @analytics/
│ └── page.tsx
├── @team/
│ └── page.tsx
├── layout.tsx
└── page.tsx
// app/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<div>
{children}
<div className="sidebar">
{analytics}
{team}
</div>
</div>
)
}
Conditional Rendering with Parallel Routes
// app/layout.tsx
import { getUser } from "@/lib/auth"
export default async function Layout({
children,
admin,
user,
}: {
children: React.ReactNode
admin: React.ReactNode
user: React.ReactNode
}) {
const currentUser = await getUser()
return (
<div>
{currentUser.role === "admin" ? admin : user}
{children}
</div>
)
}
Intercepting Routes
Intercepting routes allow loading a route within the current layout.
Convention
(.)- Match same level(..)- Match one level above(..)(..)- Match two levels above(...)- Match from root
Modal Pattern
app/
├── @modal/
│ └── (.)photo/[id]/page.tsx # Intercepted route (modal)
├── photo/
│ └── [id]/page.tsx # Direct access (full page)
├── layout.tsx
└── page.tsx
// app/@modal/(.)photo/[id]/page.tsx
import { Modal } from "@/components/modal"
export default function PhotoModal({
params,
}: {
params: { id: string }
}) {
return (
<Modal>
<img src={`/photos/${params.id}`} alt="" />
</Modal>
)
}
// app/layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
{children}
{modal}
</>
)
}
Template Pattern
Templates create a new instance on navigation (unlike layouts).
// app/template.tsx
export default function Template({
children,
}: {
children: React.ReactNode
}) {
return <div className="fade-in">{children}</div>
}
Use Cases:
- Enter/exit animations
- Features relying on useEffect (logging page views)
- Features relying on useState (per-page feedback form)
Metadata Patterns
Static Metadata
// app/page.tsx
import type { Metadata } from "next"
export const metadata: Metadata = {
title: "Home",
description: "Welcome to our website",
openGraph: {
title: "Home",
description: "Welcome to our website",
images: ["/og-image.jpg"],
},
}
Dynamic Metadata
// app/posts/[id]/page.tsx
import type { Metadata, ResolvingMetadata } from "next"
type Props = {
params: { id: string }
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const post = await getPost(params.id)
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.image, ...(await parent).openGraph?.images || []],
},
}
}
Last Updated: January 2026