# 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 ``` ┌────────────────────────────────────────────────────────────┐ │ app/ (Next.js) │ │ layout.tsx · globals.css │ │ page.tsx → CvWorkspace (page principale) │ │ documentation/page.tsx → page statique │ │ mentions-legales/page.tsx → page statique │ ├────────────────────────────────────────────────────────────┤ │ components/ (React, réutilisables) │ │ ui/ → primitives génériques (Button, Tabs, ...) │ │ cv/ → composants métier du CV │ ├────────────────────────────────────────────────────────────┤ │ lib/ (logique pure, testable) │ │ types/cv.ts → types & constantes du domaine │ │ markdown.ts → md → html sanitized │ │ storage.ts → API localStorage + migration │ │ hooks/ → useLocalStorage, useCVData │ ├────────────────────────────────────────────────────────────┤ │ public/ → manifest.webmanifest, icônes, … │ └────────────────────────────────────────────────────────────┘ ``` ### Règles de couches 1. `components/ui/` n'importe **jamais** `lib/storage` ni les types CV (il est générique). 2. `components/cv/` orchestre la logique (via hooks) et compose les primitives `ui/`. 3. `lib/` ne dépend **jamais** des composants ; il est pur (aucun JSX). 4. La persistance (`useLocalStorage`) est isolée dans `lib/hooks/` pour être remplaçable (autre backend plus tard) sans toucher les composants. 5. 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) ``` Frappe utilisateur │ ▼ CvInputPanel (textarea, onChange) │ met à jour l'état global ▼ useCVData (useState + useLocalStorage 🔁) │ ┌───────────────────┐ ├──────── persist ▶ localStorage │ hydration au boot│ │ └───────────────────┘ ▼ MarkdownTextarea / ExperienceManager mettent à jour `data` │ ▼ CvPreview (pure : props données) │ rendu md → html via lib/markdown (marked + DOMPurify) ▼ Feuille A4 (sections positionnées, @media print) ``` - **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 : ``, 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/` ``` components/ ├── ui/ │ ├── Button.tsx → variantes (primary, ghost, danger, icon) │ ├── Tabs.tsx → primitive d'onglets (accessible, contenu piloté) │ ├── Textarea.tsx → textarea générique stylée │ └── IconButton.tsx → bouton icône (Modifier, Supprimer) └── cv/ ├── CvWorkspace.tsx → layout 2 colonnes + barre d'app ├── CvToolbar.tsx → barre d'application (print, export/import JSON, liens) ├── CvInputPanel.tsx → panneau gauche │ ├── CvTabs.tsx → tabs 1..6 (labels + numéros) │ ├── MarkdownTextarea.tsx → {label, value, onChange} (textarea md) │ └── ExperienceManager.tsx │ └── ExperienceItem.tsx → ligne « n. titre [Modifier] [×] » └── CvPreview.tsx → panneau droit (feuille A4) ├── CvHeader.tsx → accroche (zone 1) ├── CvLeftColumn.tsx → 1/3 (zones 2-6) │ └── CvSection.tsx → section générique {titre, html} └── CvRightColumn.tsx → 2/3 (Expériences professionnelles) └── CvExperiencePreview.tsx ``` ## 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) ``` node:20-alpine │ ├─ deps → pnpm install --frozen-lockfile (cache, rapide) ├─ builder → pnpm build (sortie standalone Next.js) └─ runner → node server.js (image finale ≈ 100-150 Mo) ``` - 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`) : ```ts export default withSerwist(nextConfig, { output: "standalone" }); ``` ou bien : ```ts const nextConfig = { output: "standalone" }; export default withSerwist(nextConfig); ``` 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`) |