Files
cv-maker/docs/conception/02-architecture-technique.md

12 KiB
Raw Permalink Blame History

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..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) :

export default withSerwist(nextConfig, { output: "standalone" });

ou bien :

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)