Skip to content

⚡ Next.js

📋 Vue d'ensemble

Next.js est un framework React qui simplifie le développement d'applications web modernes. Next.js 13+ introduit l'App Router, une nouvelle architecture de routage basée sur les dossiers et les composants.

🏗️ App Router

Structure des dossiers

L'App Router utilise une structure de dossiers pour définir les routes :

app/
├── 📁 layout.tsx          # Layout racine (toujours présent)
├── 📁 page.tsx            # Page d'accueil (/)
├── 📁 globals.css         # Styles globaux
├── 📁 shows/
│   ├── 📁 page.tsx        # Liste des spectacles (/shows)
│   ├── 📁 [id]/
│   │   └── 📁 page.tsx    # Détail d'un spectacle (/shows/[id])
│   └── 📁 new/
│       └── 📁 page.tsx    # Créer un spectacle (/shows/new)
├── 📁 admin/
│   ├── 📁 layout.tsx      # Layout admin (/admin)
│   └── 📁 page.tsx        # Dashboard admin (/admin)
└── 📁 api/
    ├── 📁 shows/
    │   └── 📁 route.ts    # API spectacles (/api/shows)
    └── 📁 users/
        └── 📁 route.ts    # API utilisateurs (/api/users)

Layout racine

Le layout racine s'applique à toutes les pages de l'application :

tsx
// app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: {
    default: 'LIPAIX - Théâtre d\'improvisation',
    template: '%s | LIPAIX'
  },
  description: 'Découvrez nos spectacles et événements d\'improvisation',
  keywords: ['théâtre', 'improvisation', 'spectacle', 'LIPAIX'],
  authors: [{ name: 'LIPAIX Team' }],
  creator: 'LIPAIX Team',
  publisher: 'LIPAIX',
  formatDetection: {
    email: false,
    address: false,
    telephone: false,
  },
  metadataBase: new URL('https://www.lipaix.com'),
  alternates: {
    canonical: '/',
    languages: {
      'fr-FR': '/fr',
      'en-US': '/en',
    },
  },
  openGraph: {
    type: 'website',
    locale: 'fr_FR',
    url: 'https://www.lipaix.com',
    title: 'LIPAIX - Théâtre d\'improvisation',
    description: 'Découvrez nos spectacles et événements d\'improvisation',
    siteName: 'LIPAIX',
    images: [
      {
        url: '/og-image.jpg',
        width: 1200,
        height: 630,
        alt: 'LIPAIX - Théâtre d\'improvisation',
      },
    ],
  },
  twitter: {
    card: 'summary_large_image',
    title: 'LIPAIX - Théâtre d\'improvisation',
    description: 'Découvrez nos spectacles et événements d\'improvisation',
    images: ['/og-image.jpg'],
    creator: '@lipaix',
  },
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
  verification: {
    google: 'your-google-verification-code',
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="fr" className="h-full">
      <body className={`${inter.className} h-full`}>
        <div className="min-h-full flex flex-col">
          <Header />
          <main className="flex-1">
            {children}
          </main>
          <Footer />
        </div>
      </body>
    </html>
  );
}

Layouts imbriqués

Les layouts peuvent être imbriqués pour créer des structures complexes :

tsx
// app/admin/layout.tsx
import { AdminSidebar } from '@/components/AdminSidebar';
import { AdminHeader } from '@/components/AdminHeader';

export default function AdminLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="min-h-screen bg-gray-50">
      <AdminHeader />
      <div className="flex">
        <AdminSidebar />
        <main className="flex-1 p-6">
          {children}
        </main>
      </div>
    </div>
  );
}

// app/admin/page.tsx
export default function AdminPage() {
  return (
    <div>
      <h1 className="text-3xl font-bold mb-6">Tableau de bord administrateur</h1>
      {/* Contenu de la page admin */}
    </div>
  );
}

📱 Pages et composants

Page d'accueil

tsx
// app/page.tsx
import { Hero } from '@/components/Hero';
import { FeaturedEvents } from '@/components/FeaturedEvents';
import { AboutSection } from '@/components/AboutSection';

export default function HomePage() {
  return (
    <div>
      <Hero />
      <FeaturedEvents />
      <AboutSection />
    </div>
  );
}

Page dynamique avec paramètres

