Cours Next.js · Module 8: Déploiement et production

Projet : déploiement complet d'une application Next.js en production

7 min de lecture
Dans cette leçon10

Bienvenue dans le projet final du module de déploiement ! Dans la Métropole Quantum 2150, chaque système passe par un processus rigoureux de préparation à la production - du build et de l'optimisation, en passant par la configuration du SEO, jusqu'au monitoring et aux tests. Dans ce projet, nous allons combiner toutes les compétences acquises tout au long du module pour parcourir le cycle complet de déploiement d'une application Next.js.

Objectif du projet

Ta mission est de préparer une application Next.js complète au déploiement en production. Le projet couvre tous les aspects clés que nous avons abordés dans ce module :

  1. Build et optimisation (de la leçon sur build et analyze)
  2. SEO et métadonnées (de la leçon sur les métadonnées dynamiques)
  3. Open Graph et social sharing (de la leçon sur l'OG)
  4. Sitemap et robots.txt (de la leçon sur les sitemaps)
  5. Web Analytics (de la leçon sur l'analytics)
  6. Déploiement sur les plateformes d'hébergement (de la leçon sur Vercel/Netlify)
  7. Variables d'environnement et configuration de production (de la leçon sur les variables d'environnement)
  8. CI/CD (de la leçon sur l'automatisation)
  9. Monitoring (de la leçon sur le monitoring)
  10. PWA et Service Workers (de la leçon sur les PWA)
  11. Preview Deployments et staging (de la leçon sur les environnements)

Étape 1 : Configuration de next.config.js

Commence par une configuration complète de Next.js avec des optimisations de production :

1// next.config.ts
2import type { NextConfig } from 'next';
3import withBundleAnalyzer from '@next/bundle-analyzer';
4
5const config: NextConfig = {
6  // Optimisation des images
7  images: {
8    formats: ['image/avif', 'image/webp'],
9    remotePatterns: [
10      {
11        protocol: 'https',
12        hostname: 'cdn.quantum-city.com',
13        pathname: '/images/**',
14      },
15    ],
16    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 jours de cache
17  },
18
19  // Compression
20  compress: true,
21
22  // Optimisation du bundle
23  experimental: {
24    optimizePackageImports: ['lucide-react', '@quantum/ui'],
25  },
26
27  // En-têtes de sécurité
28  async headers() {
29    return [
30      {
31        source: '/:path*',
32        headers: [
33          { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
34          { key: 'X-Content-Type-Options', value: 'nosniff' },
35          { key: 'X-XSS-Protection', value: '1; mode=block' },
36          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
37          {
38            key: 'Strict-Transport-Security',
39            value: 'max-age=63072000; includeSubDomains; preload',
40          },
41          {
42            key: 'Permissions-Policy',
43            value: 'camera=(), microphone=(), geolocation=()',
44          },
45        ],
46      },
47    ];
48  },
49
50  // Redirections
51  async redirects() {
52    return [
53      {
54        source: '/old-page',
55        destination: '/new-page',
56        permanent: true, // 301 redirect
57      },
58    ];
59  },
60};
61
62const withAnalyzer = withBundleAnalyzer({
63  enabled: process.env.ANALYZE === 'true',
64});
65
66export default withAnalyzer(config);

Étape 2 : SEO et métadonnées dynamiques

Configure les métadonnées de chaque page en utilisant la Metadata API :

1// app/layout.tsx
2import type { Metadata } from 'next';
3
4export const metadata: Metadata = {
5  metadataBase: new URL('https://quantum-city.com'),
6  title: {
7    default: 'Quantum City - Portal Metropolii',
8    template: '%s | Quantum City',
9  },
10  description: 'Oficjalny portal Metropolii Quantum 2150',
11  openGraph: {
12    type: 'website',
13    locale: 'pl_PL',
14    url: 'https://quantum-city.com',
15    siteName: 'Quantum City',
16    images: [
17      {
18        url: '/og-default.png',
19        width: 1200,
20        height: 630,
21        alt: 'Quantum City',
22      },
23    ],
24  },
25  twitter: {
26    card: 'summary_large_image',
27    creator: '@QuantumCity',
28  },
29  robots: {
30    index: true,
31    follow: true,
32    googleBot: {
33      index: true,
34      follow: true,
35      'max-video-preview': -1,
36      'max-image-preview': 'large',
37      'max-snippet': -1,
38    },
39  },
40};
41
42// app/products/[id]/page.tsx - métadonnées dynamiques
43import type { Metadata } from 'next';
44
45type Props = { params: Promise<{ id: string }> };
46
47export async function generateMetadata({ params }: Props): Promise<Metadata> {
48  const product = await fetch(
49    `https://api.quantum-city.com/products/${(await params).id}`
50  ).then(r => r.json());
51
52  return {
53    title: product.name,
54    description: product.description,
55    openGraph: {
56      title: product.name,
57      description: product.description,
58      images: [{ url: product.imageUrl, width: 800, height: 600 }],
59    },
60  };
61}

Étape 3 : Sitemap et robots.txt

Génère un sitemap dynamique :

1// app/sitemap.ts
2import { MetadataRoute } from 'next';
3
4export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
5  const baseUrl = 'https://quantum-city.com';
6
7  // Récupère les pages dynamiques
8  const products = await fetch('https://api.quantum-city.com/products')
9    .then(r => r.json());
10
11  const posts = await fetch('https://api.quantum-city.com/blog')
12    .then(r => r.json());
13
14  const productPages = products.map((product: any) => ({
15    url: `${baseUrl}/products/${product.slug}`,
16    lastModified: new Date(product.updatedAt),
17    changeFrequency: 'weekly' as const,
18    priority: 0.8,
19  }));
20
21  const blogPages = posts.map((post: any) => ({
22    url: `${baseUrl}/blog/${post.slug}`,
23    lastModified: new Date(post.updatedAt),
24    changeFrequency: 'monthly' as const,
25    priority: 0.6,
26  }));
27
28  return [
29    { url: baseUrl, lastModified: new Date(), changeFrequency: 'daily', priority: 1 },
30    { url: `${baseUrl}/products`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.9 },
31    ...productPages,
32    ...blogPages,
33  ];
34}
35
36// app/robots.ts
37import { MetadataRoute } from 'next';
38
39export default function robots(): MetadataRoute.Robots {
40  return {
41    rules: [
42      {
43        userAgent: '*',
44        allow: '/',
45        disallow: ['/admin/', '/api/', '/dashboard/'],
46      },
47    ],
48    sitemap: 'https://quantum-city.com/sitemap.xml',
49  };
50}

Étape 4 : Web Analytics et monitoring

Intègre un système d'analytics et de monitoring des erreurs :

1// lib/analytics.ts
2export function trackEvent(name: string, properties?: Record<string, any>) {
3  if (typeof window === 'undefined') return;
4
5  // Analytics maison
6  fetch('/api/analytics', {
7    method: 'POST',
8    headers: { 'Content-Type': 'application/json' },
9    body: JSON.stringify({ event: name, properties, timestamp: Date.now() }),
10  }).catch(console.error);
11}
12
13// lib/monitoring.ts
14export function initErrorMonitoring() {
15  if (typeof window === 'undefined') return;
16
17  window.addEventListener('error', (event) => {
18    fetch('/api/errors', {
19      method: 'POST',
20      headers: { 'Content-Type': 'application/json' },
21      body: JSON.stringify({
22        message: event.message,
23        filename: event.filename,
24        lineno: event.lineno,
25        stack: event.error?.stack,
26        url: window.location.href,
27        timestamp: new Date().toISOString(),
28      }),
29    }).catch(console.error);
30  });
31}

Étape 5 : Pipeline CI/CD

Configure un pipeline de déploiement automatique :

1# .github/workflows/deploy.yml
2name: Deploy to Production
3
4on:
5  push:
6    branches: [main]
7
8jobs:
9  test:
10    runs-on: ubuntu-latest
11    steps:
12      - uses: actions/checkout@v4
13      - uses: actions/setup-node@v4
14        with:
15          node-version: '20'
16          cache: 'npm'
17      - run: npm ci
18      - run: npm run lint
19      - run: npm run type-check
20      - run: npm test
21
22  e2e:
23    needs: test
24    runs-on: ubuntu-latest
25    steps:
26      - uses: actions/checkout@v4
27      - uses: actions/setup-node@v4
28        with:
29          node-version: '20'
30      - run: npm ci
31      - run: npm run build
32      - name: Cypress E2E Tests
33        uses: cypress-io/github-action@v6
34        with:
35          start: npm start
36          wait-on: 'http://localhost:3000'
37          browser: chrome
38
39  deploy:
40    needs: [test, e2e]
41    runs-on: ubuntu-latest
42    steps:
43      - uses: actions/checkout@v4
44      - name: Deploy to Vercel
45        run: npx vercel --prod --token=VERCEL_TOKEN

Étape 6 : PWA et support offline

Ajoute la fonctionnalité Progressive Web App :

1// app/manifest.ts
2import { MetadataRoute } from 'next';
3
4export default function manifest(): MetadataRoute.Manifest {
5  return {
6    name: 'Quantum City Portal',
7    short_name: 'QC Portal',
8    description: 'Oficjalny portal Metropolii Quantum 2150',
9    start_url: '/',
10    display: 'standalone',
11    background_color: '#0a0a1a',
12    theme_color: '#64ffda',
13    icons: [
14      { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
15      { src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
16    ],
17  };
18}

Étape 7 : Tests E2E avec Cypress

Écris des tests couvrant les parcours utilisateur critiques :

1// cypress/e2e/production-readiness.cy.ts
2describe('Production Readiness Check', () => {
3  it('la page d\'accueil se charge en moins de 3 secondes', () => {
4    cy.visit('/', { timeout: 3000 });
5    cy.get('h1').should('be.visible');
6  });
7
8  it('les meta tags SEO sont corrects', () => {
9    cy.visit('/');
10    cy.get('head title').should('contain', 'Quantum City');
11    cy.get('meta[name="description"]').should('have.attr', 'content');
12  });
13
14  it('les en-têtes de sécurité sont définis', () => {
15    cy.request('/').then((response) => {
16      expect(response.headers).to.have.property('x-frame-options');
17      expect(response.headers).to.have.property('x-content-type-options');
18    });
19  });
20});

Étape 8 : Checklist avant le déploiement

Avant chaque déploiement en production, passe en revue ces points de contrôle :

Performance

  • La commande next build se termine sans erreurs
  • La taille du bundle JS est inférieure à 200 KB (gzippée)
  • LCP inférieur à 2.5 secondes, CLS inférieur à 0.1
  • Images optimisées (WebP/AVIF)

SEO

  • Chaque page a un title et une description uniques
  • Les tags Open Graph sont corrects
  • Sitemap.xml et robots.txt sont générés

Sécurité

  • En-têtes de sécurité (HSTS, CSP, X-Frame-Options)
  • Protection CSRF et rate limiting
  • Validation de l'input avec Zod

Monitoring

  • Suivi des erreurs configuré
  • Web Vitals monitorés
  • Alertes sur les erreurs critiques

CI/CD

  • Les tests unitaires et E2E passent
  • Linting et type-checking sans erreurs
  • Déploiement automatique après merge dans main

Résumé

Dans ce projet, tu as combiné toutes les compétences clés du déploiement d'une application Next.js en production. Chacun de ces éléments est indispensable pour créer une application web professionnelle, performante et sécurisée.

N'oublie pas - dans la Métropole Quantum 2150, il n'y a pas de place pour les demi-mesures. Chaque système doit être pleinement optimisé, sécurisé et monitoré. Maintenant tu es prêt à déployer tes applications Next.js au plus haut niveau !

Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3// Project: Full production deployment of Next.js application
4
5interface ChecklistItem {
6  id: string;
7  label: string;
8  category: string;
9  done: boolean;
10}
11
12const initialChecklist: ChecklistItem[] = [
13  { id: '1', label: 'next.config.js - image optimization and compression', category: 'Build', done: false },
14  { id: '2', label: 'Security headers (HSTS, CSP, X-Frame-Options)', category: 'Build', done: false },
15  { id: '3', label: 'Metadata API - title, description, OG tags', category: 'SEO', done: false },
16  { id: '4', label: 'Dynamic metadata for subpages', category: 'SEO', done: false },
17  { id: '5', label: 'Sitemap.xml generated', category: 'SEO', done: false },
18  { id: '6', label: 'Robots.txt configured', category: 'SEO', done: false },
19  { id: '7', label: 'Web Analytics integrated', category: 'Monitoring', done: false },
20  { id: '8', label: 'Error monitoring configured', category: 'Monitoring', done: false },
21  { id: '9', label: 'Web Vitals monitored', category: 'Monitoring', done: false },
22  { id: '10', label: 'CI/CD pipeline configured', category: 'Deploy', done: false },
23  { id: '11', label: 'E2E tests (Cypress) passing', category: 'Deploy', done: false },
24  { id: '12', label: 'PWA manifest et Service Worker', category: 'Deploy', done: false },
25  { id: '13', label: 'Deployment on Vercel/Netlify', category: 'Deploy', done: false },
26];
27
28const categoryColors: Record<string, string> = {
29  Build: '#64ffda',
30  SEO: '#bb86fc',
31  Monitoring: '#ff9800',
32  Deploy: '#4caf50',
33};
34
35export default function DeploymentChecklist() {
36  const [items, setItems] = useState(initialChecklist);
37
38  const toggle = (id: string) => {
39    setItems(prev => prev.map(item => item.id === id ? { ...item, done: !item.done } : item));
40  };
41
42  const total = items.length;
43  const done = items.filter(i => i.done).length;
44  const pct = Math.round((done / total) * 100);
45
46  const categories = Array.from(new Set(items.map(i => i.category)));
47
48  return (
49    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: 20, color: '#fff', fontFamily: 'monospace' }}>
50      <h1 style={{ color: '#64ffda', fontSize: 20, marginBottom: 4 }}>Production Deployment Checklist</h1>
51      <p style={{ color: '#888', fontSize: 13, marginBottom: 16 }}>Quantum City - Next.js Deployment</p>
52
53      <div style={{ background: '#151530', borderRadius: 8, padding: 16, marginBottom: 20 }}>
54        <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8, fontSize: 13 }}>
55          <span style={{ color: '#aaa' }}>Progress: {done}/{total}</span>
56          <span style={{ color: pct === 100 ? '#4caf50' : '#64ffda' }}>{pct}%</span>
57        </div>
58        <div style={{ height: 10, background: '#1a2744', borderRadius: 5, overflow: 'hidden' }}>
59          <div style={{ width: pct + '%', height: '100%', background: pct === 100 ? '#4caf50' : '#64ffda', borderRadius: 5, transition: 'width 0.4s ease' }} />
60        </div>
61        {pct === 100 && <p style={{ color: '#4caf50', marginTop: 8, fontSize: 14 }}>Application ready for deployment!</p>}
62      </div>
63
64      {categories.map(cat => (
65        <div key={cat} style={{ marginBottom: 16 }}>
66          <h3 style={{ color: categoryColors[cat] || '#fff', fontSize: 14, marginBottom: 8 }}>{cat}</h3>
67          {items.filter(i => i.category === cat).map(item => (
68            <div key={item.id} onClick={() => toggle(item.id)} style={{
69              display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px', marginBottom: 4,
70              background: item.done ? '#1a3a2a' : '#151530', borderRadius: 6, cursor: 'pointer',
71              border: '1px solid ' + (item.done ? '#4caf5044' : '#334'),
72            }}>
73              <span style={{ fontSize: 16 }}>{item.done ? '\u2705' : '\u2B1C'}</span>
74              <span style={{ color: item.done ? '#888' : '#ccc', fontSize: 13, textDecoration: item.done ? 'line-through' : 'none' }}>
75                {item.label}
76              </span>
77            </div>
78          ))}
79        </div>
80      ))}
81    </div>
82  );
83}

Tu as repéré une erreur dans cette leçon ?

Articles utiles