# Spécification technique — Générateur de portfolios PDF (Figures Libres) > Document de spécification v1 — établi le 2026-07-01, révisé le 2026-07-02. > Outil compagnon du site Grav « Figures Libres » permettant de composer et générer > des portfolios PDF à partir des projets saisis dans Grav. --- ## 1. Contexte & objectif Le collectif Figures Libres gère ses projets dans un site **Grav** (v1.7.52). Chaque projet y est déjà décrit (titre, texte, médias, catégorie, tags, date, lien). L'objectif est de **réutiliser ces contenus pour produire des portfolios PDF sur mesure**, sans ressaisie : - sélectionner/désélectionner les projets à inclure ; - ordonner les pages ; - choisir, **pour chaque page**, quel projet est présenté avec quel **template** ; - **curer les médias** affichés page par page (choix + ordre + disposition) ; - intercaler des **pages libres** (couverture, intro, sommaire) personnalisables ; - exporter un **PDF léger et net**, destiné à l'envoi par mail. L'outil est **séparé de Grav** (pas un plugin) : une application web dédiée qui lit le contenu Grav en lecture seule. --- ## 2. Décisions validées (récapitulatif des arbitrages) | Sujet | Décision | |---|---| | Utilisateurs | **Interne au collectif** (quelques utilisateurs de confiance) | | Qualité PDF | **Écran / partage** (RGB, pas de contrainte imprimeur) | | Templates | **Définis en code** (pas d'éditeur visuel de template) | | Composition | **1 projet / page + curation des médias** (sélection + ordre + layout, sans recadrage) | | Sauvegarde | **Oui** : bibliothèque de portfolios rééditables | | Pages non-projet | **Couverture, sommaire, pages libres d'intro** (personnalisables dans l'UI) ; séparateurs/4e de couv optionnels | | Texte des pages | **Auto depuis Grav + override ponctuel par page** | | Format de page | **Fixe, 16:9 « écran » paysage** (voir §9.4) | | Fraîcheur des données | **Live + figé à la génération** (le portfolio archive le contenu utilisé) | | Sections source | **Projets uniquement** (`user/pages/02.projets`) | | Champs exploités | Titre, texte, médias, catégorie/tags, date, lien externe | | Médias | UI en basse résolution ; génération en pleine résolution ; **post-traitement du PDF** pour redimensionner chaque image à sa taille d'affichage | | Personnalisation à la génération | Titre/sous-titre couverture, nom du fichier PDF ; couverture + pages libres personnalisables | | Langue | **FR uniquement** | | Fonds de page | **Blanc / teinté / pleine couleur** (prop `background`), dès la v1 | | Direction artistique | **Sobre** : pas de formes graphiques décoratives ni d'effets « fluo » | | Stack | **Nuxt 3 + Nuxt UI + Playwright (Chromium) + Paged.js** | | Auth | **Mot de passe partagé simple** | | Stockage | **Fichiers JSON + PDF sur disque** (volume Docker) | | Déploiement | **Serveur perso (Docker + Playwright)**, contenu tiré de la prod en **rsync/SSH lecture seule** | | Synchro | **Bouton manuel « Rafraîchir »** | | Templates de départ | **« Visuel dominant + texte »** et **« Grille / planche »** ; layouts d'images variables (§9.5) | | Références d'inspiration | 2 books dans `docs/portfolios-examples/` — **inspiration non-normative** (§17) | --- ## 3. Utilisateurs & cas d'usage **Utilisateur type** : un membre du collectif préparant un portfolio pour une candidature, un appel d'offres ou un envoi client. **Parcours principal :** 1. Se connecter (mot de passe partagé). 2. (Optionnel) Cliquer **« Rafraîchir »** pour re-synchroniser le contenu depuis la prod. 3. Créer un nouveau portfolio (ou dupliquer un existant). 4. Parcourir la **bibliothèque de projets**, filtrer par catégorie/tag, cocher ceux à inclure. 5. Pour chaque page : choisir le **template**, **curer les médias**, ajuster le **texte** si besoin. 6. Insérer des **pages libres** (couverture, intro, sommaire) et les personnaliser. 7. **Réordonner** les pages (glisser-déposer). 8. **Prévisualiser** (rendu WYSIWYG identique au PDF). 9. **Générer le PDF**, renseigner le nom de fichier, télécharger. 10. Le portfolio reste **sauvegardé** et rééditable. --- ## 4. Architecture générale ``` ┌──────────────────────────────────────────────────────────────┐ │ Serveur perso (Docker) │ │ │ │ ┌────────────────────┐ ┌──────────────────────────────┐ │ │ │ Nuxt 3 (UI + API) │ │ Cache de contenu (lecture) │ │ │ │ - Bibliothèque │◀────▶│ user/pages/02.projets/… │ │ │ │ - Composeur │ │ (md + médias) │ │ │ │ - Prévisualisation│ └──────────────▲───────────────┘ │ │ │ - Templates (Vue) │ │ rsync/SSH (RO) │ │ └─────────┬──────────┘ │ (bouton │ │ │ rendu HTML │ « Rafraîchir ») │ │ ▼ │ │ │ ┌────────────────────┐ ┌──────────────┴───────────────┐ │ │ │ Playwright/Chromium│ │ Stockage portfolios │ │ │ │ + Paged.js │ │ data/portfolios/*.json │ │ │ │ → PDF │─────▶│ data/exports/*.pdf │ │ │ │ + sharp + gs (opti)│ └──────────────────────────────┘ │ │ └────────────────────┘ │ └───────────────────────────────────┬──────────────────────────┘ │ (SSH lecture seule) ┌──────────▼───────────┐ │ Serveur PROD Grav │ │ figureslibres.cc │ │ (aucune charge PDF) │ └───────────────────────┘ ``` **Principe clé** : la prod ne fait que **servir le site et exposer son contenu en SSH**. Toute la charge lourde (Playwright, génération, optimisation PDF) tourne sur le serveur perso. --- ## 5. Topologie de déploiement & sources de contenu L'outil supporte **deux sources de contenu** via une même interface d'abstraction (`ContentSource`), sélectionnée par configuration : ### 5.1 Mode DEV (immédiat, sans prod) - Source = **le Grav local présent dans ce dépôt** (`../grav-reference/user/pages/02.projets`). - Lecture directe du système de fichiers (le dossier est déjà monté sur le serveur). - **Permet de développer et tester tout l'outil dès maintenant**, avant tout accès prod. ### 5.2 Mode PROD (cible) - Source = **cache local alimenté par rsync/SSH** depuis la prod Figures Libres. - L'outil ne se connecte jamais en écriture à la prod. Le passage dev → prod ne change **que la configuration de la source**, pas le modèle de données ni les templates. --- ## 6. Synchronisation du contenu (mode PROD) - **Mécanisme** : `rsync -az --delete -e ssh` tirant `user/pages/02.projets/` (Markdown + médias) de la prod vers `data/content-cache/`. - **Déclenchement** : **bouton manuel « Rafraîchir »** dans l'UI (endpoint API qui lance la commande et renvoie un résumé : nb de fichiers mis à jour, date). - **Incrémental** : rsync ne transfère que les fichiers modifiés → rapide même avec de nombreux médias lourds. - **Lecture seule** : aucune écriture côté prod ; identifiants SSH stockés en secret (variable d'environnement / fichier non versionné). - **Note git** : le contenu (`user/pages/*`) est exclu de git côté Grav — rsync est donc le bon outil (git ne verrait pas les projets ni les médias). --- ## 7. Modèle de données ### 7.1 Lecture du contenu Grav Structure observée dans le Grav Figures Libres : ``` user/pages/02.projets/ 01.culturelle/ ← catégorie (dossier) atlas-atterrissage/ ← projet (dossier, slug) reader.md ← frontmatter YAML + corps Markdown *.png / *.mp4 ← médias (+ variantes @2x/@3x/@4x) 02.en-francais-au-pluriel/ reader.md … 02.publique/ 03.sociale/ ``` Frontmatter type d'un projet : ```yaml title: 'En français au pluriel' aura: image: Screenshot_….png # visuel de couverture du projet media_order: 'a.png,b.png,…' # ordre des médias taxonomy: category: ['Site web', 'Identité visuelle'] tag: ['Collectif Rivage'] date: '09-12-2024 22:19' published: true ``` + corps Markdown (description, éventuel lien externe en fin de texte). ### 7.2 Modèle interne (TypeScript, indicatif) ```ts // --- Contenu importé de Grav (lecture seule) --- interface Project { id: string; // slug complet, ex: "02.projets/01.culturelle/atlas-atterrissage" category: string; // "culturelle" | "publique" | "sociale" title: string; bodyMarkdown: string; // texte descriptif coverImage?: string; // aura.image media: MediaAsset[]; // dans l'ordre de media_order categories: string[]; // taxonomy.category tags: string[]; // taxonomy.tag date?: string; // ISO externalUrl?: string; // lien détecté dans le corps published: boolean; } interface MediaAsset { filename: string; type: 'image' | 'video'; variants: { label: '1x'|'2x'|'3x'|'4x'; path: string; width?: number }[]; posterPath?: string; // pour les vidéos : image d'aperçu } // --- Composition (créée dans l'outil, sauvegardée) --- interface Portfolio { id: string; name: string; // nom interne cover: CoverConfig; // titre/sous-titre/… personnalisables pages: Page[]; // ordre = ordre du PDF createdAt: string; updatedAt: string; frozenContent?: FrozenSnapshot; // rempli à la génération (voir §7.3) } type Page = | ProjectPage | FreePage | CoverPage | TocPage; // Fond de page commun à tous les types (voir §9.5) type Background = | { mode: 'white' } | { mode: 'tint'; color: string } // aplat clair | { mode: 'color'; color: string }; // pleine couleur interface ProjectPage { kind: 'project'; projectId: string; template: 'hero-text' | 'grid'; // templates définis en code layout: string; // disposition d'images (single|mosaic|grid-2|…), voir §9.5 mediaSelection: string[]; // filenames choisis + ordonnés (curation, sans recadrage) subtitleOverride?: string; // sinon dérivé de taxonomy.category textOverride?: string; // override ponctuel du texte (sinon = Grav) background: Background; options?: Record; // réglages mineurs éventuels du template } interface FreePage { // page libre d'intro / texte kind: 'free'; template: 'free-text'; title?: string; body?: string; // markdown saisi dans l'UI media?: string[]; // médias optionnels background: Background; } interface CoverConfig { // page de couverture title: string; subtitle?: string; recipient?: string; // « préparé pour … » backgroundImage?: string; background: Background; } interface TocPage { kind: 'toc'; title?: string; background: Background; } ``` ### 7.3 « Live + figé à la génération » - Pendant l'édition, l'outil lit le **contenu live** (cache synchronisé) → les projets reflètent l'état courant de Grav. - À la **génération du PDF**, l'outil écrit un **`frozenContent`** dans le portfolio sauvegardé : copie des textes, listes de médias et fichiers réellement utilisés. - Conséquence : régénérer un ancien portfolio produit **le même PDF**, même si Grav a changé depuis. Un bouton « Réactualiser depuis Grav » permettra de re-synchroniser volontairement un portfolio avec le contenu à jour. --- ## 8. Fonctionnalités détaillées ### 8.1 Bibliothèque de projets - Liste des projets par catégorie, avec vignette (basse résolution), titre, tags, date. - Recherche + filtres (catégorie, tag). - Sélection/désélection pour ajout au portfolio. ### 8.2 Composeur de portfolio - Colonne « pages du portfolio » (liste réordonnable en **glisser-déposer**). - Ajout de pages : depuis un projet (choix du template) ou page spéciale (couverture, sommaire, page libre). - Panneau d'édition de la page sélectionnée : - **choix du template** (parmi ceux définis en code) ; - **curation des médias** : cocher/décocher + réordonner les visuels du projet (aperçus basse résolution), **sans recadrage/zoom** ; - **disposition (layout)** : choix parmi les layouts du template (§9.5) ; - **texte** : pré-rempli depuis Grav, éditable (override local, sans toucher Grav) ; - **fond de page** : blanc / teinté / pleine couleur (`background`) ; - réglages mineurs du template le cas échéant. ### 8.3 Pages spéciales / « pages types » - **Couverture** : titre, sous-titre, destinataire, image de fond — personnalisables. - **Sommaire** : généré automatiquement depuis les pages-projet (titres + n° de page). - **Page libre d'intro** : titre + texte Markdown + médias optionnels, saisis dans l'UI. - (Optionnel v1+) séparateurs de section, 4e de couverture. - Ces pages sont elles aussi des **templates définis en code**, personnalisables via l'UI. ### 8.4 Prévisualisation WYSIWYG - Rendu à l'écran par **les mêmes composants Vue** que ceux utilisés pour le PDF. - Ce que l'on voit = ce que produira le PDF (mêmes styles, même pagination logique). - En prévisualisation, les images sont servies en **basse résolution** (fluidité). ### 8.5 Génération PDF - Nom de fichier personnalisable ; métadonnées PDF (titre/auteur) renseignées. - Pipeline détaillé au §10. ### 8.6 Gestion des portfolios sauvegardés - Liste des portfolios (nom, date de MAJ, aperçu couverture). - Actions : ouvrir, dupliquer, renommer, supprimer, régénérer le PDF. --- ## 9. Système de templates ### 9.1 Principe - Templates = **composants Vue** dans `generator/components/templates/`. - Un même composant sert **aperçu écran** et **rendu PDF** (source de vérité unique). - Contrat d'entrée commun (props) pour être interchangeables dans le composeur. ### 9.2 Contrat d'interface (props) ```ts interface TemplateProps { project?: Project; // pour les pages-projet subtitle?: string; // sous-titre "type de travail" (dérivé de taxonomy.category) media: ResolvedMedia[]; // médias curés, déjà résolus à la bonne résolution layout?: string; // disposition d'images choisie (voir §9.5) text?: string; // texte effectif (override ou Grav) background: Background; // fond de page (blanc / teinté / pleine couleur) — voir §7.2 page: { index: number; total: number }; theme: FiguresLibresTheme; // charte (couleurs, fonts) — voir §9.3 renderMode: 'preview' | 'print'; } ``` > **Sous-titre projet** : dans les books existants, chaque page-projet porte un sous-titre > « type de travail » (ex. « Identité visuelle et rapports d'activités », « Édition / > principe de la collection »). Il sera dérivé de `taxonomy.category` (joignables) ou > éditable en override. ### 9.3 Charte Figures Libres (extraite du thème Grav `figureslibres-v2`) - **Couleurs** (extraites du thème) : base claire `#f5f5f5`, encre `#0e1229` (bleu nuit). Couleurs secondaires disponibles : `#ffaeab`, `#feff74`, `#71ff94`, `#fabbde`, `#4bffc9`, à employer **avec parcimonie** (aplats de fond, filets). **Direction artistique sobre : pas de formes graphiques décoratives ni d'effets « fluo ».** - **Polices maison disponibles** (réutilisables, fichiers woff2 présents dans le thème) : Avara (serif de titre), Moche, Syne, ManifontGrotesk, PlayfairDisplay, Rumeur, Lato. - Les fonts seront embarquées dans l'app via `@font-face` pour un rendu identique en PDF. ### 9.4 Format de page (à valider) - **Format « écran » 16:9 paysage.** - Recommandation : **canvas de référence 1280×720 pt** (ratio 16:9), avec les **images redimensionnées à 2× leur boîte d'affichage** (≈ 150–200 dpi effectif) → netteté sans surpoids. Le PDF garde du **texte vectoriel** (léger et net à tout zoom). - `@page { size: 1280pt 720pt; margin: 0 }` côté CSS Paged.js (valeurs ajustables). - 1920×1080 px reste la cible d'affichage logique ; la valeur pt ci-dessus en est l'équivalent physique pratique pour le PDF. ### 9.5 Templates de départ (v1) 1. **`hero-text` — Visuel dominant + texte** : un grand visuel + bloc titre/sous-titre/ description (date, catégorie/tags, lien externe en pied). Le template « projet » de référence. 2. **`grid` — Grille / planche** : plusieurs médias d'un même projet en grille, titre discret. **Dispositions d'images (layouts).** L'analyse des books existants (voir §17) montre que la page-projet réclame des **compositions variables**, pas une grille unique. La curation médias inclut donc le choix d'une **disposition** parmi un jeu prédéfini, ex. : - `single` : une image plein cadre (bord à bord ou avec marge) ; - `mosaic` : mosaïque asymétrique (1 grande + plusieurs petites) ; - `grid-2` / `grid-3` / `grid-6` : grilles régulières ; - `image-text` : une image + un bloc de texte latéral. Ces layouts sont des variantes internes aux templates (props `layout` + `media`), pas des templates séparés — ils partagent l'en-tête (titre + sous-titre) et la charte. > Les deux **books de référence** sont désormais dans `docs/portfolios-examples/`. Ce sont > des **inspirations libres**, **pas des gabarits à reproduire** : ils sont en A3/A4 paysage > (impression), alors que la cible est le **16:9 écran**. On s'en sert pour le vocabulaire de > pages (§17) et l'esprit graphique, pas pour un pixel-perfect. --- ## 10. Pipeline PDF & optimisation des images Objectif : **PDF léger (envoyable par mail) ET images irréprochables**. 1. **Résolution des médias** : pour chaque image d'une page, calcul de sa **taille d'affichage réelle** dans le template au format 16:9, puis génération (via **`sharp`**) d'une version redimensionnée à `taille_affichage × 2` (retina). Les variantes `@2x/@3x/@4x` de Grav servent de source haute résolution ; on **descend** vers la taille utile plutôt que d'embarquer des images surdimensionnées. 2. **Rendu HTML** : Nuxt rend la page composée (mêmes composants que l'aperçu), **Paged.js** gère la pagination (une page CSS = une page PDF). 3. **Impression** : **Playwright/Chromium** `page.pdf()` en mode `print`, format 16:9, `printBackground: true`. 4. **Post-traitement** : passe **Ghostscript** (`gs`) pour recompresser/normaliser (downsampling au DPI cible, compression JPEG maîtrisée) et alléger le fichier final. Métadonnées PDF injectées (titre, auteur). 5. **Sortie** : `data/exports/.pdf`, proposé au téléchargement. > Les vidéos `.mp4` sont représentées par un **poster** (image d'aperçu) puisqu'un PDF est > statique. --- ## 11. Stack technique & arborescence > **Principe d'infrastructure : tout est conteneurisé.** Aucun runtime (Node, etc.) ni > binaire système n'est installé sur l'hôte, qui héberge déjà de nombreux services. Node, > le build, Playwright/Chromium et Ghostscript s'exécutent dans des conteneurs Docker. - **Front + API** : Nuxt 3 (Nitro pour les endpoints serveur), **Nuxt UI** pour les composants. - **PDF** : Playwright (Chromium) + Paged.js. - **Images** : sharp ; **Ghostscript** (dans le conteneur) pour l'optimisation PDF. - **Parsing Grav** : lecture FS + parseur front-matter YAML + Markdown. - **Synchro** : rsync/ssh (dans le conteneur), déclenché par un endpoint Nitro. Arborescence cible (dans ce dépôt, à côté de `grav-reference/` = Grav) : ``` portfolio-maker/ grav-reference/ ← Grav de référence (existant, renommé depuis app/) generator/ ← nouvelle app Nuxt (code) components/ templates/ ← hero-text.vue, grid.vue, cover.vue, toc.vue, free-text.vue composer/ ← UI de composition server/ api/ ← endpoints (projects, portfolios, sync, generate) lib/ ← grav-reader, media-resolver, pdf-pipeline, content-source data/ content-cache/ ← contenu synchronisé (prod) — non versionné portfolios/*.json ← compositions sauvegardées exports/*.pdf ← PDF générés nuxt.config.ts docker/ grav/Dockerfile ← (existant) image du Grav de référence generator/Dockerfile ← image du générateur (base image Playwright + Ghostscript) docker-compose.yml ← services grav-reference + generator docs/ spec-generateur-portfolios.md ← ce document ``` --- ## 12. Sécurité & accès - **Mot de passe partagé** unique (middleware d'auth simple + cookie de session). - Accessible sur le LAN comme Grav (port dédié, ex. 3000/8090). - Identifiants SSH de la prod stockés hors dépôt (env/secret), utilisés uniquement pour rsync en lecture. ## 13. Stockage - **Portfolios** : un fichier JSON par portfolio (`data/portfolios/.json`). - **Exports** : PDF dans `data/exports/`. - **Cache contenu** : `data/content-cache/` (rsync). - Tout dans un **volume Docker** → sauvegardable par simple copie de dossier. --- ## 14. Roadmap par phases **Phase 0 — Fondations (dev sur Grav local)** - App Nuxt + Nuxt UI, auth mot de passe. - `ContentSource` mode DEV : lecture de `../grav-reference/user/pages/02.projets`. - Parseur Grav (projets, médias, taxonomie, lien). - Bibliothèque de projets (liste, filtres). **Phase 1 — Composition & aperçu** - Modèle Portfolio + sauvegarde JSON. - Composeur : ajout/réordonnancement de pages, choix template, curation médias, override texte. - Templates `hero-text` et `grid` + charte Figures Libres. - Prévisualisation WYSIWYG. **Phase 2 — Génération PDF** - Pipeline Playwright + Paged.js au format 16:9. - Optimisation images (sharp) + post-traitement Ghostscript. - Pages spéciales : couverture, sommaire, page libre. - Personnalisation (titre/sous-titre, nom de fichier, métadonnées). - Gel du contenu à la génération (`frozenContent`). **Phase 3 — Passage prod & finitions** - `ContentSource` mode PROD : rsync/SSH + bouton « Rafraîchir ». - Conteneur Docker + intégration au `docker-compose`. - Raffinement des templates à partir des PDF d'exemple. - Gestion de bibliothèque (dupliquer, renommer, supprimer, régénérer). --- ## 15. Décisions ouvertes / à confirmer 1. **Format de page** : valider `1280×720 pt` (16:9) et le facteur retina ×2 pour les images (§9.4), ou fournir des dimensions précises attendues. 2. **Sommaire** : numérotation de pages souhaitée ? niveau de détail (par projet, par section) ? 3. **Plusieurs pages par projet** : les books montrent souvent 1 projet = 1 page, mais parfois un projet riche s'étale. Confirmer si on autorise plusieurs pages pour un même projet (actuellement : non, 1 projet = 1 page). 4. **Sous-titre projet** : dériver automatiquement de `taxonomy.category`, ou champ dédié éditable dans l'UI ? (défaut proposé : dérivé + override.) 5. **Accès prod** : confirmer l'accès **SSH** (et non seulement FTP) pour rsync, + le chemin exact de `user/pages` sur la prod. _Décidé_ : fonds de page **teintés/pleine couleur** proposés **dès la v1** (prop `background` = `white` | `tint` | `color`). Curation médias = **sélection + ordre + choix de layout**, **sans** recadrage/zoom d'image en v1._ --- ## 16. Annexe — Rappel de la structure de contenu observée - Projets sous `user/pages/02.projets/{01.culturelle,02.publique,03.sociale}//reader.md`. - Médias à côté du `.md`, variantes `@2x/@3x/@4x`, quelques `.mp4`. - Frontmatter clés utiles : `title`, `aura.image`, `media_order`, `taxonomy.category`, `taxonomy.tag`, `date`, `published`. - Le corps Markdown contient la description et, souvent, l'URL externe du projet. --- ## 17. Annexe — Références d'inspiration (books existants) > ⚠️ **Non-normatif.** Les deux PDF de `docs/portfolios-examples/` > (`book_2025_Bagneux.pdf`, 17 p. A4 paysage ; `book2025_FiguresLibres_edition.pdf`, > 16 p. A3 paysage) sont des **books faits main**, fournis comme **inspiration libre**. > Ils donnent l'**esprit** et le **vocabulaire de pages** attendus — **pas** des gabarits à > reproduire au pixel. La cible reste le **16:9 écran**. ### Taxonomie de pages observée (à couvrir par des "pages types") | Type de page | Description observée | |---|---| | **Couverture** | Date + titre du dossier (client) + coordonnées collectif + grande image. Personnalisable. | | **Présentation** | Grand titre (« 20 ans d'expérience… ») + colonnes de texte + image latérale pleine hauteur + logo en pastille. | | **Équipe** | Grille 2 colonnes × N : portrait rond + nom/rôle en gras + bio. Peut couvrir 2 pages. | | **Colonnes / infos** | Titre + 2–4 colonnes de texte à sous-titres, filet vertical coloré (« Outils et savoir-faire », « Commanditaires »). | | **Intercalaire-titre** | Grand titre sur page quasi vide (« Portfolio Figures Libres »). | | **Page-projet** | En-tête (titre + sous-titre "type de travail") + composition d'images variable (voir layouts §9.5) ± bloc texte ± fond teinté. | | **4e de couverture** | Contact « Figures Libres.cc » + adresse + citation d'un membre. | ### Grammaire graphique retenue comme inspiration - **Logo** en pastille (cercle noir ou couleur). - **Titres** grotesques géométriques arrondis (Syne / Moche) ; **texte courant** en Lato. - **Fonds** blancs, gris très clairs, beige, ou **pleine couleur** selon la page/projet. - En-tête projet **titre + sous-titre** systématique ; pagination + mention « FIGURES LIBRES » en pied. - Compositions d'images **asymétriques et respirées** (une dominante + satellites), pas seulement des grilles régulières. --- ## 18. Notes pour le développement autonome **Par où commencer.** Démarrer par la **Phase 0** sur le Grav local. Le CMS de référence est déjà servi (`docker compose up -d`, http://localhost:8089) et son contenu est lisible dans `../grav-reference/user/pages/02.projets/`. Aucune connexion prod n'est nécessaire pour les phases 0 à 2. **Contraintes fermes.** - **Ne jamais écrire dans `grav-reference/`** : c'est une source en **lecture seule**. - **TOUT tourne dans des conteneurs Docker — rien n'est installé sur l'hôte.** Le serveur héberge de nombreux services ; on n'y ajoute aucun runtime ni paquet. Node, npm/pnpm, le build Nuxt, Playwright/Chromium et Ghostscript vivent **dans des conteneurs**. Toute commande `node`/`npm`/`npx` s'exécute via un conteneur (ex. `docker compose run`), jamais sur la machine. - **Base recommandée** : image **officielle Playwright** (`mcr.microsoft.com/playwright`, Node + Chromium + dépendances déjà présents), + Ghostscript ajouté dans l'image du service. - **Ne rien installer sur le serveur ni lancer de `sudo` sans validation explicite de l'utilisateur.** - **FR uniquement.** **DA sobre** : pas de formes graphiques décoratives, pas d'effets « fluo ». **Conventions.** - Un **template = un composant Vue piloté uniquement par ses props** (`TemplateProps`, §9.2) : aucun accès direct au système de fichiers depuis un template → testable et identique en `preview` et `print`. - Toute lecture de contenu passe par l'abstraction **`ContentSource`** (§5) : brancher DEV d'abord, PROD ensuite sans toucher au reste. - Ghostscript et rsync (dans le conteneur) ne servent qu'aux phases 2–3 ; ne pas bloquer les phases 0–1 dessus. **Outils disponibles pour l'agent.** - **context7** (MCP branché) : doc à jour des libs (Nuxt 3, Nuxt UI, Playwright, Paged.js) — s'en servir plutôt que deviner les APIs. - **poppler-utils** : lire les PDF générés pour auto-vérifier le rendu et le comparer aux books d'inspiration (§17). **Definition of done — Phase 0.** La bibliothèque liste les projets réels du `grav-reference` (les 3 catégories), avec vignette basse résolution, titre, tags et date, filtrables ; l'auth par mot de passe protège l'accès ; rien n'est écrit côté Grav.