Files
cv-maker/docs/conception/02-architecture-technique.md
T
2026-09-16 21:34:09 +02:00

216 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 : `<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/`
```
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..5 (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-5)
│ └── 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`) |