feat(client-handover): 4-chapter doc structure + branded HTML/PDF rendering

This commit is contained in:
bastien 2026-05-07 19:08:59 +02:00
parent 5d8103f595
commit a963faa764
5 changed files with 1081 additions and 141 deletions

View File

@ -1,6 +1,6 @@
--- ---
name: client-handover-writer name: client-handover-writer
description: Final ship-and-handover orchestrator. Runs SEO+GEO and HARDEN with auto-fix loops in parallel until each ≥17/20, commits/pushes, pauses for deploy confirmation, runs VALIDATE against live site, gates on all-scores ≥17/20, then synthesizes a non-technical client deliverable with before/after scores and an owner-maintenance checklist. Reads git history + .claude/memory/ registries. Optional manual SEO/GEO platform chapter for web/local-business projects and a build/deploy chapter. description: Final ship-and-handover orchestrator. Runs SEO+GEO and HARDEN with auto-fix loops in parallel until each ≥17/20, commits/pushes, pauses for deploy confirmation, runs VALIDATE against live site, gates on all-scores ≥17/20, then synthesizes a non-technical client deliverable as Markdown + branded HTML + PDF (ZenQuality cover page, Inter+Playfair Display typography, green palette). The deliverable is structured in 4 chapters: what was needed (and why), what was done (≤300 words, zero jargon, no internal tool names), what the client must do, and technical details for the curious. Reads git history + .claude/memory/ registries. Optional manual SEO/GEO platform chapter for web/local-business projects and a build/deploy chapter.
tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, AskUserQuestion, Agent tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, AskUserQuestion, Agent
model: opus model: opus
--- ---
@ -9,19 +9,35 @@ model: opus
## GOAL ## GOAL
Orchestrate a final **ship-and-handover pipeline** then produce a single Markdown Orchestrate a final **ship-and-handover pipeline** then produce a triple
deliverable (`LIVRAISON.md` or `HANDOVER.md`) that a non-technical client can deliverable next to each other on disk:
read end-to-end and understand what was built, what was hardened in the final - `LIVRAISON.md` / `HANDOVER.md` — source markdown (editable)
pass, and what they must do/maintain going forward. - `LIVRAISON.html` / `HANDOVER.html` — branded HTML (browser-printable fallback)
- `LIVRAISON.pdf` / `HANDOVER.pdf` — branded PDF (when a PDF engine is available)
The branded HTML and PDF use the ZenQuality identity: green palette
(`#1A3A25 / #2D5A3D / #4A7C59 / #87A878`), cream background `#F5F0EB`,
Inter (body) + Playfair Display (headings), cover page with logo + tagline,
running header/footer with project name and page numbers.
The deliverable is structured in **4 chapters**, optimised for a non-technical
client who reads top-to-bottom and may stop after chapter 2:
1. **Ce qu'il fallait faire (et pourquoi)** — the brief and the underlying problem.
2. **Ce qui a été fait** — lay summary, ≤300 words, zero jargon, **no internal
tool / skill names**.
3. **Ce qui vous reste à faire** — action-only checklist grouped by cadence.
4. **Détails techniques (pour les curieux)** — score table, key technical
choices, phases, optional glossary. Internal labels may appear here.
Plus optional annex chapters: §5 external platforms (web), §6 build & deploy.
Pipeline (each step gates the next): Pipeline (each step gates the next):
1. Baseline audits: /seo (SEO+GEO) and /harden in parallel. 1. Baseline audits: SEO+GEO and security hardening in parallel.
2. Fix loops: re-invoke each audit with auto-fix until ≥17/20 or `MAX_ITERATIONS` hit. 2. Fix loops: re-invoke each audit with auto-fix until ≥17/20 or `MAX_ITERATIONS` hit.
3. Commit + push if files changed. 3. Commit + push if files changed.
4. Deploy pause: list deploy artifacts + process, wait for user confirmation. 4. Deploy pause: list deploy artifacts + process, wait for user confirmation.
5. /validate against the deployed/live site. 5. Live-site validation against the deployed URL.
6. Per-audit gate: every score ≥17/20 OR stop + roadmap. 6. Per-axis gate: every score ≥17/20 OR stop + roadmap.
7. Synthesize client deliverable with before/after scores + owner responsibilities. 7. Synthesize the markdown + render the branded HTML + PDF.
Source of truth for the deliverable: git history since first commit + `.claude/memory/` Source of truth for the deliverable: git history since first commit + `.claude/memory/`
registries (decisions, learnings, blockers, journal, evals). Output language follows registries (decisions, learnings, blockers, journal, evals). Output language follows
@ -817,8 +833,32 @@ If false, the chapter focuses on general directory + AI search.
## STEP 12 — SYNTHESIZE THE DOCUMENT ## STEP 12 — SYNTHESIZE THE DOCUMENT
Generate the deliverable section by section. Translate headings to `LANG`. Generate the deliverable as a tight 4-chapter structure: what was needed,
Tone: friendly, concrete, no jargon. One short paragraph per idea. what was done (lay summary), what the client must do, then technical
details for the curious. Translate headings to `LANG`. Tone: friendly,
concrete, no jargon. One short paragraph per idea.
### Hard rules for this document
1. **Never name internal tools or skill identifiers in chapters 13.**
Forbidden tokens (do not appear, in any case, in the lay portion):
`/seo`, `/harden`, `/validate`, `/cso`, `/feat`, `/bugfix`,
`/ship-feature`, `/ship`, `/code-clean`, `/refactor`, `seo-analyzer`,
`geo-analyzer`, `validator-analyzer`, `harden`-as-product-name,
`SEO.md`, `HARDEN.md`, `VALIDATE.md`, `CSO.md`, `MAX_ITERATIONS`,
`ALL_PASS`, `SCORE_*`. Replace with what they correspond to in client
language: référencement / visibilité IA / sécurité / conformité
technique / audit interne. Internal tool names may appear ONLY in
chapter 4 ("Détails techniques") inside the optional glossary.
2. **Chapter 2 hard cap: 300 words max, zero technical jargon.** Plain
French (or plain English if `LANG=en`). No acronyms not already in
common usage (HTTPS is fine; CSP is not). Run `wc -w` against the
chapter body; if over 300, rewrite shorter.
3. **Chapter 3 is action-only.** Every bullet starts with a verb the
client can act on without a developer.
4. **Chapter 4 may use technical terms** (SEO, GEO, HSTS, CSP, etc.) but
each term gets a one-line plain-language definition the first time it
appears, or a glossary at the end of the chapter.
### Document structure ### Document structure
@ -830,102 +870,78 @@ Tone: friendly, concrete, no jargon. One short paragraph per idea.
> Ce document récapitule l'ensemble du travail réalisé sur votre projet > Ce document récapitule l'ensemble du travail réalisé sur votre projet
> du JJ/MM/AAAA au JJ/MM/AAAA. > du JJ/MM/AAAA au JJ/MM/AAAA.
## 1. En une minute ## 1. Ce qu'il fallait faire (et pourquoi)
[2-3 sentences. What is the project, what does it do, current state.] [Briefing + motivation. 100180 words max. Two short paragraphs.
- §1.1 (the brief): what the client wanted, in their own words if
possible. Pull from the project journal's earliest entry, the README,
or the first commit message.
- §1.2 (the why): the underlying problem this project solves for the
client (no audience, weak online presence, manual process to
automate, broken legacy site, etc.). Concrete. Their reality, not
ours.
## 2. Ce que vous avez maintenant End the chapter with a one-line success criterion in their words —
"À la livraison, vous deviez pouvoir ___." If unknown, omit rather
than invent.]
[Bullet list of features as USER BENEFITS. Pull from journal + commit clusters.] ## 2. Ce qui a été fait
## 3. Comment on en est arrivé là [**HARD CAP: 300 words. ZERO technical jargon.** This is the chapter the
client reads first, possibly the only one they read.
[3 to 7 phases. For each: what was done, why it mattered. Plain phase names.] Structure as a single short narrative + a tight bullet list of
user-visible benefits:
## 4. État de santé du site (avant / après) Para 1 (35 sentences): the project today, in their words. What it
looks like to a visitor, what the client can do with it. NOT what
technologies were used.
[NEW SECTION — score table from STEP 8. SEO classique and GEO (IA) are Bullet list (510 items): visible benefits, each phrased as something
shown on separate rows so the client sees both axes explicitly.] the client or their visitors can now do that they couldn't before.
Pattern: "Vos visiteurs peuvent ___" / "Vous pouvez ___" /
"Le site est maintenant ___".
Avant la passe finale → après la passe finale (cette semaine) : Forbidden in this chapter: framework names, audit names, score numbers,
file paths, package names, command-line tool names, anything ending in
`.md`, `.json`, `.yaml`. If you cannot describe a feature without one
of those, the feature belongs in chapter 4, not here.
| Domaine | Avant | Après | Statut | After drafting, count words. Cap at 300. If over, cut paragraphs not
|------------------------------------------|-----------:|-----------:|:------:| bullets — bullets are the value-dense part.]
| Référencement Google (SEO classique) | <X.X>/20 | <Y.Y>/20 | ✅ |
| Visibilité IA (GEO — ChatGPT, Perplexity)| <X.X>/20 | <Y.Y>/20 | ✅ |
| Sécurité du site | <X.X>/20 | <Y.Y>/20 | ✅ |
| Conformité technique (W3C) | — | <Z.Z>/20 | ✅ |
[If LANG=en: "Site health (before / after)" with the same columns. ## 3. Ce qui vous reste à faire
Use these column labels: "Domain" / "Before" / "After" / "Status".
Row labels: "Google search (classical SEO)", "AI visibility (GEO —
ChatGPT, Perplexity)", "Site security", "Technical compliance (W3C)".]
Plain explanation under the table: [Action-only checklist for the client. Pull from: open `blockers.md`
- **Référencement Google (SEO classique)** = comment Google, Bing et entries, ongoing-monitoring items, external platforms to claim,
les autres moteurs traditionnels trouvent et classent votre site. content updates only the client can make, deploy steps if self-hosted.
C'est ce qui amène la majorité du trafic aujourd'hui.
- **Visibilité IA (GEO)** = comment les moteurs de recherche par IA
(ChatGPT, Perplexity, Gemini, Google AI Overviews) lisent et citent
votre site. Trafic encore minoritaire mais en forte croissance —
votre site est maintenant prêt pour ce canal (llms.txt, données
structurées pour extraction IA, signaux d'entité).
- **Sécurité** = protections contre les attaques courantes (en-têtes
HTTPS, anti-injection, etc.).
- **Conformité technique** = respect des standards web (HTML, CSS,
accessibilité). Ouvert dans la plupart des navigateurs et lecteurs
d'écran sans bug.
[If any score had a notable jump, add a one-liner: "La sécurité est passée Format as a checklist grouped by cadence. Every line starts with a
de 12 à 18 — on a ajouté les en-têtes manquants et forcé le passage en verb. Every line is something the client can do without a developer.
HTTPS." Do the same for SEO and GEO independently if either jumped.]
## 5. Les choix importants qu'on a faits
[Vulgarize BDR entries. 3-7 decisions max — design, framework, security,
hosting choices the client would care about.]
## 6. Ce qu'on a appris en route (optionnel)
[Only if learnings.md has client-relevant entries. 3-5 bullets max.]
## 7. Ce qui reste à faire ou à surveiller
[From blockers.md (open) + code TODOs. Plain description, urgency,
trigger.]
## 8. Comment utiliser le projet au quotidien
[1-page guide for the client to USE what was delivered. URL, CMS, contact.]
## 9. Ce que vous devez faire et maintenir vous-même
[NEW CONSOLIDATED SECTION — explicit owner-responsibility checklist.
Pull from: SEO/GEO chapter actions, deploy chapter actions, blockers,
ongoing-monitoring items.
Format as actionable checklist grouped by cadence:
### Une fois (à faire dans les premières semaines) ### Une fois (à faire dans les premières semaines)
- [ ] Réclamer la fiche Google Business Profile et la vérifier (lien : ...) - [ ] Réclamer la fiche Google Business Profile et la vérifier (lien : ...)
- [ ] Compléter le profil Apple Business Connect (lien : ...) - [ ] Compléter le profil Apple Business Connect (lien : ...)
- [ ] Vérifier la cohérence NAP (Nom / Adresse / Téléphone) sur toutes - [ ] Vérifier la cohérence Nom / Adresse / Téléphone sur toutes les
les plateformes — voir tableau au §10 plateformes — voir l'annexe à la fin du document
- [ ] [Si self-host : configurer le certificat SSL (renouvellement auto Let's Encrypt)] - [ ] [Si vous gérez l'hébergement vous-même : configurer le certificat
- [ ] [Si self-host : programmer une sauvegarde quotidienne] de sécurité (renouvellement automatique recommandé)]
- [ ] [Si vous gérez l'hébergement vous-même : programmer une sauvegarde
quotidienne]
- [ ] Sauvegarder ce document hors du dépôt (PDF, email) - [ ] Sauvegarder ce document hors du dépôt (PDF, email)
### Mensuel ### Mensuel
- [ ] Ajouter / mettre à jour 5 photos sur Google Business - [ ] Ajouter ou mettre à jour 5 photos sur Google Business
- [ ] Répondre aux avis Google (positifs et négatifs) - [ ] Répondre aux avis Google (positifs et négatifs) sous 48 h
- [ ] Vérifier que le site est toujours en ligne (test simple : ouvrir - [ ] Vérifier que le site est toujours en ligne (test simple : ouvrir
l'URL depuis un autre appareil) l'URL depuis un autre appareil)
- [ ] [Si CMS : mettre à jour les contenus saisonniers] - [ ] [Si système de gestion de contenu : mettre à jour les contenus
saisonniers]
### Trimestriel ### Trimestriel
- [ ] Faire un test de visibilité IA : taper le nom du commerce dans - [ ] Faire un test de visibilité IA : taper le nom du commerce dans
ChatGPT, Perplexity, Gemini. Noter ce qui s'affiche. ChatGPT, Perplexity, Gemini. Noter ce qui s'affiche.
- [ ] Demander à 3-5 clients de laisser un avis Google - [ ] Demander à 35 clients de laisser un avis Google
- [ ] Publier un post Google Business (offre, événement, actualité) - [ ] Publier un post Google Business (offre, événement, actualité)
### Annuel ### Annuel
@ -934,25 +950,90 @@ Format as actionable checklist grouped by cadence:
- [ ] Renouveler les noms de domaine - [ ] Renouveler les noms de domaine
### Quand quelque chose change dans la vie du commerce ### Quand quelque chose change dans la vie du commerce
- [ ] Changement d'adresse / téléphone / horaires → modifier d'abord sur - [ ] Changement d'adresse, de téléphone ou d'horaires → modifier
Google Business, puis sur toutes les autres plateformes (la d'abord sur Google Business, puis sur toutes les autres
cohérence est cruciale, voir §10) plateformes (la cohérence est cruciale)
[Adapt cadences to project type. For SaaS / non-local: replace [Adapt cadences to project type. For SaaS / non-local: replace
Google Business with appropriate platforms.] Google Business cadences with appropriate platforms (Slack, App Store,
``` Play Store, Trustpilot, G2, Capterra, etc.). For pure tooling /
internal projects, this chapter may shrink to a 5-line "à surveiller"
list — that is fine, do not pad.]
## 10. [SEO/GEO manual chapter — web projects only — see STEP 13] ## 4. Détails techniques (pour les curieux)
## 11. [Build & deploy chapter — only if Q1=Yes — see STEP 14] [Same content as before but consolidated and labelled as the
technical-depth chapter. Internal tool names may appear here.
The client is not required to read this chapter.]
## 12. Pour aller plus loin ### 4.1 État de santé du site (avant / après)
[3-5 concrete suggestions. Phrase as opportunities.] | Domaine | Avant | Après | Statut |
|------------------------------------------|-----------:|-----------:|:------:|
| Référencement Google (recherche classique)| <X.X>/20 | <Y.Y>/20 | ✅ |
| Visibilité IA (ChatGPT, Perplexity, Gemini)| <X.X>/20 | <Y.Y>/20 | ✅ |
| Sécurité du site | <X.X>/20 | <Y.Y>/20 | ✅ |
| Conformité technique | — | <Z.Z>/20 | ✅ |
## Annexe — Détails techniques [If LANG=en: "Site health (before / after)" with the same columns.
Use these column labels: "Domain" / "Before" / "After" / "Status".
Row labels: "Google search (classical)", "AI visibility (ChatGPT,
Perplexity, Gemini)", "Site security", "Technical compliance".]
[Pointer for the technically curious — README, source repo, etc.] Lecture rapide :
- **Référencement Google** = comment Google, Bing et les autres moteurs
classiques trouvent et classent votre site. Majorité du trafic
aujourd'hui.
- **Visibilité IA** = comment les moteurs par IA (ChatGPT, Perplexity,
Gemini, Google AI Overviews) lisent et citent votre site. Trafic
minoritaire mais en croissance forte — votre site est désormais prêt
pour ce canal.
- **Sécurité** = protections contre les attaques courantes (chiffrement
HTTPS, en-têtes anti-injection, redirections sûres).
- **Conformité technique** = respect des standards web (HTML, CSS,
accessibilité). Ouvert dans la plupart des navigateurs et lecteurs
d'écran sans bug.
[If any score had a notable jump, add a one-liner per axis: "La sécurité
est passée de 12 à 18 — en-têtes manquants ajoutés, passage HTTPS forcé."]
### 4.2 Choix techniques importants
[Vulgarize 37 BDR entries. Design, framework, security, hosting
decisions the client would care about. One paragraph each:
what was chosen, why over the alternative, what it changes for the
client. Drop entries the client cannot act on or care about.]
### 4.3 Comment on en est arrivé là (phases)
[37 phases. For each: what was done, why it mattered, in technical
detail this time. Reference commit clusters from STEP 10. Plain phase
names, not skill names.]
### 4.4 Glossaire (optionnel)
[Include only if at least 4 of the terms below appear in chapter 4.
Format: term — one-line plain-language definition. Sort alphabetically.
This is the ONLY place internal tooling names may be mentioned by
their internal label, and only when explaining what they correspond
to.]
- **SEO (référencement classique)** — ensemble des pratiques pour
apparaître dans Google, Bing, DuckDuckGo.
- **GEO (visibilité IA)** — équivalent du SEO pour les moteurs par IA
comme ChatGPT, Perplexity, Gemini.
- **HSTS** — en-tête HTTP qui force la navigation en HTTPS.
- **CSP (Content Security Policy)** — règle qui limite ce que le
navigateur charge depuis le site, pour bloquer les injections.
- **WCAG** — standard d'accessibilité (AA = niveau recommandé).
- **Schema.org / JSON-LD** — annotations cachées qui aident moteurs et
IA à comprendre le contenu.
- **llms.txt** — fichier qui dit aux moteurs IA quel est le contenu
important du site.
## 5. Annexe — Plateformes externes (web)
## 6. Annexe — Build & déploiement (optionnel)
--- ---
@ -963,20 +1044,24 @@ des audits de santé. Pour toute question, contactez [contact].*
### Tone rules ### Tone rules
1. Address the client directly ("votre site", "vous pouvez"). 1. Address the client directly ("votre site", "vous pouvez").
2. Replace tech terms with user-facing equivalents. 2. Chapters 13: replace every tech term with a user-facing equivalent.
3. No abbreviations the client wouldn't use. 3. No abbreviations the client wouldn't use (HTTPS yes, CSP no — unless
in chapter 4 with definition).
4. Concrete numbers > adjectives. 4. Concrete numbers > adjectives.
5. Short paragraphs. Bullet lists for things you can count. 5. Short paragraphs. Bullet lists for things you can count.
6. **Score deltas explained in plain words**. Never just dump numbers. 6. **Score deltas explained in plain words**. Never just dump numbers.
7. **Owner-responsibility section is action-oriented**. Every line starts 7. **Chapter 3 is action-oriented**. Every line starts with a verb.
with a verb. Every line is something the client can do without a dev. Every line is something the client can do without a developer.
8. **No skill-name leaks in chapters 13.** See "Hard rules" above.
--- ---
## STEP 13 — SEO/GEO MANUAL CHECKLIST (web projects only) ## STEP 13 — SEO/GEO MANUAL CHECKLIST (web projects only)
If `PROJECT_TYPE=web` AND `--skip-seo` NOT set, append this chapter If `PROJECT_TYPE=web` AND `--skip-seo` NOT set, append this chapter
(numbered §10 in the doc). as **§5 Annexe — Plateformes externes** in the new 4-chapter structure
(see STEP 12). Replace the §5 stub with the full content rendered from
the resource file.
Read the resource file: Read the resource file:
`$HOME/.claude/skills/client-handover/checklists/seo-geo-manual.md` `$HOME/.claude/skills/client-handover/checklists/seo-geo-manual.md`
@ -1027,7 +1112,9 @@ that are recurring belong in §9's cadence checklist.
## STEP 14 — BUILD & DEPLOY CHAPTER (only if Q1=Yes) ## STEP 14 — BUILD & DEPLOY CHAPTER (only if Q1=Yes)
For each `DEPLOY_HINTS` match, generate a short subsection: If included, this becomes **§6 Annexe — Build & déploiement** in the new
4-chapter structure (see STEP 12). For each `DEPLOY_HINTS` match,
generate a short subsection:
1. What this means (1 paragraph). 1. What this means (1 paragraph).
2. First-time setup (numbered steps + signup link). 2. First-time setup (numbered steps + signup link).
3. Day-to-day deploy (typical command / click sequence). 3. Day-to-day deploy (typical command / click sequence).
@ -1045,7 +1132,7 @@ For each: signup + 5-step deploy walkthrough.
--- ---
## STEP 15 — WRITE OUTPUT ## STEP 15 — WRITE MARKDOWN OUTPUT
Default output path: project root. Default output path: project root.
- `LIVRAISON.md` if `LANG=fr` - `LIVRAISON.md` if `LANG=fr`
@ -1058,28 +1145,123 @@ If a file at that path already exists, AskUserQuestion:
Write the file with the `Write` tool. Write the file with the `Write` tool.
Sanity check: Sanity checks (do them in this order, before STEP 16):
```bash ```bash
wc -l <output> # expect 200-800 lines wc -l <output> # expect 250-800 lines
grep -c "^## " <output> # expect 8-13 chapters (NEW: §4 health, §9 owner-resp) grep -c "^## " <output> # expect 4-6 top-level chapters
# §1, §2, §3, §4, [§5 web], [§6 deploy]
``` ```
**Chapter 2 word-count gate.** Extract the body of `## 2. Ce qui a été fait`
(or `## 2. What we did` if `LANG=en`) and run `wc -w` on it. **Hard cap:
300 words.** If over, edit the chapter (remove paragraphs, keep bullets)
and re-write before moving to STEP 16. Do not skip this gate — chapter 2
is the part the client reads first.
```bash
awk '/^## 2\. /{flag=1; next} /^## 3\. /{flag=0} flag' "$OUTPUT" | wc -w
# expected: ≤ 300
```
**Skill-name leak gate.** Forbidden tokens must NOT appear in chapters
13 (chapter 4 may use them in the optional glossary):
```bash
awk '/^## 1\./{flag=1} /^## 4\./{flag=0} flag' "$OUTPUT" \
| grep -niE '/(seo|harden|validate|cso|feat|bugfix|ship-feature|ship|code-clean|refactor)\b|seo-analyzer|geo-analyzer|validator-analyzer|SEO\.md|HARDEN\.md|VALIDATE\.md|CSO\.md|MAX_ITERATIONS|ALL_PASS|SCORE_[A-Z_]+'
# expected: no matches. Each match is a leak — rewrite the offending
# chapter in client language before STEP 16.
```
If either gate fails, fix and re-write the markdown before continuing.
--- ---
## STEP 16 — FINAL REPORT ## STEP 16 — RENDER BRANDED HTML + PDF
Always produce a branded `.html` next to the `.md`. Produce a branded
`.pdf` when a PDF engine is available on the host. The file is the
client-visible deliverable.
### Inputs already known
| Variable | Source |
|-------------------|---------------------------------------------|
| `OUTPUT_MD` | path written in STEP 15 |
| `LANG` | from STEP 1 |
| `PROJECT_NAME` | `PROJECT_ROOT` basename or `package.json` `name` |
| `CLIENT_NAME` | from journal first entry, README, or AskUserQuestion |
| `PROJECT_PERIOD` | `<first commit date> → <last commit date>` (DD/MM/YYYY) |
| `PROJECT_URL` | `DEPLOYED_URL` from STEP 6 (or `—` if none) |
If `CLIENT_NAME` is unknown after best-effort detection, ask once with
AskUserQuestion: `"Nom du client à afficher sur la couverture du PDF
(ou laisser vide pour ne rien afficher)?"`. A blank answer becomes `—`.
### Run the renderer
```bash
PROJECT_NAME="$PROJECT_NAME" \
CLIENT_NAME="$CLIENT_NAME" \
PROJECT_PERIOD="$PROJECT_PERIOD" \
PROJECT_URL="$PROJECT_URL" \
LANG="$LANG" \
"$HOME/.claude/skills/client-handover/scripts/handover-to-pdf.sh" \
"$OUTPUT_MD"
```
The renderer:
1. Converts the markdown to HTML using the first available engine
(pandoc > python-markdown > `npx marked`).
2. Wraps the body in the ZenQuality template (cover page + branded
typography Inter + Playfair Display, ZenQuality green palette
`#1A3A25 / #2D5A3D / #4A7C59 / #87A878`, cream page background
`#F5F0EB`).
3. Embeds the ZenQuality logo (default: `https://zenquality.fr/logo-horizontal.svg`;
override with `LOGO_URL` env var to use a local file).
4. Emits `LIVRAISON.html` (or `HANDOVER.html`) next to the `.md`.
5. Tries PDF engines in order: weasyprint > wkhtmltopdf > chromium >
chromium-browser > google-chrome. First match writes
`LIVRAISON.pdf` (or `HANDOVER.pdf`).
6. If no PDF engine is available, exits with code 2 and prints
install hints. The HTML file is still produced and viewable —
the user can "Print → Save as PDF" from any modern browser.
### Exit code handling
| `$?` | Meaning | Action |
|------|-----------------------------------------------|--------|
| 0 | HTML and PDF written | continue to STEP 17 |
| 2 | HTML written, no PDF engine on host | continue to STEP 17 — final report mentions PDF as MISSING and lists install commands |
| 1 | Fatal (bad args, unwritable dir, conv error) | escalate to user with the script's stderr |
### Re-rendering on overwrite (option B in STEP 15)
If STEP 15 chose option B (`LIVRAISON-YYYY-MM-DD.md` versioned),
the renderer produces matching `LIVRAISON-YYYY-MM-DD.html` and
`LIVRAISON-YYYY-MM-DD.pdf`. Pass the versioned path as `$OUTPUT_MD`.
---
## STEP 17 — FINAL REPORT
Output to the user: Output to the user:
``` ```
DONE — ship-and-handover pipeline complete. DONE — ship-and-handover pipeline complete.
OUTPUT: <path> OUTPUT:
Markdown: <path-to-md>
HTML: <path-to-html>
PDF: <path-to-pdf> (or: NOT GENERATED — see install hints below)
LANGUAGE: fr | en LANGUAGE: fr | en
PROJECT TYPE: web (local-business) | web | cli | library | mobile | other PROJECT TYPE: web (local-business) | web | cli | library | mobile | other
COMMITS ANALYZED: <count> from <first date> to <last date> COMMITS ANALYZED: <count> from <first date> to <last date>
PIPELINE RESULT (web): PIPELINE RESULT (web):
SEO <BEFORE>/20 → <AFTER>/20 ✅ (iterations: <N>) SEO classique <BEFORE>/20 → <AFTER>/20 ✅ (iterations: <N>)
GEO (IA) <BEFORE>/20 → <AFTER>/20 ✅ (iterations: <N>)
HARDEN <BEFORE>/20 → <AFTER>/20 ✅ (iterations: <N>) HARDEN <BEFORE>/20 → <AFTER>/20 ✅ (iterations: <N>)
VALIDATE — → <AFTER>/20 ✅ (post-deploy) VALIDATE — → <AFTER>/20 ✅ (post-deploy)
@ -1092,23 +1274,29 @@ DECISIONS VULGARIZED: <count>
BLOCKERS REMAINING: <count> (open) BLOCKERS REMAINING: <count> (open)
DOC SECTIONS WRITTEN: DOC SECTIONS WRITTEN:
§1-3 Project recap §1 Ce qu'il fallait faire (et pourquoi)
§4 Site health (before/after) ← NEW §2 Ce qui a été fait (≤ 300 mots, sans jargon)
§5 Key decisions §3 Ce qui vous reste à faire (action checklist)
§6 Lessons learned (optional) §4 Détails techniques (scores, choix, glossaire)
§7 Open items / things to monitor §5 Annexe — plateformes externes (web only)
§8 Day-to-day usage §6 Annexe — build & déploiement (only if requested)
§9 Owner responsibilities checklist ← NEW
§10 SEO/GEO manual chapter (web)
§11 Build & deploy chapter (only if requested)
§12 Pour aller plus loin
Next steps for the user: Next steps for the user:
1. Read the document end-to-end before sending — fill any 1. Open <path-to-pdf> (or the .html) — verify cover page, branding,
[À COMPLÉTER] / [À CONFIRMER] markers (especially NAP in §10). and that the score table renders. Adjust the .md if needed and
2. Save a copy outside the repo (PDF, email). re-run STEP 16 to regenerate.
3. Walk through §9 (owner responsibilities) with the client during the 2. Read the document end-to-end before sending — fill any
handover meeting — it's the part they MUST act on. [À COMPLÉTER] / [À CONFIRMER] markers (NAP in §5 especially).
3. Save a copy outside the repo (the .pdf is already client-ready).
4. Walk through §3 (ce qui vous reste à faire) with the client
during the handover meeting — that's the part they MUST act on.
[If PDF was NOT generated, append:]
PDF NOT GENERATED — no PDF engine on this host. Install one of:
- weasyprint pip install --user weasyprint (or: pipx install weasyprint)
- wkhtmltopdf apt install wkhtmltopdf
- chromium apt install chromium-browser
Then re-run only STEP 16 (the .md does not need to change).
``` ```
If anything was skipped or uncertain, list under `CONCERNS:`. If anything was skipped or uncertain, list under `CONCERNS:`.

View File

@ -3,17 +3,22 @@ name: client-handover
description: | description: |
Final ship-and-handover orchestrator. End-to-end pipeline that hardens the Final ship-and-handover orchestrator. End-to-end pipeline that hardens the
project, commits, pauses for deploy, validates the live site, and only then project, commits, pauses for deploy, validates the live site, and only then
generates the non-technical client deliverable (LIVRAISON.md / HANDOVER.md). generates the non-technical client deliverable as Markdown + branded HTML +
Pipeline: (1) /seo (SEO+GEO) and /harden run in parallel with auto-fix loops PDF (ZenQuality identity: green palette, Inter + Playfair Display fonts,
until each score ≥17/20, (2) /commit-change + push if changes made, (3) pause cover page with logo and tagline). The deliverable uses a 4-chapter
to tell user what to deploy and wait for confirmation, (4) /validate against structure: §1 what was needed and why, §2 what was done (≤300 words, zero
the live site, (5) per-audit gate ≥17/20 — stop and analyze if any below, jargon, no internal tool/skill names), §3 what the client must do (action
(6) write client doc with before/after score table and explicit checklist), §4 technical details for the curious (scores, key choices,
owner-maintenance checklist. Reads git history + .claude/memory/ registries. glossary). Pipeline: (1) /seo (SEO+GEO) and /harden run in parallel with
For local-business projects, appends manual SEO/GEO platform checklist (NAP auto-fix loops until each score ≥17/20, (2) /commit-change + push if
consistency across Google Business, Pages Jaunes, Yelp, Facebook, Instagram, changes made, (3) pause to tell user what to deploy and wait for
TikTok, Apple Maps, Bing Places, TripAdvisor, etc.). Asks whether to include confirmation, (4) /validate against the live site, (5) per-axis gate
build/deploy chapter. ≥17/20 — stop and analyze if any below, (6) write client doc + render
branded HTML/PDF. Reads git history + .claude/memory/ registries. For
local-business projects, appends manual SEO/GEO platform checklist (NAP
consistency across Google Business, Pages Jaunes, Yelp, Facebook,
Instagram, TikTok, Apple Maps, Bing Places, TripAdvisor, etc.). Asks
whether to include build/deploy chapter.
Trigger: "client handover", "compte rendu client", "livraison client", Trigger: "client handover", "compte rendu client", "livraison client",
"synthese projet", "rapport client", "deliverable", "summary for client", "synthese projet", "rapport client", "deliverable", "summary for client",
"recap projet", "handover doc", "livrable", "ship and handover", "recap projet", "handover doc", "livrable", "ship and handover",
@ -51,13 +56,14 @@ The agent runs a **ship-and-handover pipeline** with explicit gates:
5. **DEPLOY PAUSE** — List exact deploy artifacts: changed files since baseline, deploy hints from project (vercel.json, netlify.toml, Dockerfile, .github/workflows/deploy.yml, etc.), and the deploy process in plain words. Use AskUserQuestion: "Deploy done? (Yes / Not yet / Skip validate)". Block until Yes or Skip. 5. **DEPLOY PAUSE** — List exact deploy artifacts: changed files since baseline, deploy hints from project (vercel.json, netlify.toml, Dockerfile, .github/workflows/deploy.yml, etc.), and the deploy process in plain words. Use AskUserQuestion: "Deploy done? (Yes / Not yet / Skip validate)". Block until Yes or Skip.
6. **/validate (live site)** — Run validator-analyzer against the deployed URL. Capture `SCORE_VALIDATE`. 6. **/validate (live site)** — Run validator-analyzer against the deployed URL. Capture `SCORE_VALIDATE`.
7. **GATE — per-axis threshold ≥17/20** — Compute final `SCORE_*_AFTER` for SEO classique, GEO (IA), HARDEN, VALIDATE. If ANY < 17/20: STOP. Generate `.claude/audits/HANDOVER-ROADMAP.md` with prioritized analysis of what's blocking each below-threshold axis. Do NOT write the client deliverable. Report to user. 7. **GATE — per-axis threshold ≥17/20** — Compute final `SCORE_*_AFTER` for SEO classique, GEO (IA), HARDEN, VALIDATE. If ANY < 17/20: STOP. Generate `.claude/audits/HANDOVER-ROADMAP.md` with prioritized analysis of what's blocking each below-threshold axis. Do NOT write the client deliverable. Report to user.
8. **DOC GENERATION (only if all scores ≥17/20)** — Read `.claude/memory/` registries + full git history. Ask whether to include build/deploy chapter. Synthesize concise client deliverable with: 8. **DOC GENERATION (only if all scores ≥17/20)** — Read `.claude/memory/` registries + full git history. Ask whether to include build/deploy chapter. Synthesize the client deliverable using the 4-chapter structure:
- Before/after score table with SEO classique and GEO (IA) on separate rows, plus HARDEN and VALIDATE — values + delta. SEO classique, GEO, HARDEN and VALIDATE are gated independently — each must reach ≥17/20 for the pipeline to pass. - **§1 Ce qu'il fallait faire (et pourquoi)** — brief + motivation, 100180 words.
- Plain-language summary of all changes since first commit. - **§2 Ce qui a été fait** — lay summary, **≤300 words, zero technical jargon**, **no internal tool/skill names** (no `/seo`, `/harden`, `/validate`, `seo-analyzer`, etc. — replace with concept names: référencement / sécurité / conformité technique). Forbidden-token grep gate runs before write.
- **Owner responsibilities** section: explicit checklist of what the client must do / maintain (SEO platforms, content updates, monitoring, deploy if self-hosted). - **§3 Ce qui vous reste à faire** — action-only checklist grouped by cadence (one-time / monthly / quarterly / yearly / when something changes).
- Optional build/deploy chapter. - **§4 Détails techniques (pour les curieux)** — score table (SEO classique + GEO + sécurité + conformité, before/after, gated independently at ≥17/20), vulgarized BDR decisions, phases with technical detail, optional glossary.
- For web projects with local-business signals: manual SEO/GEO platform checklist with registration links. - **§5 Annexe — plateformes externes** (web/local-business only).
9. **OUTPUT** — Write to `LIVRAISON.md` (fr) or `HANDOVER.md` (en) at project root. - **§6 Annexe — build & déploiement** (only if requested).
9. **RENDER** — Write `LIVRAISON.md` (fr) or `HANDOVER.md` (en) at project root, then run `scripts/handover-to-pdf.sh` to produce the matching branded `.html` (always) and `.pdf` (when a PDF engine is on the host: weasyprint > wkhtmltopdf > chromium). HTML/PDF use the ZenQuality cover page, green palette, Inter + Playfair Display typography, running header/footer with project name + page numbers.
Flags: Flags:
- `--skip-fix-loop` — run baseline audits once, skip auto-fix iterations. - `--skip-fix-loop` — run baseline audits once, skip auto-fix iterations.

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="{{LANG}}">
<head>
<meta charset="UTF-8">
<title>{{TITLE}}</title>
<style>
{{CSS}}
</style>
</head>
<body>
<section class="cover">
<div class="cover-header">
<img class="cover-logo" src="{{LOGO_URL}}" alt="ZenQuality">
<div class="cover-tagline">La sérénité numérique,<br>la qualité en plus</div>
</div>
<div class="cover-body">
<div class="cover-eyebrow">{{EYEBROW}}</div>
<h1 class="cover-title">{{COVER_TITLE}}</h1>
<p class="cover-subtitle">{{COVER_SUBTITLE}}</p>
<div class="cover-meta">
<div><strong>{{LABEL_CLIENT}}</strong> {{CLIENT_NAME}}</div>
<div><strong>{{LABEL_PROJECT}}</strong> {{PROJECT_NAME}}</div>
<div><strong>{{LABEL_DATE}}</strong> {{DATE_HUMAN}}</div>
<div><strong>{{LABEL_PERIOD}}</strong> {{PROJECT_PERIOD}}</div>
<div><strong>{{LABEL_URL}}</strong> <a href="{{PROJECT_URL}}">{{PROJECT_URL}}</a></div>
</div>
</div>
<div class="cover-footer">
<span>ZenQuality — {{LABEL_PREPARED_BY}}</span>
<a href="https://zenquality.fr">zenquality.fr</a>
</div>
</section>
<main class="content" data-title="{{TITLE}}">
{{CONTENT}}
</main>
</body>
</html>

View File

@ -0,0 +1,438 @@
/*
* ZenQuality client handover stylesheet
* Used to render LIVRAISON.md / HANDOVER.md as a branded HTML/PDF.
* Source brand tokens: zenquality.fr (CSS custom properties extracted from
* the live site) Inter (body) + Playfair Display (headings), green palette.
*/
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@400;600;700&display=swap');
:root {
--green-dark: #1A3A25;
--green-forest: #2D5A3D;
--green-moss: #4A7C59;
--green-sage: #87A878;
--black-deep: #0A0A0A;
--black-soft: #1A1A1A;
--gray-dark: #2A2A2A;
--gray-mid: #666666;
--gray-light: #B0B0B0;
--white-cream: #F5F0EB;
--white-pure: #FFFFFF;
--status-ok: #2D5A3D;
--status-warn: #b58900;
--status-fail: #a83232;
}
@page {
size: A4;
margin: 22mm 18mm 22mm 18mm;
@top-right {
content: string(doctitle);
font-family: 'Inter', sans-serif;
font-size: 8.5pt;
color: var(--green-moss);
}
@bottom-right {
content: counter(page) " / " counter(pages);
font-family: 'Inter', sans-serif;
font-size: 8.5pt;
color: var(--gray-mid);
}
@bottom-left {
content: "ZenQuality — zenquality.fr";
font-family: 'Inter', sans-serif;
font-size: 8.5pt;
color: var(--gray-mid);
}
}
@page :first {
margin: 0;
@top-right { content: ""; }
@bottom-right { content: ""; }
@bottom-left { content: ""; }
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-size: 10.5pt;
line-height: 1.6;
color: var(--black-deep);
background: var(--white-pure);
}
/* ============ COVER PAGE ============ */
.cover {
page-break-after: always;
height: 297mm;
width: 210mm;
padding: 35mm 22mm 22mm 22mm;
background:
radial-gradient(ellipse at top right, rgba(135, 168, 120, 0.18) 0%, transparent 55%),
radial-gradient(ellipse at bottom left, rgba(74, 124, 89, 0.10) 0%, transparent 55%),
var(--white-cream);
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
page: cover;
}
.cover::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 8mm;
background: linear-gradient(90deg, var(--green-dark), var(--green-forest), var(--green-moss));
}
.cover-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.cover-logo {
width: 55mm;
height: auto;
max-height: 30mm;
object-fit: contain;
}
.cover-tagline {
font-family: 'Playfair Display', Georgia, serif;
font-size: 10pt;
font-style: italic;
color: var(--green-forest);
text-align: right;
max-width: 70mm;
margin-top: 6mm;
}
.cover-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
margin: -10mm 0 0 0;
}
.cover-eyebrow {
font-family: 'Inter', sans-serif;
font-size: 9pt;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--green-moss);
margin-bottom: 6mm;
}
.cover-title {
font-family: 'Playfair Display', Georgia, serif;
font-size: 34pt;
font-weight: 700;
color: var(--green-dark);
line-height: 1.1;
margin: 0 0 6mm 0;
letter-spacing: -0.015em;
}
.cover-subtitle {
font-family: 'Playfair Display', Georgia, serif;
font-size: 16pt;
font-weight: 400;
font-style: italic;
color: var(--green-forest);
margin: 0 0 18mm 0;
max-width: 140mm;
}
.cover-meta {
font-family: 'Inter', sans-serif;
font-size: 10.5pt;
color: var(--black-soft);
line-height: 1.9;
border-left: 2px solid var(--green-moss);
padding-left: 5mm;
}
.cover-meta strong {
color: var(--green-dark);
font-weight: 600;
display: inline-block;
min-width: 25mm;
}
.cover-footer {
font-family: 'Inter', sans-serif;
font-size: 9pt;
color: var(--gray-mid);
border-top: 1px solid var(--green-sage);
padding-top: 5mm;
display: flex;
justify-content: space-between;
}
.cover-footer a {
color: var(--green-forest);
text-decoration: none;
font-weight: 500;
}
.cover-footer a:hover { color: var(--green-dark); }
/* ============ DOCUMENT BODY ============ */
.content {
string-set: doctitle attr(data-title);
}
h1 {
font-family: 'Playfair Display', Georgia, serif;
font-size: 22pt;
font-weight: 700;
color: var(--green-dark);
margin: 0 0 6mm 0;
page-break-after: avoid;
string-set: doctitle content();
}
h2 {
font-family: 'Playfair Display', Georgia, serif;
font-size: 17pt;
font-weight: 600;
color: var(--green-forest);
margin: 12mm 0 4mm 0;
padding-bottom: 2.5mm;
border-bottom: 2px solid var(--green-sage);
page-break-before: always;
page-break-after: avoid;
}
.content > h2:first-of-type,
h2.no-break,
h2.continue {
page-break-before: auto;
}
h3 {
font-family: 'Playfair Display', Georgia, serif;
font-size: 13.5pt;
font-weight: 600;
color: var(--green-forest);
margin: 8mm 0 3mm 0;
page-break-after: avoid;
}
h4 {
font-family: 'Inter', sans-serif;
font-size: 10pt;
font-weight: 600;
color: var(--green-moss);
margin: 6mm 0 2mm 0;
text-transform: uppercase;
letter-spacing: 0.06em;
page-break-after: avoid;
}
p { margin: 0 0 3mm 0; }
p, li { orphans: 3; widows: 3; }
ul, ol { margin: 0 0 3mm 0; padding-left: 6mm; }
ul li, ol li { margin: 0 0 1.5mm 0; }
ul li::marker { color: var(--green-moss); }
ol li::marker { color: var(--green-moss); font-weight: 600; }
strong { color: var(--green-dark); font-weight: 600; }
em { color: var(--green-forest); font-style: italic; }
blockquote {
border-left: 3px solid var(--green-moss);
padding: 3mm 5mm;
margin: 4mm 0;
background: var(--white-cream);
color: var(--gray-dark);
font-style: italic;
page-break-inside: avoid;
}
blockquote p:last-child { margin-bottom: 0; }
a { color: var(--green-forest); text-decoration: underline; text-decoration-thickness: 0.5pt; text-underline-offset: 1.5pt; }
a:hover { color: var(--green-dark); }
code {
font-family: 'JetBrains Mono', 'Fira Code', Menlo, monospace;
font-size: 9pt;
background: var(--white-cream);
padding: 0.5mm 1.5mm;
border-radius: 1mm;
color: var(--green-dark);
}
pre {
background: var(--white-cream);
padding: 4mm 5mm;
border-radius: 1.5mm;
border-left: 3px solid var(--green-moss);
font-size: 8.5pt;
line-height: 1.45;
white-space: pre-wrap;
word-wrap: break-word;
page-break-inside: avoid;
margin: 4mm 0;
}
pre code { background: none; padding: 0; color: var(--black-deep); font-size: inherit; }
/* ============ TABLES ============ */
table {
width: 100%;
border-collapse: collapse;
margin: 4mm 0;
font-size: 9.5pt;
page-break-inside: avoid;
}
th {
font-family: 'Inter', sans-serif;
background: var(--green-forest);
color: var(--white-pure);
text-align: left;
padding: 2.5mm 3mm;
font-weight: 600;
font-size: 9pt;
text-transform: uppercase;
letter-spacing: 0.04em;
border-bottom: 0;
}
td {
padding: 2.5mm 3mm;
border-bottom: 1px solid var(--green-sage);
vertical-align: top;
}
tr:nth-child(even) td { background: rgba(245, 240, 235, 0.55); }
/* Numeric / status cols of score tables auto-detected via header text */
table th:nth-child(2),
table th:nth-child(3),
table th:nth-child(4),
table td:nth-child(2),
table td:nth-child(3),
table td:nth-child(4) {
text-align: right;
font-variant-numeric: tabular-nums;
}
table th:last-child,
table td:last-child {
text-align: center;
}
/* ============ CHECKLISTS ============ */
ul.checklist,
ul.task-list {
list-style: none;
padding-left: 0;
}
ul.checklist li,
ul.task-list li {
padding-left: 8mm;
position: relative;
margin-bottom: 2.5mm;
}
ul.checklist li::before,
ul.task-list li::before,
li input[type="checkbox"] + *,
li.task-list-item::before {
content: "☐";
position: absolute;
left: 0;
color: var(--green-moss);
font-size: 12pt;
line-height: 1;
}
input[type="checkbox"] {
display: none;
}
input[type="checkbox"]:checked + label::before {
content: "☑";
color: var(--green-forest);
}
/* ============ CALLOUTS ============ */
.callout {
padding: 4mm 6mm;
margin: 4mm 0;
border-radius: 2mm;
page-break-inside: avoid;
font-size: 10pt;
}
.callout.info {
background: var(--white-cream);
border-left: 4px solid var(--green-moss);
}
.callout.warn {
background: #fdf6e3;
border-left: 4px solid var(--status-warn);
}
.callout.success {
background: rgba(135, 168, 120, 0.14);
border-left: 4px solid var(--green-forest);
}
.callout-title {
font-family: 'Inter', sans-serif;
font-weight: 600;
font-size: 10pt;
color: var(--green-dark);
margin-bottom: 2mm;
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* ============ SECTION DIVIDERS ============ */
hr {
border: none;
border-top: 1px solid var(--green-sage);
margin: 8mm 0;
}
/* ============ STATUS PILLS (used by text replacement) ============ */
.status-ok { color: var(--status-ok); font-weight: 600; }
.status-warn { color: var(--status-warn); font-weight: 600; }
.status-fail { color: var(--status-fail); font-weight: 600; }
/* ============ LINK BEHAVIOR IN PRINT ============ */
@media print {
a[href^="http"]::after {
content: " (" attr(href) ")";
font-size: 7.5pt;
color: var(--gray-mid);
font-style: italic;
font-weight: 400;
}
a[href^="#"]::after,
a[href^="mailto:"]::after,
a[href^="tel:"]::after,
.cover a::after,
table a::after { content: ""; }
}

View File

@ -0,0 +1,265 @@
#!/usr/bin/env bash
#
# handover-to-pdf.sh
# ------------------
# Renders a client-handover Markdown report (LIVRAISON.md / HANDOVER.md)
# into a branded HTML and (when a converter is available) a PDF using
# ZenQuality brand styling.
#
# Inputs:
# $1 Path to the source Markdown file (required)
#
# Optional environment variables:
# PROJECT_NAME Displayed on the cover and as PDF page header.
# Defaults to the source filename.
# CLIENT_NAME Displayed on the cover. Defaults to "—".
# PROJECT_PERIOD Displayed on the cover (e.g. "01/01/2026 → 31/03/2026").
# Defaults to "—".
# PROJECT_URL Displayed on the cover. Defaults to "—".
# LANG "fr" (default) or "en". Drives cover labels.
# COVER_TITLE Defaults to PROJECT_NAME.
# COVER_SUBTITLE Defaults to "Compte rendu de livraison" (fr) /
# "Project handover recap" (en).
# EYEBROW Eyebrow line above the title. Defaults to
# "Livraison" / "Handover".
# LOGO_URL Logo URL or local path. Defaults to a remote
# ZenQuality logo (no offline fallback).
# BRANDING_DIR Override branding-asset directory. Defaults to the
# resources/branding/ folder next to this script.
#
# Behaviour:
# 1. Convert the Markdown body to HTML.
# 2. Wrap it in the ZenQuality template (cover + branded body).
# 3. Convert that HTML into a PDF using the first available engine:
# weasyprint > wkhtmltopdf > chromium > headless Chrome
# 4. Always keep the .html file next to the .md.
# 5. If no PDF engine is available, exit with code 2 and a clear
# message — never fail silently.
#
# Exit codes:
# 0 HTML and PDF written successfully.
# 1 Fatal error (bad arguments, missing files, conversion error).
# 2 HTML written but no PDF engine available — manual print needed.
set -euo pipefail
# ---------------------------- CLI ----------------------------------
if [ "$#" -lt 1 ]; then
echo "usage: handover-to-pdf.sh <markdown-file>" >&2
exit 1
fi
SRC_MD="$1"
if [ ! -f "$SRC_MD" ]; then
echo "error: markdown file not found: $SRC_MD" >&2
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_BRANDING_DIR="$SCRIPT_DIR/../resources/branding"
BRANDING_DIR="${BRANDING_DIR:-$DEFAULT_BRANDING_DIR}"
if [ ! -f "$BRANDING_DIR/zenquality.css" ] || [ ! -f "$BRANDING_DIR/zenquality-template.html" ]; then
echo "error: branding assets missing under $BRANDING_DIR" >&2
echo " expected: zenquality.css + zenquality-template.html" >&2
exit 1
fi
OUT_DIR="$(cd "$(dirname "$SRC_MD")" && pwd)"
BASE="$(basename "$SRC_MD" .md)"
OUT_HTML="$OUT_DIR/$BASE.html"
OUT_PDF="$OUT_DIR/$BASE.pdf"
LANG_CODE="${LANG:-fr}"
case "$LANG_CODE" in
en|EN|en_*|en-*)
LANG_CODE="en"
EYEBROW="${EYEBROW:-Project handover}"
DEFAULT_SUBTITLE="Project handover recap"
LABEL_CLIENT="Client"
LABEL_PROJECT="Project"
LABEL_DATE="Issued"
LABEL_PERIOD="Period"
LABEL_URL="Website"
LABEL_PREPARED_BY="prepared for the client"
;;
*)
LANG_CODE="fr"
EYEBROW="${EYEBROW:-Livraison}"
DEFAULT_SUBTITLE="Compte rendu de livraison"
LABEL_CLIENT="Client"
LABEL_PROJECT="Projet"
LABEL_DATE="Date d'émission"
LABEL_PERIOD="Période"
LABEL_URL="Site"
LABEL_PREPARED_BY="préparé pour le client"
;;
esac
PROJECT_NAME_RESOLVED="${PROJECT_NAME:-$BASE}"
CLIENT_NAME_RESOLVED="${CLIENT_NAME:-}"
PROJECT_PERIOD_RESOLVED="${PROJECT_PERIOD:-}"
PROJECT_URL_RESOLVED="${PROJECT_URL:-}"
COVER_TITLE_RESOLVED="${COVER_TITLE:-$PROJECT_NAME_RESOLVED}"
COVER_SUBTITLE_RESOLVED="${COVER_SUBTITLE:-$DEFAULT_SUBTITLE}"
LOGO_URL_RESOLVED="${LOGO_URL:-https://zenquality.fr/logo-horizontal.svg}"
if command -v date >/dev/null 2>&1; then
if [ "$LANG_CODE" = "fr" ]; then
DATE_HUMAN="$(LC_ALL=fr_FR.UTF-8 date "+%d %B %Y" 2>/dev/null || date "+%Y-%m-%d")"
else
DATE_HUMAN="$(LC_ALL=en_US.UTF-8 date "+%d %B %Y" 2>/dev/null || date "+%Y-%m-%d")"
fi
else
DATE_HUMAN="$(date "+%Y-%m-%d")"
fi
# ---------------------------- MD -> HTML ---------------------------
md_to_html_body() {
local src="$1"
if command -v pandoc >/dev/null 2>&1; then
pandoc --from=gfm --to=html5 --no-highlight "$src"
return
fi
if command -v python3 >/dev/null 2>&1 && python3 -c "import markdown" >/dev/null 2>&1; then
python3 -c "
import sys, markdown
src = open(sys.argv[1], encoding='utf-8').read()
print(markdown.markdown(
src,
extensions=['extra', 'tables', 'sane_lists', 'toc'],
))" "$src"
return
fi
if command -v npx >/dev/null 2>&1; then
npx --yes marked < "$src"
return
fi
echo "error: no Markdown converter available (need pandoc, python3+markdown, or npx)" >&2
exit 1
}
BODY_HTML="$(md_to_html_body "$SRC_MD")"
# ---------------------------- WRAP HTML ----------------------------
CSS_CONTENT="$(cat "$BRANDING_DIR/zenquality.css")"
render_template() {
# Read template path from $1, output the substituted HTML on stdout.
# Substitution variables are pulled from HQ_* environment variables.
HQ_TEMPLATE_PATH="$1" python3 <<'PY'
import os, sys
path = os.environ["HQ_TEMPLATE_PATH"]
with open(path, encoding="utf-8") as f:
template = f.read()
mapping = {
"{{LANG}}": os.environ.get("HQ_LANG", "fr"),
"{{TITLE}}": os.environ.get("HQ_TITLE", ""),
"{{CSS}}": os.environ.get("HQ_CSS", ""),
"{{LOGO_URL}}": os.environ.get("HQ_LOGO_URL", ""),
"{{EYEBROW}}": os.environ.get("HQ_EYEBROW", ""),
"{{COVER_TITLE}}": os.environ.get("HQ_COVER_TITLE", ""),
"{{COVER_SUBTITLE}}": os.environ.get("HQ_COVER_SUBTITLE", ""),
"{{CLIENT_NAME}}": os.environ.get("HQ_CLIENT_NAME", "—"),
"{{PROJECT_NAME}}": os.environ.get("HQ_PROJECT_NAME", ""),
"{{DATE_HUMAN}}": os.environ.get("HQ_DATE_HUMAN", ""),
"{{PROJECT_PERIOD}}": os.environ.get("HQ_PROJECT_PERIOD", "—"),
"{{PROJECT_URL}}": os.environ.get("HQ_PROJECT_URL", "—"),
"{{LABEL_CLIENT}}": os.environ.get("HQ_LABEL_CLIENT", ""),
"{{LABEL_PROJECT}}": os.environ.get("HQ_LABEL_PROJECT", ""),
"{{LABEL_DATE}}": os.environ.get("HQ_LABEL_DATE", ""),
"{{LABEL_PERIOD}}": os.environ.get("HQ_LABEL_PERIOD", ""),
"{{LABEL_URL}}": os.environ.get("HQ_LABEL_URL", ""),
"{{LABEL_PREPARED_BY}}": os.environ.get("HQ_LABEL_PREPARED_BY", ""),
"{{CONTENT}}": os.environ.get("HQ_CONTENT", ""),
}
for k, v in mapping.items():
template = template.replace(k, v)
sys.stdout.write(template)
PY
}
export HQ_LANG="$LANG_CODE"
export HQ_TITLE="$COVER_TITLE_RESOLVED"
export HQ_CSS="$CSS_CONTENT"
export HQ_LOGO_URL="$LOGO_URL_RESOLVED"
export HQ_EYEBROW="$EYEBROW"
export HQ_COVER_TITLE="$COVER_TITLE_RESOLVED"
export HQ_COVER_SUBTITLE="$COVER_SUBTITLE_RESOLVED"
export HQ_CLIENT_NAME="$CLIENT_NAME_RESOLVED"
export HQ_PROJECT_NAME="$PROJECT_NAME_RESOLVED"
export HQ_DATE_HUMAN="$DATE_HUMAN"
export HQ_PROJECT_PERIOD="$PROJECT_PERIOD_RESOLVED"
export HQ_PROJECT_URL="$PROJECT_URL_RESOLVED"
export HQ_LABEL_CLIENT="$LABEL_CLIENT"
export HQ_LABEL_PROJECT="$LABEL_PROJECT"
export HQ_LABEL_DATE="$LABEL_DATE"
export HQ_LABEL_PERIOD="$LABEL_PERIOD"
export HQ_LABEL_URL="$LABEL_URL"
export HQ_LABEL_PREPARED_BY="$LABEL_PREPARED_BY"
export HQ_CONTENT="$BODY_HTML"
render_template "$BRANDING_DIR/zenquality-template.html" > "$OUT_HTML"
echo "wrote: $OUT_HTML"
# ---------------------------- HTML -> PDF --------------------------
PDF_ENGINE=""
PDF_REASON=""
if command -v weasyprint >/dev/null 2>&1; then
PDF_ENGINE="weasyprint"
elif command -v wkhtmltopdf >/dev/null 2>&1; then
PDF_ENGINE="wkhtmltopdf"
elif command -v chromium >/dev/null 2>&1; then
PDF_ENGINE="chromium"
elif command -v chromium-browser >/dev/null 2>&1; then
PDF_ENGINE="chromium-browser"
elif command -v google-chrome >/dev/null 2>&1; then
PDF_ENGINE="google-chrome"
else
PDF_REASON="no PDF engine found (looked for: weasyprint, wkhtmltopdf, chromium, google-chrome)"
fi
if [ -n "$PDF_ENGINE" ]; then
case "$PDF_ENGINE" in
weasyprint)
weasyprint --base-url "$OUT_DIR/" "$OUT_HTML" "$OUT_PDF"
;;
wkhtmltopdf)
wkhtmltopdf --enable-local-file-access \
--margin-top 0 --margin-bottom 0 \
--margin-left 0 --margin-right 0 \
--print-media-type \
"$OUT_HTML" "$OUT_PDF"
;;
chromium|chromium-browser|google-chrome)
"$PDF_ENGINE" --headless --disable-gpu --no-sandbox \
--no-pdf-header-footer \
--print-to-pdf="$OUT_PDF" \
--print-to-pdf-no-header \
"file://$OUT_HTML"
;;
esac
echo "wrote: $OUT_PDF (engine: $PDF_ENGINE)"
exit 0
fi
cat <<EOF >&2
note: HTML written, but no PDF engine is available.
reason: $PDF_REASON
To generate $OUT_PDF, install one of:
- weasyprint pip install --user weasyprint
- wkhtmltopdf apt install wkhtmltopdf (or download from wkhtmltopdf.org)
- chromium apt install chromium-browser
Or open $OUT_HTML in a browser and use "Print → Save as PDF".
EOF
exit 2