Générateur de portfolios PDF Figures Libres (app Nuxt autonome)

App Nuxt 3 + Nuxt UI qui lit les projets du CMS Grav et compose des portfolios
A3 paysage exportés en PDF (gabarit book_v3) : composeur WYSIWYG, templates
cover/toc/projet/grille/page libre, pipeline Playwright + Ghostscript, gel du
contenu à la génération, partage public opt-in.

Dépôt autonome : Dockerfile (base Playwright + Ghostscript + ffmpeg + rsync) inclus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 18:37:09 +02:00
co-authored by Claude Opus 4.8
commit 59dfbe4155
79 changed files with 18587 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
# 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).
- `components/preview/` — `PageRenderer` (dispatch) + `PageViewport` (aperçu à l'échelle).
- `components/composer/` — UI de composition (curation médias, fonds, pickers).
- `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`,
`toc`, `project` (couverture bandeau + image), `project-grid` (grille d'images
entières), `free`. 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.
## 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.
## 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).