diff --git a/agents/client-handover-writer.md b/agents/client-handover-writer.md index 789cb04..9b10b16 100644 --- a/agents/client-handover-writer.md +++ b/agents/client-handover-writer.md @@ -1,6 +1,6 @@ --- 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 model: opus --- @@ -9,19 +9,35 @@ model: opus ## GOAL -Orchestrate a final **ship-and-handover pipeline** then produce a single Markdown -deliverable (`LIVRAISON.md` or `HANDOVER.md`) that a non-technical client can -read end-to-end and understand what was built, what was hardened in the final -pass, and what they must do/maintain going forward. +Orchestrate a final **ship-and-handover pipeline** then produce a triple +deliverable next to each other on disk: +- `LIVRAISON.md` / `HANDOVER.md` — source markdown (editable) +- `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): -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. 3. Commit + push if files changed. 4. Deploy pause: list deploy artifacts + process, wait for user confirmation. -5. /validate against the deployed/live site. -6. Per-audit gate: every score ≥17/20 OR stop + roadmap. -7. Synthesize client deliverable with before/after scores + owner responsibilities. +5. Live-site validation against the deployed URL. +6. Per-axis gate: every score ≥17/20 OR stop + roadmap. +7. Synthesize the markdown + render the branded HTML + PDF. Source of truth for the deliverable: git history since first commit + `.claude/memory/` 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 -Generate the deliverable section by section. Translate headings to `LANG`. -Tone: friendly, concrete, no jargon. One short paragraph per idea. +Generate the deliverable as a tight 4-chapter structure: what was needed, +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 1–3.** + 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 @@ -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 > 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. 100–180 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 (3–5 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 -shown on separate rows so the client sees both axes explicitly.] + Bullet list (5–10 items): visible benefits, each phrased as something + 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 | -|------------------------------------------|-----------:|-----------:|:------:| -| Référencement Google (SEO classique) | /20 | /20 | ✅ | -| Visibilité IA (GEO — ChatGPT, Perplexity)| /20 | /20 | ✅ | -| Sécurité du site | /20 | /20 | ✅ | -| Conformité technique (W3C) | — | /20 | ✅ | +After drafting, count words. Cap at 300. If over, cut paragraphs not +bullets — bullets are the value-dense part.] -[If LANG=en: "Site health (before / after)" with the same columns. -Use these column labels: "Domain" / "Before" / "After" / "Status". -Row labels: "Google search (classical SEO)", "AI visibility (GEO — -ChatGPT, Perplexity)", "Site security", "Technical compliance (W3C)".] +## 3. Ce qui vous reste à faire -Plain explanation under the table: -- **Référencement Google (SEO classique)** = comment Google, Bing et - les autres moteurs traditionnels trouvent et classent votre site. - 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. +[Action-only checklist for the client. Pull from: open `blockers.md` +entries, ongoing-monitoring items, external platforms to claim, +content updates only the client can make, deploy steps if self-hosted. -[If any score had a notable jump, add a one-liner: "La sécurité est passée -de 12 à 18 — on a ajouté les en-têtes manquants et forcé le passage en -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: +Format as a checklist grouped by cadence. Every line starts with a +verb. Every line is something the client can do without a developer. ### Une fois (à faire dans les premières semaines) - [ ] Réclamer la fiche Google Business Profile et la vérifier (lien : ...) - [ ] Compléter le profil Apple Business Connect (lien : ...) -- [ ] Vérifier la cohérence NAP (Nom / Adresse / Téléphone) sur toutes - les plateformes — voir tableau au §10 -- [ ] [Si self-host : configurer le certificat SSL (renouvellement auto Let's Encrypt)] -- [ ] [Si self-host : programmer une sauvegarde quotidienne] +- [ ] Vérifier la cohérence Nom / Adresse / Téléphone sur toutes les + plateformes — voir l'annexe à la fin du document +- [ ] [Si vous gérez l'hébergement vous-même : configurer le certificat + 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) ### Mensuel -- [ ] Ajouter / mettre à jour 5 photos sur Google Business -- [ ] Répondre aux avis Google (positifs et négatifs) +- [ ] Ajouter ou mettre à jour 5 photos sur Google Business +- [ ] Répondre aux avis Google (positifs et négatifs) sous 48 h - [ ] Vérifier que le site est toujours en ligne (test simple : ouvrir 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 - [ ] Faire un test de visibilité IA : taper le nom du commerce dans ChatGPT, Perplexity, Gemini. Noter ce qui s'affiche. -- [ ] Demander à 3-5 clients de laisser un avis Google +- [ ] Demander à 3–5 clients de laisser un avis Google - [ ] Publier un post Google Business (offre, événement, actualité) ### Annuel @@ -934,25 +950,90 @@ Format as actionable checklist grouped by cadence: - [ ] Renouveler les noms de domaine ### Quand quelque chose change dans la vie du commerce -- [ ] Changement d'adresse / téléphone / horaires → modifier d'abord sur - Google Business, puis sur toutes les autres plateformes (la - cohérence est cruciale, voir §10) +- [ ] Changement d'adresse, de téléphone ou d'horaires → modifier + d'abord sur Google Business, puis sur toutes les autres + plateformes (la cohérence est cruciale) [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)| /20 | /20 | ✅ | +| Visibilité IA (ChatGPT, Perplexity, Gemini)| /20 | /20 | ✅ | +| Sécurité du site | /20 | /20 | ✅ | +| Conformité technique | — | /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 3–7 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) + +[3–7 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 1. Address the client directly ("votre site", "vous pouvez"). -2. Replace tech terms with user-facing equivalents. -3. No abbreviations the client wouldn't use. +2. Chapters 1–3: replace every tech term with a user-facing equivalent. +3. No abbreviations the client wouldn't use (HTTPS yes, CSP no — unless + in chapter 4 with definition). 4. Concrete numbers > adjectives. 5. Short paragraphs. Bullet lists for things you can count. 6. **Score deltas explained in plain words**. Never just dump numbers. -7. **Owner-responsibility section is action-oriented**. Every line starts - with a verb. Every line is something the client can do without a dev. +7. **Chapter 3 is action-oriented**. Every line starts with a verb. + Every line is something the client can do without a developer. +8. **No skill-name leaks in chapters 1–3.** See "Hard rules" above. --- ## STEP 13 — SEO/GEO MANUAL CHECKLIST (web projects only) 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: `$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) -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). 2. First-time setup (numbered steps + signup link). 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. - `LIVRAISON.md` if `LANG=fr` @@ -1058,30 +1145,125 @@ If a file at that path already exists, AskUserQuestion: Write the file with the `Write` tool. -Sanity check: +Sanity checks (do them in this order, before STEP 16): + ```bash -wc -l # expect 200-800 lines -grep -c "^## " # expect 8-13 chapters (NEW: §4 health, §9 owner-resp) +wc -l # expect 250-800 lines +grep -c "^## " # 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 +1–3 (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` | `` (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: ``` DONE — ship-and-handover pipeline complete. -OUTPUT: +OUTPUT: + Markdown: + HTML: + PDF: (or: NOT GENERATED — see install hints below) LANGUAGE: fr | en PROJECT TYPE: web (local-business) | web | cli | library | mobile | other COMMITS ANALYZED: from to PIPELINE RESULT (web): - SEO /20 → /20 ✅ (iterations: ) - HARDEN /20 → /20 ✅ (iterations: ) - VALIDATE — → /20 ✅ (post-deploy) + SEO classique /20 → /20 ✅ (iterations: ) + GEO (IA) /20 → /20 ✅ (iterations: ) + HARDEN /20 → /20 ✅ (iterations: ) + VALIDATE — → /20 ✅ (post-deploy) PIPELINE RESULT (non-web): CSO /20 → /20 ✅ (iterations: ) @@ -1092,23 +1274,29 @@ DECISIONS VULGARIZED: BLOCKERS REMAINING: (open) DOC SECTIONS WRITTEN: - §1-3 Project recap - §4 Site health (before/after) ← NEW - §5 Key decisions - §6 Lessons learned (optional) - §7 Open items / things to monitor - §8 Day-to-day usage - §9 Owner responsibilities checklist ← NEW - §10 SEO/GEO manual chapter (web) - §11 Build & deploy chapter (only if requested) - §12 Pour aller plus loin + §1 Ce qu'il fallait faire (et pourquoi) + §2 Ce qui a été fait (≤ 300 mots, sans jargon) + §3 Ce qui vous reste à faire (action checklist) + §4 Détails techniques (scores, choix, glossaire) + §5 Annexe — plateformes externes (web only) + §6 Annexe — build & déploiement (only if requested) Next steps for the user: -1. Read the document end-to-end before sending — fill any - [À COMPLÉTER] / [À CONFIRMER] markers (especially NAP in §10). -2. Save a copy outside the repo (PDF, email). -3. Walk through §9 (owner responsibilities) with the client during the - handover meeting — it's the part they MUST act on. +1. Open (or the .html) — verify cover page, branding, + and that the score table renders. Adjust the .md if needed and + re-run STEP 16 to regenerate. +2. Read the document end-to-end before sending — fill any + [À 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:`. diff --git a/skills/client-handover/SKILL.md b/skills/client-handover/SKILL.md index 62f1872..42f2eac 100644 --- a/skills/client-handover/SKILL.md +++ b/skills/client-handover/SKILL.md @@ -3,17 +3,22 @@ name: client-handover description: | Final ship-and-handover orchestrator. End-to-end pipeline that hardens the project, commits, pauses for deploy, validates the live site, and only then - generates the non-technical client deliverable (LIVRAISON.md / HANDOVER.md). - Pipeline: (1) /seo (SEO+GEO) and /harden run in parallel with auto-fix loops - until each score ≥17/20, (2) /commit-change + push if changes made, (3) pause - to tell user what to deploy and wait for confirmation, (4) /validate against - the live site, (5) per-audit gate ≥17/20 — stop and analyze if any below, - (6) write client doc with before/after score table and explicit - owner-maintenance checklist. 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. + generates the non-technical client deliverable as Markdown + branded HTML + + PDF (ZenQuality identity: green palette, Inter + Playfair Display fonts, + cover page with logo and tagline). The deliverable uses a 4-chapter + structure: §1 what was needed and why, §2 what was done (≤300 words, zero + jargon, no internal tool/skill names), §3 what the client must do (action + checklist), §4 technical details for the curious (scores, key choices, + glossary). Pipeline: (1) /seo (SEO+GEO) and /harden run in parallel with + auto-fix loops until each score ≥17/20, (2) /commit-change + push if + changes made, (3) pause to tell user what to deploy and wait for + confirmation, (4) /validate against the live site, (5) per-axis gate + ≥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", "synthese projet", "rapport client", "deliverable", "summary for client", "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. 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. -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: - - 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. - - Plain-language summary of all changes since first commit. - - **Owner responsibilities** section: explicit checklist of what the client must do / maintain (SEO platforms, content updates, monitoring, deploy if self-hosted). - - Optional build/deploy chapter. - - For web projects with local-business signals: manual SEO/GEO platform checklist with registration links. -9. **OUTPUT** — Write to `LIVRAISON.md` (fr) or `HANDOVER.md` (en) at project root. +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: + - **§1 Ce qu'il fallait faire (et pourquoi)** — brief + motivation, 100–180 words. + - **§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. + - **§3 Ce qui vous reste à faire** — action-only checklist grouped by cadence (one-time / monthly / quarterly / yearly / when something changes). + - **§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. + - **§5 Annexe — plateformes externes** (web/local-business only). + - **§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: - `--skip-fix-loop` — run baseline audits once, skip auto-fix iterations. diff --git a/skills/client-handover/resources/branding/zenquality-template.html b/skills/client-handover/resources/branding/zenquality-template.html new file mode 100644 index 0000000..a428819 --- /dev/null +++ b/skills/client-handover/resources/branding/zenquality-template.html @@ -0,0 +1,43 @@ + + + + + {{TITLE}} + + + + +
+
+ +
La sérénité numérique,
la qualité en plus
+
+ +
+
{{EYEBROW}}
+

{{COVER_TITLE}}

+

{{COVER_SUBTITLE}}

+ +
+
{{LABEL_CLIENT}} {{CLIENT_NAME}}
+
{{LABEL_PROJECT}} {{PROJECT_NAME}}
+
{{LABEL_DATE}} {{DATE_HUMAN}}
+
{{LABEL_PERIOD}} {{PROJECT_PERIOD}}
+
{{LABEL_URL}} {{PROJECT_URL}}
+
+
+ + +
+ +
+{{CONTENT}} +
+ + + diff --git a/skills/client-handover/resources/branding/zenquality.css b/skills/client-handover/resources/branding/zenquality.css new file mode 100644 index 0000000..f299afd --- /dev/null +++ b/skills/client-handover/resources/branding/zenquality.css @@ -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: ""; } +} diff --git a/skills/client-handover/scripts/handover-to-pdf.sh b/skills/client-handover/scripts/handover-to-pdf.sh new file mode 100755 index 0000000..238787c --- /dev/null +++ b/skills/client-handover/scripts/handover-to-pdf.sh @@ -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 " >&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 <&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