Introduction à l'App Router
Next.js 13 introduit l'App Router, une nouvelle façon de structurer vos applications avec des fonctionnalités avancées basées sur React Server Components.
Structure des dossiers
app/
├── layout.tsx // Layout racine
├── page.tsx // Page d'accueil
├── loading.tsx // UI de chargement
├── error.tsx // UI d'erreur
├── not-found.tsx // Page 404
├── globals.css // Styles globaux
├── blog/
│ ├── layout.tsx // Layout pour /blog
│ ├── page.tsx // Page /blog
│ ├── loading.tsx // Loading pour /blog
│ └── [slug]/
│ ├── page.tsx // Page /blog/[slug]
│ └── loading.tsx
├── dashboard/
│ ├── (auth)/ // Route group
│ │ ├── login/
│ │ └── register/
│ └── settings/
└── api/
└── posts/
└── route.ts // API route
Server Components par défaut
// app/blog/page.tsx
import { Metadata } from 'next';
// Fetch des données côté serveur
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Cache pendant 1h
});
if (!res.ok) {
throw new Error('Failed to fetch posts');
}
return res.json();
}
export const metadata: Metadata = {
title: 'Blog - Mon Site',
description: 'Articles et tutoriels sur le développement web'
};
export default async function BlogPage() {
const posts = await getPosts();
return (
<div>
<h1>Blog</h1>
<div className="grid gap-4">
{posts.map(post => (
<article key={post.id} className="border p-4 rounded">
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<time>{new Date(post.date).toLocaleDateString()}</time>
</article>
))}
</div>
</div>
);
}
Layouts partagés et imbriqués
// app/layout.tsx (Root Layout)
import './globals.css';
export const metadata = {
title: {
template: '%s | Mon Site',
default: 'Mon Site'
},
description: 'Site web moderne avec Next.js 13'
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr">
<body>
<header>
<nav>Navigation globale</nav>
</header>
<main>{children}</main>
<footer>Footer global</footer>
</body>
</html>
);
}
// app/blog/layout.tsx (Layout pour le blog)
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="container mx-auto">
<aside className="sidebar">
<h3>Catégories</h3>
<ul>
<li>React</li>
<li>Next.js</li>
<li>TypeScript</li>
</ul>
</aside>
<div className="content">
{children}
</div>
</div>
);
}
Loading et Error UI
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded mb-4"></div>
<div className="space-y-3">
<div className="h-4 bg-gray-200 rounded"></div>
<div className="h-4 bg-gray-200 rounded"></div>
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
</div>
</div>
);
}
// app/blog/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="text-center p-8">
<h2 className="text-2xl font-bold text-red-600 mb-4">
Une erreur est survenue
</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<button
onClick={reset}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Réessayer
</button>
</div>
);
}
// app/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div className="text-center p-8">
<h2 className="text-4xl font-bold mb-4">404</h2>
<p className="text-gray-600 mb-4">Page non trouvée</p>
<Link href="/" className="text-blue-500 hover:underline">
Retour à l'accueil
</Link>
</div>
);
}
Streaming et Suspense
import { Suspense } from 'react';
async function SlowComponent() {
// Simulation d'une opération lente
await new Promise(resolve => setTimeout(resolve, 2000));
return <div>Contenu lent chargé !</div>;
}
function LoadingSkeleton() {
return (
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
</div>
);
}
export default function BlogPage() {
return (
<div>
<h1>Blog</h1>
{/* Contenu immédiat */}
<section>
<h2>Articles récents</h2>
<p>Contenu disponible immédiatement</p>
</section>
{/* Contenu en streaming */}
<Suspense fallback={<LoadingSkeleton />}>
<SlowComponent />
</Suspense>
{/* Autres composants qui peuvent charger en parallèle */}
<Suspense fallback={<div>Chargement des commentaires...</div>}>
<CommentsSection />
</Suspense>
</div>
);
}
Métadonnées dynamiques
// app/blog/[slug]/page.tsx
import { Metadata } from 'next';
interface Props {
params: { slug: string };
}
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`);
if (!res.ok) return null;
return res.json();
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug);
if (!post) {
return {
title: 'Article non trouvé'
};
}
return {
title: post.title,
description: post.excerpt,
keywords: post.tags,
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.excerpt,
images: [
{
url: post.image,
width: 1200,
height: 630,
alt: post.title,
}
],
type: 'article',
publishedTime: post.publishedAt,
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.image],
},
};
}
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug);
if (!post) {
return <div>Article non trouvé</div>;
}
return (
<article>
<header>
<h1>{post.title}</h1>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
<p className="text-gray-600">{post.excerpt}</p>
</header>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Route Groups et organisation
app/
├── (marketing)/
│ ├── page.tsx // /
│ ├── about/
│ │ └── page.tsx // /about
│ └── contact/
│ └── page.tsx // /contact
├── (shop)/
│ ├── layout.tsx // Layout spécifique shop
│ ├── products/
│ │ └── page.tsx // /products
│ └── cart/
│ └── page.tsx // /cart
└── dashboard/
├── layout.tsx // Layout dashboard
├── page.tsx // /dashboard
└── settings/
└── page.tsx // /dashboard/settings
API Routes avec App Router
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') || '1';
try {
const posts = await getPosts(parseInt(page));
return NextResponse.json(posts);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const newPost = await createPost(body);
return NextResponse.json(newPost, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create post' },
{ status: 500 }
);
}
}
// app/api/posts/[id]/route.ts
interface Context {
params: { id: string };
}
export async function GET(
request: NextRequest,
{ params }: Context
) {
const post = await getPost(params.id);
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
return NextResponse.json(post);
}
Client Components
// components/InteractiveComponent.tsx
'use client';
import { useState } from 'react';
export default function InteractiveComponent() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
);
}
// Utilisation dans un Server Component
// app/page.tsx
import InteractiveComponent from '../components/InteractiveComponent';
export default function HomePage() {
return (
<div>
<h1>Page statique rendue côté serveur</h1>
<InteractiveComponent />
</div>
);
}
Middleware et redirections
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Redirection basée sur l'URL
if (request.nextUrl.pathname === '/old-blog') {
return NextResponse.redirect(new URL('/blog', request.url));
}
// Authentification
if (request.nextUrl.pathname.startsWith('/dashboard')) {
const token = request.cookies.get('auth-token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/old-blog']
};
Next.js 13+ avec l'App Router révolutionne le développement React avec ces nouvelles fonctionnalités puissantes !
