- Page « équipe » : roster statique du collectif (composables/useTeam), portraits servis depuis public/portraits, grille ≤ 3 colonnes équilibrée (portrait/nom/rôle/présentation), sélection + surcharges par membre (repliables, réordonnables, réinitialisables) dans le composeur - Page « conclusion » : « Merci ! » centré + rappel des contacts - Page libre : 1 à 3 colonnes texte (markdown) ou images avec sous-titres, réordonnables ; migration des champs body/media legacy vers columns - Couverture : texte libre sous l'intro, adresse en bas à gauche (3 lignes), case « afficher les années » au niveau du document - Sommaire : items centrés (self-center), filet en points dégradés stable - Retrait de la dépendance morte pagedjs - Docs à jour (CLAUDE.md, README) ; spec marquée comme archive réalisée
5.7 KiB
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.
# 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. Modedev= lecture directe du FS Grav monté en:ro; modeprod= cache alimenté par rsync/SSH.grav-reader.ts: parse les pages Grav (frontmatter YAML,media_order, taxonomies) enProject(voirtypes/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, équipeTeamTemplate, conclusionOutroTemplate).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 depuispublic/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, dispositionband(bandeau haut) ousplit(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 champsbody/medialegacy sont migrés verscolumnsà 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é
:rocô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 modefulldérive sa couleur du filet au rendu (fullColor) — ne rien stocker en dur ; leBackgroundEditor(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 :
stripTargetParamretire le?target=_blankque 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 viapdftoppm.- Webfonts en
font-display: block= texte absent du PDF. Le pipeline réinvalide les styles avantpage.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).