Cours Next.js · Module 8: Déploiement et production
Projet : déploiement complet d'une application Next.js en production
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 :
- Build et optimisation (de la leçon sur build et analyze)
- SEO et métadonnées (de la leçon sur les métadonnées dynamiques)
- Open Graph et social sharing (de la leçon sur l'OG)
- Sitemap et robots.txt (de la leçon sur les sitemaps)
- Web Analytics (de la leçon sur l'analytics)
- Déploiement sur les plateformes d'hébergement (de la leçon sur Vercel/Netlify)
- Variables d'environnement et configuration de production (de la leçon sur les variables d'environnement)
- CI/CD (de la leçon sur l'automatisation)
- Monitoring (de la leçon sur le monitoring)
- PWA et Service Workers (de la leçon sur les PWA)
- 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 buildse 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 ?