# CLAUDE.md — générateur de portfolios

App Nuxt (Nuxt 3 + Nuxt UI v3) qui lit les projets du CMS Grav de Figures Libres
et compose des portfolios **A3 paysage** exportés en PDF, fidèles au gabarit
`book_v3`. Ce dépôt est **autonome et déployable seul** (son `Dockerfile` est ici).

## Tout tourne dans Docker

Node, build Nuxt, Playwright/Chromium, Ghostscript, ffmpeg, rsync — **rien sur
l'hôte**. En dev, l'app est lancée via le dépôt d'orchestration `portfolio-stack`
(`docker compose up -d`), qui monte ce dossier en volume sur le port 8091.

```bash
# depuis le dépôt stack (ce dossier cloné en ./generator) :
docker compose run --rm --no-deps generator npm install   # dépendances
docker compose exec generator npm run build               # build de prod (.output)
docker logs -f figureslibres-portfolio-generator          # logs du dev server
```

Mot de passe dev : `figureslibres` (`PORTFOLIO_PASSWORD`).

## Architecture

- `server/lib/` — le cœur :
  - `content-source.ts` : abstrait la source de contenu. Mode `dev` = lecture
    directe du FS Grav monté en `:ro` ; mode `prod` = cache alimenté par rsync/SSH.
  - `grav-reader.ts` : parse les pages Grav (frontmatter YAML, `media_order`,
    taxonomies) en `Project` (voir `types/index.ts`).
  - `pdf-pipeline.ts` : rend `/print/<id>` via Playwright/Chromium en A3 paysage
    (1190,55 × 841,89 pt), images à 2× (sharp), post-traitement Ghostscript, métadonnées.
  - `portfolio-store.ts` : CRUD des portfolios (JSON sur disque), gel/dégel du contenu.
  - `media.ts`, `session.ts`.
- `server/api/` — endpoints (auth, projects, portfolios, media, sync, exports, public).
- `components/templates/` — pages du gabarit (cover, toc, projet, grille, page
  libre, équipe `TeamTemplate`, conclusion `OutroTemplate`).
- `components/preview/` — `PageRenderer` (dispatch) + `PageViewport` (aperçu à l'échelle).
- `components/composer/` — UI de composition (curation médias, fonds, pickers).
- `composables/useTeam.ts` — roster statique du collectif (`TEAM_ROSTER` : nom,
  portrait, rôle et présentation par défaut de chaque membre) ; les portraits
  sont servis en statique depuis `public/portraits/`.
- `pages/print/[id].vue` — **rendu print servant à la génération PDF** (ne pas casser).
- `types/index.ts` — modèle de données complet et commenté (source de vérité du schéma).

## Modèle de données (résumé)

Un `Portfolio` = `cover` (CoverConfig) + `pages[]`. Types de page :
- `cover` — couverture (titre, texte libre, date, site/mail, adresse en bas à
  gauche) ; `showYears` (au niveau du portfolio) masque les années des projets.
- `toc` — sommaire, généré depuis les pages projet (hérite du filet de la couverture).
- `project` — couverture d'un projet, disposition `band` (bandeau haut) **ou**
  `split` (colonne texte + image pleine hauteur à droite).
- `project-grid` — grille d'images entières.
- `free` — page libre : 1 à 3 **colonnes** (`FreeColumn`) de texte (markdown) ou
  d'images, chacune avec sous-titre optionnel (les champs `body`/`media` legacy
  sont migrés vers `columns` à l'ouverture, avec repli au rendu disque).
- `team` — page équipe : membres du roster en grille (≤ 3 colonnes, équilibrée),
  portrait + nom + rôle + présentation ; sélection et surcharges par membre.
- `outro` — page de conclusion : « Merci ! » centré + rappel des contacts.

Le contenu Grav (`Project`, `MediaAsset`) est **en lecture seule**. À la
génération, le contenu utilisé est **figé** dans `data/frozen/<id>/`
(`FrozenSnapshot`) : régénérer redonne le même PDF ; « Réactualiser depuis Grav »
resynchronise volontairement.

Chaque page porte un **fond unifié** `background` à trois modes :
`white` | `tint` (aplat beige fixe `#efece3`) | `full` (la page — ou le bandeau /
la colonne d'une couverture projet — prend la couleur du filet `bandColor`, défaut
encre `FL_INK`). Il n'y a **plus** de couleur automatique par catégorie
(`bandColorOf`/`stripColorOf` retombent sur `FL_INK`).

## Contraintes fermes

- Le contenu Grav n'est jamais modifié par l'app (monté `:ro` côté stack).
- Les images ne sont **jamais tronquées** dans les grilles (calées entières).
- Les vidéos ne sont pas sélectionnables dans les grilles.
- Format PDF : A3 paysage aux dimensions exactes du gabarit.

## Conventions de rendu (templates)

- **Fond `background`** : `useTheme` → `backgroundColorOf(bg, fullColor)` /
  `backgroundStyle(bg, fullColor)`. Le mode `full` **dérive** sa couleur du filet
  au rendu (`fullColor`) — ne rien stocker en dur ; le `BackgroundEditor` (Blanc /
  Teinté / Plein) est le même sur tous les types de page.
- **Syne toujours en Regular** (poids 400), y compris titres et gras de prose ;
  seule exception : le mot « Figures Libres » de la couverture (Bold).
- **Liens Grav** : `stripTargetParam` retire le `?target=_blank` que Grav accole à
  chaque URL (lien projet + liens dans les corps de texte), à l'affichage.
- Filet : `--fl-strip` (5 mm, couverture/sommaire/page libre/équipe/conclusion) ou
  `--fl-strip-light` (2 mm, filet fin des grilles et des couvertures projet non « plein »).

## Pièges Chromium print (avant de toucher aux templates)

- **`columns-*` (CSS multicol) = page PDF blanche** dans Chromium print. Ne pas
  utiliser de colonnes CSS dans les templates ; vérifier chaque template via `pdftoppm`.
- **Webfonts en `font-display: block` = texte absent du PDF**. Le pipeline
  réinvalide les styles avant `page.pdf()` — ne pas retirer cette étape.

## Données

Tout est sur disque dans `data/` (sauvegarde = copie du dossier), git-ignoré :
`portfolios/*.json`, `exports/*.pdf`, `frozen/<id>/`, `cache/` (régénérable),
`content-cache/` (rsync prod).
