02 — Architecture technique
1. Stack technique
| Couche |
Choix |
Version cible |
Justification |
| Framework |
Next.js (App Router) |
15.x (React 19) |
Standard actuel ; SSR/SSG natif, metadata, conventions de structure |
| Langage |
TypeScript |
strict mode |
Typage des modèles de données, cohérence du contrat d'état, qualité CDA |
| Styles |
Tailwind CSS |
4.x |
Utility-first, tokens design, @media print piloté par classes |
| Markdown |
marked |
12.x+ |
Rendu Markdown → HTML, léger et synchrone, configurable |
| Assainissement |
dompurify |
3.x |
Neutralise tout contenu HTML non sûr avant injection (anti-XSS) |
| Persistance |
localStorage (natif) |
— |
Aucun backend, données 100 % locales |
| PWA |
@serwist/next (fork maintenu de next-pwa) |
9.x/10.x |
Register + precache des pages statiques, manifest, offline |
| Icônes / UX |
lucide-react |
≥ 0.4 |
Icônes légères, cohérentes, arborescentes |
| Gestion de paquets |
pnpm |
≥ 9 |
Hooks de workspace, rapidité, espace disque (cf. choix pnpm) |
Rendu Markdown côté client : les zones interactives étant des Client Components, marked et dompurify sont importés uniquement dans la couche lib/markdown.ts consommée par les composants client. Le bundle initial des pages statiques ne les embarque volontairement pas.
2. Architecture en couches
Règles de couches
components/ui/ n'importe jamais lib/storage ni les types CV (il est générique).
components/cv/ orchestre la logique (via hooks) et compose les primitives ui/.
lib/ ne dépend jamais des composants ; il est pur (aucun JSX).
- La persistance (
useLocalStorage) est isolée dans lib/hooks/ pour être remplaçable (autre backend plus tard) sans toucher les composants.
- Les pages
/documentation et /mentions-legales sont des Server Components (statiques, aucune interactivité).
3. Répartition client / serveur
| Élément |
Côté |
Raison |
CvWorkspace, panneaux, textareas, preview |
Client ("use client") |
Interactivité temps réel, localStorage, window |
/documentation, /mentions-legales |
Serveur (SSG) |
Contenu statique, zéro JS nécessaire |
layout.tsx, metadata, manifest |
Serveur |
SEO/meta, PWA |
4. Flux de données (vue temps réel)
- Temps réel : chaque frappe provoque une réécriture de l'état React → re-rendu du preview. Pas de debounce bloquant (les textes de CV sont courts) ; un
onChange direct suffit.
- Persistance : synchronisée dans le même cycle (effet sur l'état) ; voir
03-modele-donnees.md.
5. Détail des dossiers Next.js (App Router)
5.1 app/
| Fichier |
Rôle |
app/layout.tsx |
Layout racine : <html lang="fr">, metadata (titre, description), icône, manifest PWA |
app/page.tsx |
Page principale : rend CvWorkspace |
app/documentation/page.tsx |
Contenu Markdown statique (aide d'utilisation) |
app/mentions-legales/page.tsx |
Mentions légales / vie privée |
components/ui/Footer.tsx |
Pied de page (lien Mentions légales, masqué à l'impression) |
app/globals.css |
Tokens Tailwind, styles de base, styles d'impression @media print |
5.2 components/
6. Gestion d'état
- État global minimal : pas de Redux/Zustand nécessaire. Un seul état racine porté par
useCVData (voir 03-) et transmis par props (Composition). Les textareas sont contrôlés par cet état.
- État partagé entre
CvInputPanel et CvPreview via remontée d'état (CvWorkspace).
ExperienceManager gère son état local de navigation (expérience en cours d'édition, « mode ajout »).
7. Impression PDF
- Bouton « Imprimer / PDF » →
window.print().
- Feuille A4 visée via
@media print dans globals.css :
- masquage du panneau gauche et de la barre d'outils (
.print:hidden) ;
- la feuille A4 occupe la page entière :
@page { size: A4; margin: 0 } ;
print-color-adjust: exact pour conserver les couleurs d'accentuations éventuelles.
- Le PDF est produit par le navigateur (option « Enregistrer en PDF »).
8. PWA
- Package :
@serwist/next (plugin Next.js officiel pour PWA).
- Config
next.config.ts : withSerwist({ swSrc, register: true }).
sw.ts : précache des routes /, /documentation, /mentions-legales + assets (icônes, manifest).
public/manifest.webmanifest : name, short_name, start_url: "/", display: "standalone", theme_color, icons (192 et 512 px, PNG).
- Icônes générées dans
public/icons/ (192, 512, masque d'application 1024) — voir 05-.
9. Scripts (package.json)
| Script |
Commande |
dev |
pnpm dev — serveur de développement |
build |
pnpm build — build de production |
start |
pnpm start — démarrage prod local |
lint |
pnpm lint — ESLint (Next.js) |
typecheck |
tsc --noEmit — validation des types |
10. Choix techniques justifiés
- pnpm : rapide, économise l'espace disque (hardlinks), verrouillage strict des dépendances ; documenté de bout en bout dans
05-.
- TypeScript strict : démontre la maîtrise du typage, évite les bugs d'état (union des zones, types des expériences).
- marked + dompurify : le binôme standard pour « Markdown sûr côté navigateur ».
marked ne sanitiase pas nativement → DOMPurify est le garde-fou (voir 06-).
- tailwind
print:hidden / print: : contrôle CSS pur de l'impression PDF sans librairie tierce.
- @serwist/next : successeur maintenu de
next-pwa, typé, pensé pour l'App Router.
11. Déploiement conteneurisé
Pourquoi Docker ?
- Reproductibilité : l'image de production embarque la même version de Node, de pnpm, et les mêmes dépendances compilées.
- Portabilité :
docker run fonctionne sur n'importe quel hôte doté du démon Docker (Linux, macOS, Windows, cloud).
- Distribution via Docker Hub : l'image est tagguée et poussée sur un registre public → n'importe qui peut la tirer et l'exécuter (
docker pull).
Architecture de l'image (multi-stage)
- Le champ
output: "standalone" dans next.config.ts génère un server.js autonome dans .next/standalone → aucun node_modules complet embarqué dans l'image finale.
- Base
node:20-alpine : image runtime minimale, compatible glibc/musl (ce projet n'utilise pas de dépendances natives).
Fichiers associés
| Fichier |
Rôle |
Dockerfile |
3 stages (deps → builder → runner), EXPOSE 3000, HOSTNAME=0.0.0.0 |
.dockerignore |
Exclut node_modules/, .next/, docs/, *.md, .git/, .env* |
docker-compose.yml |
Service cv-maker, build, ports 3000:3000, healthcheck wget |
Note sur output: "standalone"
Ajouter dans next.config.ts (côte à côte avec withSerwist) :
ou bien :
L'ensemble withSerwist + standalone est compatible : le service worker (sw.js) est généré dans public/ lors du build et copié dans l'image finale avec le reste du dossier public/.
Variables d'environnement au runtime
| Variable |
Valeur |
Rôle |
NODE_ENV |
production |
Désactive les warnings React, active les optimisations Next.js |
HOSTNAME |
0.0.0.0 |
Écoute sur toutes les interfaces (sinon localhost uniquement → conteneur inaccessible) |
PORT |
3000 |
Port du serveur Next.js (mappé via -p 3000:3000) |