tsx
// app/shows/[id]/page.tsx
import { notFound } from 'next/navigation';
import { getShowById } from '@/lib/shows';
import { EventDetails } from '@/components/EventDetails';
import { AvailabilityForm } from '@/components/AvailabilityForm';

interface ShowPageProps {
  params: {
    id: string;
  };
}

export default async function ShowPage({ params }: ShowPageProps) {
  const show = await getShowById(params.id);

  if (!show) {
    notFound();
  }

  return (
    <div className="container mx-auto px-4 py-8">
      <div className="max-w-4xl mx-auto">
        <EventDetails show={show} />
        <AvailabilityForm showId={show.id} />
      </div>
    </div>
  );
}

// Génération des métadonnées dynamiques
export async function generateMetadata({ params }: ShowPageProps) {
  const show = await getShowById(params.id);

  if (!show) {
    return {
      title: 'Spectacle non trouvé',
    };
  }

  return {
    title: show.title,
    description: show.description,
    openGraph: {
      title: show.title,
      description: show.description,
      type: 'event',
      startTime: show.date,
    },
  };
}

Page avec recherche

tsx
// app/shows/page.tsx
import { SearchForm } from '@/components/SearchForm';
import { EventList } from '@/components/EventList';
import { getShows } from '@/lib/shows';

interface ShowsPageProps {
  searchParams: {
    q?: string;
    status?: string;
    date?: string;
  };
}

export default async function ShowsPage({ searchParams }: ShowsPageProps) {
  const shows = await getShows(searchParams);

  return (
    <div className="container mx-auto px-4 py-8">
      <div className="max-w-6xl mx-auto">
        <header className="mb-8">
          <h1 className="text-4xl font-bold text-gray-900 mb-4">
            Nos spectacles
          </h1>
          <p className="text-lg text-gray-600">
            Découvrez tous nos événements d'improvisation
          </p>
        </header>

        <SearchForm />
        
        <div className="mt-8">
          <EventList events={shows} />
        </div>
      </div>
    </div>
  );
}

🔌 API Routes

Route API simple

tsx
// app/api/shows/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getShows, createShow } from '@/lib/shows';
import { validateShowData } from '@/lib/validation';

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    const query = searchParams.get('q') || '';
    const status = searchParams.get('status') || '';
    const limit = parseInt(searchParams.get('limit') || '10');

    const shows = await getShows({ query, status, limit });

    return NextResponse.json({
      success: true,
      data: shows,
      pagination: {
        total: shows.length,
        limit,
        hasMore: shows.length === limit
      }
    });
  } catch (error) {
    console.error('Erreur lors de la récupération des spectacles:', error);
    
    return NextResponse.json(
      {
        success: false,
        error: 'Erreur lors de la récupération des spectacles'
      },
      { status: 500 }
    );
  }
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    
    // Validation des données
    const validationResult = validateShowData(body);
    if (!validationResult.success) {
      return NextResponse.json(
        {
          success: false,
          error: 'Données invalides',
          details: validationResult.errors
        },
        { status: 400 }
      );
    }

    // Vérification de l'authentification
    const authHeader = request.headers.get('authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return NextResponse.json(
        {
          success: false,
          error: 'Authentification requise'
        },
        { status: 401 }
      );
    }

    // Création du spectacle
    const show = await createShow(body);

    return NextResponse.json({
      success: true,
      data: show
    }, { status: 201 });
  } catch (error) {
    console.error('Erreur lors de la création du spectacle:', error);
    
    return NextResponse.json(
      {
        success: false,
        error: 'Erreur lors de la création du spectacle'
      },
      { status: 500 }
    );
  }
}

Route API avec paramètres dynamiques

tsx
// app/api/shows/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getShowById, updateShow, deleteShow } from '@/lib/shows';

interface ShowRouteProps {
  params: {
    id: string;
  };
}

export async function GET(
  request: NextRequest,
  { params }: ShowRouteProps
) {
  try {
    const show = await getShowById(params.id);
    
    if (!show) {
      return NextResponse.json(
        {
          success: false,
          error: 'Spectacle non trouvé'
        },
        { status: 404 }
      );
    }

    return NextResponse.json({
      success: true,
      data: show
    });
  } catch (error) {
    console.error('Erreur lors de la récupération du spectacle:', error);
    
    return NextResponse.json(
      {
        success: false,
        error: 'Erreur lors de la récupération du spectacle'
      },
      { status: 500 }
    );
  }
}

