216 lines
12 KiB
Markdown
216 lines
12 KiB
Markdown
# 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`) | |