11 KiB
CV-Maker — Documentation technique
Application web de création de CV en temps réel : saisie Markdown dans un panneau latéral, preview A4 instantanée, sauvegarde locale automatique et export PDF — le tout sans backend, 100 % côté navigateur.
Sommaire
- Prérequis
- Installation et lancement
- Scripts
- Stack technique
- Structure du projet
- Modèle de données
- Fonctionnement clé
- Conception détaillée
- Limitations connues
Prérequis
| Outil | Version minimale |
|---|---|
| Node.js | 20.9+ |
| pnpm | 12.x (corepack enable puis pnpm -v — figé via packageManager) |
Installation et lancement
# 1. Installer les dépendances
pnpm install
# 2. Lancer en développement
pnpm dev # → http://localhost:3000
# 3. Construire + servir en production
pnpm build
pnpm start
Scripts
| Script | Commande | Description |
|---|---|---|
dev |
pnpm dev |
Serveur de développement (Turbopack) |
dev:sw |
concurrently … |
Dev + re-build du service worker en continu (--watch) |
build |
next build && serwist build |
Build de production + génération du service worker |
start |
pnpm start |
Lancement du build de production |
lint |
pnpm lint |
Vérification ESLint (Next.js) |
typecheck |
tsc --noEmit |
Validation TypeScript strict |
Stack technique
| Brique | Choix | Rôle |
|---|---|---|
| Framework | Next.js (App Router) | Routage, composants serveur/client, métadonnées |
| Langage | TypeScript (strict) | Typage du modèle de données |
| Styles | Tailwind CSS | Design tokens, responsive, print |
| Markdown | marked |
Conversion Markdown → HTML |
| Sécurité | dompurify |
Assainissement du HTML avant injection (anti-XSS) |
| Persistance | localStorage |
Données locales, aucune transmission |
| PWA | @serwist/next + @serwist/cli |
Service worker (mode configurator), manifest, installabilité |
| Icônes | lucide-react |
Iconographie |
| Conteneurisation | Docker (multi-stage) | Image de production, distribution Docker Hub |
Structure du projet
cv-maker/
├── app/
│ ├── layout.tsx # Layout racine (lang, meta, manifest)
│ ├── page.tsx # Éditeur principal
│ ├── globals.css # Tokens Tailwind + @media print
│ ├── documentation/
│ │ └── page.tsx # Page d'aide (statique)
│ ├── mentions-legales/
│ │ └── page.tsx # Page Mentions légales (statique)
│ └── sw.ts # Source du service worker (Serwist)
├── components/
│ ├── ui/ # Primitives réutilisables
│ │ ├── Button.tsx
│ │ ├── IconButton.tsx
│ │ ├── Textarea.tsx
│ │ ├── Tabs.tsx
│ │ └── Footer.tsx # Pied de page (lien Mentions légales)
│ └── cv/ # Composants métier du CV
│ ├── CvWorkspace.tsx # Assemblage 2 colonnes + toolbar
│ ├── CvToolbar.tsx # Barre d'app (PDF, JSON, liens)
│ ├── CvInputPanel.tsx # Panneau gauche (tabs + expériences)
│ ├── CvTabs.tsx # Onglets 1..6
│ ├── MarkdownTextarea.tsx # Textarea contrôlé
│ ├── ExperienceManager.tsx # Formulaire + liste expériences
│ ├── ExperienceItem.tsx # Ligne « n. titre [Modifier] [×] »
│ ├── CvPreview.tsx # Feuille A4
│ ├── CvHeader.tsx # Accroche (zone 1)
│ ├── CvLeftColumn.tsx # Sections 2→6 (1/3)
│ ├── CvSection.tsx # Section générique
│ ├── CvRightColumn.tsx # Expériences pro (2/3)
│ └── CvExperiencePreview.tsx
├── lib/
│ ├── types/cv.ts # Contracts du domaine
│ ├── markdown.ts # markdownToHtml (marked + DOMPurify)
│ ├── storage.ts # Adapter localStorage
│ └── hooks/
│ ├── useLocalStorage.ts # Persistance générique
│ └── useCVData.ts # Façade métier d'édition
├── public/
│ ├── manifest.webmanifest # Manifest PWA
│ └── icons/ # icônes 192 / 512
├── Dockerfile # Image multi-stage (deps/builder/runner)
├── docker-compose.yml # Service cv-maker, healthcheck
├── .dockerignore # Exclusions de contexte build
├── serwist.config.mjs # Config service worker (mode configurator)
├── next.config.ts # output: "standalone"
├── tsconfig.json # strict
├── GOAL.md # Cahier des charges source
└── README.md # Ce document
Modèle de données
Contenu stocké dans une seule clé localStorage : cv-maker:v1 (JSON).
interface CVCareerData {
version: 1;
accroche: string; // onglet 1 — message d'accroche
etatCivil: string; // onglet 2 — état civil + contacts + site + GitHub
formations: string; // onglet 3 — formations
competences: string; // onglet 4 — compétences techniques
softSkills: string; // onglet 5 — soft skills
centresInteret: string; // onglet 6 — centres d'intérêt
experiences: Experience[];
}
interface Experience {
id: string; // crypto.randomUUID() — identifiant stable
titre: string; // titre affiché (liste + section CV)
contenu: string; // corps en Markdown
createdAt: number;
updatedAt: number;
}
- Les 6 zones sont du Markdown brut (le HTML rendu n'est jamais stocké).
- Les expériences sont affichées dans l'ordre de saisie : la 1ʳᵉ ajoutée en haut de la section « Expériences professionnelles », la suivante en dessous, etc. (la 1ʳᵉ porte le numéro 1).
- Sauvegarde invalide/corrompue → état par défaut, sans crash.
Fonctionnement clé
Rendu Markdown sécurisé
lib/markdown.ts — toute injection HTML passe par ici :
const rawHtml = marked.parse(source, { async: false }) as string;
return DOMPurify.sanitize(rawHtml);
marked génère le HTML ; DOMPurify neutralise tout script/événement URL dangereuse avant le dangerouslySetInnerHTML. Aucun HTML brut saisi n'est injecté directement.
Persistance locale
useLocalStorage synchronise l'état React avec localStorage à chaque changement (via useEffect) ; l'enregistrement est silencieux (aucun indicateur affiché). useCVData expose la façade d'édition (setText, addExperience, updateExperience, removeExperience, resetCV, importCV), et la barre d'application permet l'export/import JSON (cv-maker.json) pour partager un CV entre appareils.
Gestion des expériences
- Bouton + : nouvelle expérience (formulaire titre + contenu).
- Liste numérotée avec Modifier / × (suppression confirmée).
- Identifiants UUID : jamais d'index comme identifiant (robustesse lors des suppressions).
Impression PDF
- Bouton « Imprimer / PDF » →
window.print(). - CSS
@media printdansapp/globals.css:@page { size: A4; margin: 0 }, panneau de saisie et toolbar masqués, seule la feuille A4 est imprimée.
PWA
- Mode « configurator » (
@serwist/next+@serwist/cli, compatible Turbopack) :serwist.config.mjsdécrit le service worker, compilé parpnpm buildaprès Next.js (next build && serwist build). public/sw.jspré-cache les routes/,/documentation,/mentions-legaleset tous les assets (787 kB, 20 URL).- En développement le service worker est désactivé (
SerwistProvider disable) ; utiliserpnpm dev:swpour le reconstruire à chaud. public/manifest.webmanifest+ icônes 192/512 → installable, fonctionne hors-ligne (les données du CV restent dans localStorage).
Dockerisation
L'image est construite en 3 étapes (Dockerfile multi-stage) :
| Étape | Image | Contenu |
|---|---|---|
deps |
node:20-alpine |
Installation déterministe des dépendances (pnpm install --frozen-lockfile, incl. pnpm-workspace.yaml) |
builder |
node:20-alpine |
Compilation (pnpm build, sortie standalone + sw.js) |
runner |
node:20-alpine |
Exécution uniquement (node server.js, image ≈ 60 Mo) |
Build et lancement :
docker build -t cv-maker:latest .
docker run --rm -p 3001:3000 cv-maker:latest # → http://localhost:3001
Si le port 3000 est déjà occupé en local, mapper un autre port hôte (
-p 3001:3000).
Avec Docker Compose :
docker compose up -d
docker compose down
Publication sur Docker Hub :
docker login
docker tag cv-maker:latest <mon-user-dockerhub>/cv-maker:latest
docker push <mon-user-dockerhub>/cv-maker:latest
Voir la documentation détaillée Docker pour le contenu complet des fichiers (Dockerfile, docker-compose.yml, .dockerignore) et la recette de validation.
Conception détaillée
Le dossier de conception complet (analyse fonctionnelle, architecture, modèle de données, UI, plan de réalisation pas-à-pas, sécurité/RGPD, plan de tests) se trouve dans :
docs/conception/
├── README.md → sommaire et ordre de lecture
├── 01-analyse-fonctionnelle.md
├── 02-architecture-technique.md
├── 03-modele-donnees.md
├── 04-specifications-ui.md
├── 05-etapes-realisation.md
├── 06-securite-rgpd.md
├── 07-plan-tests.md
└── 08-deploiement-docker.md
Limitations connues
- Desktop first : tablette/mobile = niveau « utilisable », non optimisé.
- One page A4 à l'impression : feuille de preview à hauteur flexible ; un contenu très long peut générer plusieurs pages PDF.
- Pas de tests automatisés (v1) : recette manuelle documentée dans
docs/conception/07-plan-tests.md; tests Vitest prévus en v2. - Pas de drag & drop de réorganisation des expériences (évolution prévue).