Next.jsIntermédiaire18 min de lecture

Guide complet Next.js 13+ avec App Router

Découvrez les nouvelles fonctionnalités de Next.js 13 et comment utiliser l'App Router efficacement.

NG

Next Gen Dev

12 septembre 2025

Retour au blogMe contacter
  • Next.js
  • App Router
  • React
  • SSR

A retenir

Découvrez les nouvelles fonctionnalités de Next.js 13 et comment utiliser l'App Router efficacement.

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 !

Sommaire

Intentions de recherche

Requetes ciblees et recherches associees

Ces formulations aident a couvrir des variantes de recherche proches du sujet principal, y compris des requetes longues traines.

  • comment nextjs 13 app router guide ?
  • guide next.js

Mot-cle principal

guide next.js

Variantes: Next.js, App Router, React, SSR, nextjs 13 app router guide

FAQ SEO

Questions frequentes autour du sujet

Que couvre cet article sur guide next.js ?

Cet article synthese des actions concretes, des points de vigilance et des priorites d execution pour passer plus vite de la theorie a la mise en place.

Suite logique

Transformer ce sujet en plan d action

Si tu veux appliquer ce que tu viens de lire a ton site (SEO, conversion, structure de contenu, automatisation), je peux t aider a prioriser les actions utiles.

Maillage interne

Articles similaires

Voir tous les articles