docs: versionner spec + guide de déploiement (hors portfolios-examples)
Guide de déploiement prod : VM Debian nue (sans Docker), service systemd, déploiement par push Git (bare repo + post-receive), branchement rsync/SSH en lecture seule à la prod Grav, reverse proxy HTTPS, dimensionnement VM.
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -4,9 +4,9 @@
|
||||
generator/
|
||||
grav-reference/
|
||||
|
||||
# Docs : PDF de référence + spec (déjà réalisée, désormais désynchro du code).
|
||||
# Le Dockerfile de l'image Grav reste versionné (docker/grav/).
|
||||
docs/
|
||||
# Docs versionnées (spec, guide de déploiement…), SAUF les exemples volumineux
|
||||
# (PDF de référence). Le Dockerfile de l'image Grav reste versionné (docker/grav/).
|
||||
docs/portfolios-examples/
|
||||
|
||||
# Divers
|
||||
.DS_Store
|
||||
|
||||
485
docs/deploiement-production.md
Normal file
485
docs/deploiement-production.md
Normal file
@@ -0,0 +1,485 @@
|
||||
# Déploiement en production
|
||||
|
||||
Ce document décrit comment déployer le **générateur de portfolios** sur une VM
|
||||
**Debian nue** (sans Docker) et le brancher, en lecture seule, à la VM **Grav de
|
||||
production** de Figures Libres.
|
||||
|
||||
Le déploiement se fait par **push Git** vers un dépôt *bare* sur la VM (même
|
||||
principe que les autres projets Figures Libres :
|
||||
<https://figureslibres.io/dokuwiki/developement:git_deployement>). L'app tourne
|
||||
comme **service systemd** (serveur Nitro), derrière un **reverse proxy HTTPS**.
|
||||
|
||||
> ⚠️ **Exception à la règle « tout en Docker ».** Docker n'est utilisé qu'en
|
||||
> développement local. En production, l'app tourne **nativement** sur Debian.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture cible
|
||||
|
||||
```
|
||||
Poste dev VM « portfolios » (Debian nue) VM Grav (prod)
|
||||
┌───────────┐ git push prod ┌──────────────────────────────────────┐ rsync/SSH ┌──────────────┐
|
||||
│ generator │ ─────────────────▶ │ bare repo .git ──post-receive──▶ deploy│ ─(lecture ────▶│ user/pages/ │
|
||||
│ (repo) │ │ → checkout + npm ci + build │ seule) │ 02.projets/ │
|
||||
└───────────┘ │ → systemctl restart │◀──── pull ──────│ (inchangé) │
|
||||
│ │ └──────────────┘
|
||||
│ systemd: node .output/server/index.mjs │
|
||||
│ écoute 127.0.0.1:3000 │
|
||||
│ données: /var/lib/portfolio-generator/ │
|
||||
└──────────────┬─────────────────────────┘
|
||||
│ 127.0.0.1:3000
|
||||
┌────────▼─────────┐
|
||||
│ nginx :443 (TLS) │ ◀── navigateurs (équipe + liens publics)
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
Points clés :
|
||||
|
||||
- Le contenu ne circule que **dans un sens** : la VM portfolios *tire* les projets
|
||||
depuis la prod Grav par `rsync`/SSH en **lecture seule**. La prod n'est jamais
|
||||
modifiée et **ignore** l'existence de l'app.
|
||||
- On **ne réimporte pas** `grav-reference` en prod (c'est un jeu d'exemple de dev).
|
||||
Le contenu prod vit dans un cache local rafraîchi par le bouton « Rafraîchir ».
|
||||
- Le rendu PDF appelle le serveur en interne sur **`localhost:3000`** : le port
|
||||
Nitro **doit** rester `3000` (voir §6).
|
||||
|
||||
---
|
||||
|
||||
## 2. Dimensionnement de la VM
|
||||
|
||||
### RAM
|
||||
|
||||
| Poste | Empreinte |
|
||||
|-------|-----------|
|
||||
| Serveur Nitro au repos | ~150–300 Mo |
|
||||
| Pic à la **génération PDF** : Chromium (~300–600 Mo) + traitement d'images `sharp` en 2× sur des pages A3 + Ghostscript | **1–2 Go** ponctuels |
|
||||
|
||||
- **Recommandé : 4 Go.** Plancher : 2 Go (risqué sur des portfolios très riches en
|
||||
images — le processus de génération peut être tué par l'OOM killer).
|
||||
- La génération est **séquentielle** (une seule instance Chromium à la fois via une
|
||||
file d'attente) : la RAM ne monte pas linéairement avec le nombre d'utilisateurs.
|
||||
|
||||
### Disque
|
||||
|
||||
Ordres de grandeur mesurés / à prévoir (l'essentiel vit dans `data/`) :
|
||||
|
||||
| Élément | Taille | Note |
|
||||
|---------|--------|------|
|
||||
| Debian + Node + outils | ~3–4 Go | système |
|
||||
| `node_modules` | ~1–1,5 Go | Nuxt + Playwright + sharp |
|
||||
| Chromium (Playwright) | ~0,5 Go | navigateur de rendu |
|
||||
| `.output` (build Nitro) | ~0,1–0,3 Go | régénéré à chaque déploiement |
|
||||
| `data/content-cache/` | ~0,75 Go aujourd'hui | miroir de `02.projets` (croît avec le contenu) |
|
||||
| `data/frozen/` | ~0,4 Go aujourd'hui | médias figés des portfolios générés (croît à l'usage) |
|
||||
| `data/exports/` | ~0,015 Go | PDF (1–8 Mo pièce, régénérables) |
|
||||
| `data/cache/` | transitoire | rendu/travail PDF |
|
||||
| `data/portfolios/` | quelques Mo | **la donnée métier** (JSON) |
|
||||
|
||||
- **Recommandé : 30 Go** (confort + croissance du contenu et des instantanés).
|
||||
Plancher : 20 Go.
|
||||
|
||||
### CPU
|
||||
|
||||
- **2 vCPU recommandés.** La génération PDF est *CPU-bound* (rendu Chromium +
|
||||
ré-échantillonnage d'images + Ghostscript), par à-coups.
|
||||
|
||||
---
|
||||
|
||||
## 3. Prérequis logiciels (installation unique, en root)
|
||||
|
||||
Sur la **VM portfolios**, une seule fois :
|
||||
|
||||
```bash
|
||||
apt update && apt install -y curl git rsync openssh-client ghostscript ffmpeg nginx
|
||||
|
||||
# Node.js 22 LTS (dépôt NodeSource)
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
apt install -y nodejs
|
||||
node --version # v22.x
|
||||
|
||||
# Dépendances système de Chromium (pour Playwright) — installées plus tard
|
||||
# par « npx playwright install-deps » (§7), qui nécessite root.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Utilisateur et arborescence
|
||||
|
||||
On dédie un utilisateur système `portfolio` qui **possède tout** (dépôt bare,
|
||||
checkout, données, service). Cela évite tout jonglage de permissions.
|
||||
|
||||
```bash
|
||||
# en root
|
||||
adduser --disabled-password --gecos "" portfolio
|
||||
|
||||
# arborescences
|
||||
install -d -o portfolio -g portfolio /home/portfolio/gitBareRepos
|
||||
install -d -o portfolio -g portfolio /var/www/portfolio-generator
|
||||
install -d -o portfolio -g portfolio /var/lib/portfolio-generator/data
|
||||
install -d -o portfolio -g portfolio -m 700 /var/lib/portfolio-generator/.ssh
|
||||
```
|
||||
|
||||
| Chemin | Rôle |
|
||||
|--------|------|
|
||||
| `/home/portfolio/gitBareRepos/portfolio-generator.git` | dépôt *bare* (cible du push) |
|
||||
| `/var/www/portfolio-generator/public_html` | checkout de travail (build) |
|
||||
| `/var/www/portfolio-generator/deploy.sh` | script de déploiement |
|
||||
| `/var/lib/portfolio-generator/data` | **données persistantes** (`NUXT_DATA_DIR`) |
|
||||
| `/var/lib/portfolio-generator/.ssh/portfolio_sync` | clé privée de synchro rsync |
|
||||
| `/etc/portfolio-generator.env` | variables d'environnement du service |
|
||||
|
||||
---
|
||||
|
||||
## 5. Déploiement par push Git
|
||||
|
||||
> Tout le §5 s'exécute **en tant qu'utilisateur `portfolio`** (`sudo -iu portfolio`).
|
||||
|
||||
### 5.1 Dépôt *bare*
|
||||
|
||||
```bash
|
||||
cd ~/gitBareRepos
|
||||
git init --bare portfolio-generator.git
|
||||
```
|
||||
|
||||
### 5.2 Hook `post-receive`
|
||||
|
||||
`~/gitBareRepos/portfolio-generator.git/hooks/post-receive` :
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
PRODDIR="/var/www/portfolio-generator"
|
||||
while read oldrev newrev refname; do
|
||||
if [ "$refname" = "refs/heads/prod" ]; then
|
||||
echo "===== DÉPLOIEMENT ====="
|
||||
unset GIT_DIR
|
||||
cd "$PRODDIR" || exit 1
|
||||
. deploy.sh
|
||||
echo "===== OK ====="
|
||||
else
|
||||
echo "Commit non déployé — pushez la branche prod."
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
```bash
|
||||
chmod +x ~/gitBareRepos/portfolio-generator.git/hooks/post-receive
|
||||
```
|
||||
|
||||
### 5.3 Checkout de travail
|
||||
|
||||
Cloné une fois depuis le dépôt bare (il aura ainsi `origin` = bare) :
|
||||
|
||||
```bash
|
||||
git clone ~/gitBareRepos/portfolio-generator.git /var/www/portfolio-generator/public_html
|
||||
```
|
||||
|
||||
### 5.4 Script `deploy.sh`
|
||||
|
||||
`/var/www/portfolio-generator/deploy.sh` :
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
APPDIR="/var/www/portfolio-generator/public_html"
|
||||
|
||||
echo "→ mise à jour du code"
|
||||
cd "$APPDIR"
|
||||
git fetch origin
|
||||
git reset --hard origin/prod
|
||||
|
||||
echo "→ dépendances + build Nitro"
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
echo "→ redémarrage du service"
|
||||
sudo systemctl restart portfolio-generator
|
||||
```
|
||||
|
||||
```bash
|
||||
chmod +x /var/www/portfolio-generator/deploy.sh
|
||||
```
|
||||
|
||||
`npm ci` télécharge automatiquement Chromium dans le cache de l'utilisateur
|
||||
`portfolio` (les dépendances **système** de Chromium sont, elles, installées une
|
||||
fois en root à l'étape §7).
|
||||
|
||||
### 5.5 Autoriser le redémarrage du service (sudoers)
|
||||
|
||||
`deploy.sh` a besoin de redémarrer le service ; on limite ce `sudo` au strict
|
||||
nécessaire. `/etc/sudoers.d/portfolio-generator` (en root, `visudo -f …`) :
|
||||
|
||||
```
|
||||
portfolio ALL=(root) NOPASSWD: /usr/bin/systemctl restart portfolio-generator
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Service systemd et environnement
|
||||
|
||||
### 6.1 Fichier d'environnement
|
||||
|
||||
`/etc/portfolio-generator.env` (droits `600`, propriétaire `portfolio` — il
|
||||
contient des secrets) :
|
||||
|
||||
```dotenv
|
||||
# --- Serveur ---
|
||||
# Le port DOIT rester 3000 : le rendu PDF appelle le serveur sur localhost:3000.
|
||||
NITRO_HOST=127.0.0.1
|
||||
NITRO_PORT=3000
|
||||
|
||||
# --- Secrets (valeurs aléatoires longues : openssl rand -hex 32) ---
|
||||
NUXT_PORTFOLIO_PASSWORD=<mot-de-passe-partagé>
|
||||
NUXT_SESSION_SECRET=<aléatoire-≥32-caractères>
|
||||
NUXT_INTERNAL_TOKEN=<aléatoire-≥32-caractères>
|
||||
|
||||
# --- Données persistantes (hors du checkout, préservées aux déploiements) ---
|
||||
NUXT_DATA_DIR=/var/lib/portfolio-generator/data
|
||||
|
||||
# --- Source de contenu : production (cache rsync) ---
|
||||
NUXT_CONTENT_SOURCE=prod
|
||||
NUXT_SYNC_REMOTE=portfolio-sync@<HOTE_GRAV>:/var/www/figureslibres/user/pages/02.projets/
|
||||
NUXT_SYNC_SSH_KEY=/var/lib/portfolio-generator/.ssh/portfolio_sync
|
||||
```
|
||||
|
||||
### 6.2 Unité systemd
|
||||
|
||||
`/etc/systemd/system/portfolio-generator.service` :
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Générateur de portfolios Figures Libres
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=portfolio
|
||||
Group=portfolio
|
||||
WorkingDirectory=/var/www/portfolio-generator/public_html
|
||||
EnvironmentFile=/etc/portfolio-generator.env
|
||||
ExecStart=/usr/bin/node .output/server/index.mjs
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl enable portfolio-generator # démarrage auto (ne pas encore start : voir §7)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Premier déploiement (bootstrap)
|
||||
|
||||
1. **Côté poste dev**, dans le dépôt `generator`, ajouter le distant et pousser la
|
||||
branche `prod` (déclenche le hook) :
|
||||
```bash
|
||||
git remote add prod portfolio@<VM_PORTFOLIOS>:gitBareRepos/portfolio-generator.git
|
||||
git push prod main:prod # pousse la branche locale main sur la branche prod distante
|
||||
```
|
||||
Le hook lance `deploy.sh` → `npm ci` + `npm run build`. (Au tout premier push,
|
||||
`git reset --hard origin/prod` échoue si le checkout est vide : voir la note.)
|
||||
|
||||
2. **Côté VM (utilisateur `portfolio`)**, une fois le code présent dans
|
||||
`public_html`, installer les **dépendances système de Chromium** (root) :
|
||||
```bash
|
||||
sudo npx --prefix /var/www/portfolio-generator/public_html playwright install-deps chromium
|
||||
```
|
||||
|
||||
3. Démarrer le service :
|
||||
```bash
|
||||
sudo systemctl start portfolio-generator
|
||||
sudo systemctl status portfolio-generator
|
||||
journalctl -u portfolio-generator -f
|
||||
```
|
||||
|
||||
> **Note premier push** : si `git reset --hard origin/prod` du `deploy.sh` bute sur
|
||||
> un checkout vierge, faire une fois manuellement dans `public_html` :
|
||||
> `git fetch origin && git checkout -B prod origin/prod`, puis relancer un push.
|
||||
|
||||
---
|
||||
|
||||
## 8. Brancher à la prod Grav (rsync/SSH en lecture seule)
|
||||
|
||||
### 8.1 Clé de synchronisation (VM portfolios, utilisateur `portfolio`)
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -N "" \
|
||||
-f /var/lib/portfolio-generator/.ssh/portfolio_sync -C "portfolio-sync"
|
||||
cat /var/lib/portfolio-generator/.ssh/portfolio_sync.pub # ← à déposer côté Grav
|
||||
```
|
||||
|
||||
### 8.2 Côté VM Grav : accès en lecture seule
|
||||
|
||||
Créer un utilisateur dédié et l'autoriser à **lire** les projets, sans shell ni
|
||||
écriture :
|
||||
|
||||
```bash
|
||||
sudo adduser --disabled-password --gecos "" portfolio-sync
|
||||
sudo setfacl -R -m u:portfolio-sync:rX /var/www/figureslibres/user/pages/02.projets
|
||||
sudo setfacl -R -d -m u:portfolio-sync:rX /var/www/figureslibres/user/pages/02.projets
|
||||
```
|
||||
|
||||
Déposer la clé publique dans `~portfolio-sync/.ssh/authorized_keys` en la
|
||||
**restreignant à rsync** (recommandé) via `rrsync` :
|
||||
|
||||
```
|
||||
command="rrsync -ro /var/www/figureslibres/user/pages/02.projets",no-agent-forwarding,no-port-forwarding,no-pty,no-X11-forwarding ssh-ed25519 AAAA...portfolio-sync...
|
||||
```
|
||||
|
||||
> `rrsync` accompagne rsync (`/usr/bin/rrsync` ou
|
||||
> `/usr/share/doc/rsync/scripts/rrsync`). Il verrouille la clé à un `rsync` en
|
||||
> lecture seule sur ce seul dossier. À défaut, une clé standard en lecture seule
|
||||
> convient. La commande exécutée par l'app est :
|
||||
> `rsync -az --delete -e "ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes -i <clé>" <remote>/ <cache>/`.
|
||||
|
||||
### 8.3 Réseau
|
||||
|
||||
- Ouvrir **SSH (22)** en sortie de la VM portfolios vers la VM Grav.
|
||||
- Renseigner `NUXT_SYNC_REMOTE` avec le **chemin absolu** de `02.projets` sur la VM
|
||||
Grav, terminé par `/` (voir §6.1).
|
||||
- Tester la connexion une fois pour accepter la clé d'hôte :
|
||||
```bash
|
||||
sudo -u portfolio ssh -i /var/lib/portfolio-generator/.ssh/portfolio_sync \
|
||||
portfolio-sync@<HOTE_GRAV> "echo ok"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Reverse proxy HTTPS (indispensable)
|
||||
|
||||
Le HTTPS est **requis**, pas seulement confortable :
|
||||
|
||||
- les **liens de partage publics** (`/view/<id>`) sont faits pour être ouverts hors
|
||||
réseau local ;
|
||||
- la **copie du lien dans le presse-papiers** n'est autorisée par les navigateurs
|
||||
qu'en *contexte sécurisé* (HTTPS ou `localhost`) — en HTTP sur IP elle est bloquée.
|
||||
|
||||
`/etc/nginx/sites-available/portfolio-generator` :
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name portfolios.figureslibres.io;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name portfolios.figureslibres.io;
|
||||
|
||||
# ssl_certificate ... ; # via certbot (Let's Encrypt)
|
||||
# ssl_certificate_key ... ;
|
||||
|
||||
client_max_body_size 64m; # médias volumineux
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 600s; # la génération PDF peut être longue
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
ln -s /etc/nginx/sites-available/portfolio-generator /etc/nginx/sites-enabled/
|
||||
apt install -y certbot python3-certbot-nginx
|
||||
certbot --nginx -d portfolios.figureslibres.io
|
||||
nginx -t && systemctl reload nginx
|
||||
```
|
||||
|
||||
Le service Nitro écoutant sur `127.0.0.1:3000`, il **n'est pas** exposé
|
||||
directement : seul le port 443 est public. Un pare-feu (`ufw allow 443,22`) durcit
|
||||
l'ensemble.
|
||||
|
||||
---
|
||||
|
||||
## 10. Première synchronisation & vérification
|
||||
|
||||
1. Ouvrir `https://portfolios.figureslibres.io`.
|
||||
2. Se connecter avec `NUXT_PORTFOLIO_PASSWORD`.
|
||||
3. Cliquer **« Rafraîchir »** : premier `rsync` vers
|
||||
`/var/lib/portfolio-generator/data/content-cache/`. Le toast indique le nombre
|
||||
de fichiers et de projets synchronisés.
|
||||
4. Créer un portfolio de test et **générer un PDF** pour valider toute la chaîne
|
||||
(Chromium + Ghostscript).
|
||||
|
||||
---
|
||||
|
||||
## 11. Sauvegardes
|
||||
|
||||
L'état applicatif vit dans **`/var/lib/portfolio-generator/data/`** :
|
||||
|
||||
| Dossier | Contenu | Sauvegarder ? |
|
||||
|---------|---------|---------------|
|
||||
| `portfolios/` | portfolios (JSON) — **la donnée métier** | **Oui** |
|
||||
| `frozen/` | instantanés figés (contenu + composition) | Oui |
|
||||
| `exports/` | PDF générés | Optionnel (régénérables) |
|
||||
| `content-cache/` | miroir rsync de la prod | Non (re-tirable) |
|
||||
| `cache/` | cache de rendu | Non |
|
||||
|
||||
Sauvegarde à chaud (les écritures des portfolios sont atomiques) :
|
||||
|
||||
```bash
|
||||
tar czf portfolios-backup-$(date +%F).tar.gz \
|
||||
-C /var/lib/portfolio-generator/data portfolios frozen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Mises à jour (au quotidien)
|
||||
|
||||
Depuis le poste dev, une fois le travail prêt :
|
||||
|
||||
```bash
|
||||
git push prod main:prod
|
||||
```
|
||||
|
||||
Le hook redéploie tout seul (`git reset --hard` + `npm ci` + `npm run build` +
|
||||
redémarrage). Les données de `/var/lib/portfolio-generator/data/` sont préservées.
|
||||
Suivre : `journalctl -u portfolio-generator -f`.
|
||||
|
||||
---
|
||||
|
||||
## 13. Dépannage
|
||||
|
||||
| Symptôme | Piste |
|
||||
|----------|-------|
|
||||
| Le push ne déploie pas | Vérifier qu'on pousse bien vers la branche **`prod`** (`git push prod main:prod`) et que `post-receive` est exécutable. |
|
||||
| `deploy.sh` échoue sur `systemctl restart` | Vérifier l'entrée `sudoers.d/portfolio-generator` (§5.5). |
|
||||
| Service qui ne démarre pas | `journalctl -u portfolio-generator -e` ; souvent `.output` absent (build échoué) ou `EnvironmentFile` manquant. |
|
||||
| Génération PDF en échec | Dépendances Chromium manquantes → `npx playwright install-deps chromium` (§7.2) ; sinon vérifier RAM (OOM) dans `journalctl`. |
|
||||
| « Rafraîchir » échoue (« Échec rsync ») | Tester la commande SSH (§8.3) ; vérifier `NUXT_SYNC_REMOTE` (chemin absolu, `/` final), les droits de lecture et le pare-feu sortant (22). |
|
||||
| Copie du lien public impossible | Accès en HTTP/IP → passer par l'URL **HTTPS** (§9). |
|
||||
| PDF « à jour » ne se met pas à jour | La régénération réutilise l'instantané figé ; utiliser « Réactualiser depuis Grav » pour resynchroniser le contenu. |
|
||||
|
||||
---
|
||||
|
||||
## 14. Récapitulatif express
|
||||
|
||||
```bash
|
||||
# ── VM portfolios (root, une fois) ──
|
||||
apt update && apt install -y curl git rsync openssh-client ghostscript ffmpeg nginx
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt install -y nodejs
|
||||
adduser --disabled-password --gecos "" portfolio
|
||||
install -d -o portfolio -g portfolio /home/portfolio/gitBareRepos /var/www/portfolio-generator
|
||||
install -d -o portfolio -g portfolio /var/lib/portfolio-generator/data
|
||||
install -d -o portfolio -g portfolio -m 700 /var/lib/portfolio-generator/.ssh
|
||||
# → créer bare repo + hook + deploy.sh (§5), env + service systemd (§6),
|
||||
# sudoers (§5.5), clé de synchro (§8.1)
|
||||
|
||||
# ── Poste dev ──
|
||||
git remote add prod portfolio@<VM>:gitBareRepos/portfolio-generator.git
|
||||
git push prod main:prod
|
||||
|
||||
# ── VM portfolios (après 1er push) ──
|
||||
sudo npx --prefix /var/www/portfolio-generator/public_html playwright install-deps chromium
|
||||
sudo systemctl start portfolio-generator
|
||||
|
||||
# ── Côté Grav : déposer la clé publique en lecture seule (§8.2) ──
|
||||
# ── nginx + certbot en façade (§9), puis « Rafraîchir » dans l'UI ──
|
||||
```
|
||||
579
docs/spec-generateur-portfolios.md
Normal file
579
docs/spec-generateur-portfolios.md
Normal file
@@ -0,0 +1,579 @@
|
||||
# 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<string, unknown>; // 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/<nom-fichier>.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/<id>.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}/<slug>/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.
|
||||
Reference in New Issue
Block a user