export async function PATCH(
  request: NextRequest,
  { params }: ShowRouteProps
) {
  try {
    const body = await request.json();
    
    // Vérification de l'authentification et des permissions
    const authHeader = request.headers.get('authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return NextResponse.json(
        {
          success: false,
          error: 'Authentification requise'
        },
        { status: 401 }
      );
    }

    // Mise à jour du spectacle
    const updatedShow = await updateShow(params.id, body);

    return NextResponse.json({
      success: true,
      data: updatedShow
    });
  } catch (error) {
    console.error('Erreur lors de la mise à jour du spectacle:', error);
    
    return NextResponse.json(
      {
        success: false,
        error: 'Erreur lors de la mise à jour du spectacle'
      },
      { status: 500 }
    );
  }
}

export async function DELETE(
  request: NextRequest,
  { params }: ShowRouteProps
) {
  try {
    // Vérification de l'authentification et des permissions
    const authHeader = request.headers.get('authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return NextResponse.json(
        {
          success: false,
          error: 'Authentification requise'
        },
        { status: 401 }
      );
    }

    // Suppression du spectacle
    await deleteShow(params.id);

    return NextResponse.json({
      success: true,
      message: 'Spectacle supprimé avec succès'
    });
  } catch (error) {
    console.error('Erreur lors de la suppression du spectacle:', error);
    
    return NextResponse.json(
      {
        success: false,
        error: 'Erreur lors de la suppression du spectacle'
      },
      { status: 500 }
    );
  }
}

🎯 Server Components vs Client Components

Server Components (par défaut)

Les Server Components s'exécutent côté serveur et sont parfaits pour :

  • Récupération de données - Accès direct aux bases de données
  • Rendu statique - Contenu qui ne change pas souvent
  • SEO - Contenu indexable par les moteurs de recherche
tsx
// app/shows/page.tsx (Server Component)
import { getShows } from '@/lib/shows';
import { EventList } from '@/components/EventList';

export default async function ShowsPage() {
  // Cette fonction s'exécute côté serveur
  const shows = await getShows();
  
  return (
    <div>
      <h1>Nos spectacles</h1>
      <EventList events={shows} />
    </div>
  );
}

Client Components

Les Client Components s'exécutent côté navigateur et sont nécessaires pour :

  • Interactivité - Gestion des événements utilisateur
  • État local - useState, useEffect, etc.
  • APIs du navigateur - localStorage, window, etc.
tsx
// components/EventForm.tsx (Client Component)
'use client';

import { useState } from 'react';

export function EventForm() {
  const [formData, setFormData] = useState({
    title: '',
    description: '',
    date: ''
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    try {
      const response = await fetch('/api/shows', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });

      if (response.ok) {
        // Gérer le succès
        alert('Spectacle créé avec succès !');
      }
    } catch (error) {
      console.error('Erreur:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Champs du formulaire */}
    </form>
  );
}

Mélange Server et Client

tsx
// app/admin/page.tsx (Server Component)
import { getStats } from '@/lib/admin';
import { AdminDashboard } from '@/components/AdminDashboard';

export default async function AdminPage() {
  // Récupération des données côté serveur
  const stats = await getStats();
  
  return (
    <div>
      <h1>Tableau de bord administrateur</h1>
      {/* Passer les données au composant client */}
      <AdminDashboard initialStats={stats} />
    </div>
  );
}

// components/AdminDashboard.tsx (Client Component)
'use client';

import { useState, useEffect } from 'react';

interface AdminDashboardProps {
  initialStats: any;
}

export function AdminDashboard({ initialStats }: AdminDashboardProps) {
  const [stats, setStats] = useState(initialStats);
  const [loading, setLoading] = useState(false);

  // Mise à jour en temps réel
  useEffect(() => {
    const interval = setInterval(async () => {
      setLoading(true);
      try {
        const response = await fetch('/api/admin/stats');
        const newStats = await response.json();
        setStats(newStats.data);
      } catch (error) {
        console.error('Erreur lors de la mise à jour des stats:', error);
      } finally {
        setLoading(false);
      }
    }, 30000); // Mise à jour toutes les 30 secondes

    return () => clearInterval(interval);
  }, []);

  return (
    <div>
      {loading && <div>Mise à jour...</div>}
      {/* Affichage des statistiques */}
    </div>
  );
}

🚀 Optimisations de performance

Génération statique

tsx
// app/shows/[id]/page.tsx
export async function generateStaticParams() {
  // Générer les pages statiques pour les spectacles populaires
  const popularShows = await getPopularShows();
  
  return popularShows.map((show) => ({
    id: show.id,
  }));
}

// Revalidation toutes les heures
export const revalidate = 3600;

Lazy loading des composants

tsx
// app/admin/page.tsx
import dynamic from 'next/dynamic';

// Charger le composant AdminPanel seulement quand nécessaire
const AdminPanel = dynamic(() => import('@/components/AdminPanel'), {
  loading: () => <div>Chargement de l'admin...</div>,
  ssr: false // Désactiver le rendu côté serveur
});

// Charger le composant Chart avec un délai
const Chart = dynamic(() => import('@/components/Chart'), {
  loading: () => <div>Chargement du graphique...</div>,
  ssr: false
});

export default function AdminPage() {
  const [showAdmin, setShowAdmin] = useState(false);
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowAdmin(!showAdmin)}>
        {showAdmin ? 'Masquer' : 'Afficher'} l'admin
      </button>
      
      {showAdmin && <AdminPanel />}
      
      <button onClick={() => setShowChart(!showChart)}>
        {showChart ? 'Masquer' : 'Afficher'} le graphique
      </button>
      
      {showChart && <Chart />}
    </div>
  );
}

Optimisation des images

tsx
// components/EventImage.tsx
import Image from 'next/image';

interface EventImageProps {
  event: {
    id: string;
    title: string;
    imageUrl?: string;
  };
}

export function EventImage({ event }: EventImageProps) {
  return (
    <div className="relative aspect-video overflow-hidden rounded-lg">
      {event.imageUrl ? (
        <Image
          src={event.imageUrl}
          alt={event.title}
          fill
          className="object-cover"
          sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
          priority={false}
          placeholder="blur"
          blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
        />
      ) : (
        <div className="w-full h-full bg-gradient-to-br from-blue-400 to-purple-600 flex items-center justify-center">
          <span className="text-white text-4xl">🎭</span>
        </div>
      )}
    </div>
  );
}

🧪 Tests et qualité

Tests des pages

tsx
// __tests__/pages/shows.test.tsx
import { render, screen } from '@testing-library/react';
import ShowsPage from '@/app/shows/page';

// Mock de la fonction getShows
jest.mock('@/lib/shows', () => ({
  getShows: jest.fn()
}));

describe('ShowsPage', () => {
  it('should render shows list correctly', async () => {
    const mockShows = [
      { id: '1', title: 'Show 1', description: 'Description 1' },
      { id: '2', title: 'Show 2', description: 'Description 2' }
    ];

    // Mock de la réponse
    const { getShows } = require('@/lib/shows');
    getShows.mockResolvedValue(mockShows);

    // Rendu de la page
    const page = await ShowsPage();
    render(page);

    expect(screen.getByText('Nos spectacles')).toBeInTheDocument();
    expect(screen.getByText('Show 1')).toBeInTheDocument();
    expect(screen.getByText('Show 2')).toBeInTheDocument();
  });
});

🚀 Bonnes pratiques

Do's

  • Utiliser les Server Components par défaut pour les données statiques
  • Séparer les responsabilités entre Server et Client Components
  • Optimiser les images avec le composant Image de Next.js
  • Gérer les erreurs avec notFound() et error.tsx
  • Utiliser la génération statique quand c'est possible

Don'ts

  • Abuser des Client Components - Utiliser les Server Components quand possible
  • Oublier la gestion d'erreurs - Toujours gérer les cas d'erreur
  • Ignorer le SEO - Utiliser les métadonnées appropriées
  • Négliger les performances - Optimiser avec le lazy loading

🚀 Prochaines étapes


Next.js est le cœur de notre application. Maîtrisez l'App Router pour créer des expériences utilisateur exceptionnelles ! 🚀

Released under the MIT License.