fix(client-handover): PDF render bugs — marked CLI, white cover, PNG logo

Three bugs surfaced on the LIVRAISON.pdf test render:

1. **MD→HTML conversion** — host had no pandoc, no python-markdown, fell
   back to `npx marked < "$src"`. marked CLI 16.x ignores stdin and
   dumps its own cli.js source. Resulting PDF body = marked's binary
   source (`#!/usr/bin/env node`, `Marked CLI`, copyright). Fix:
   `npx --yes marked --gfm -i "$src"` (file path via -i, not stdin).

2. **Cover background** — original cream `#F5F0EB` + 8mm green stripe
   was washed out. Iterated to white-pure bg with subtle radial
   sage/forest tints, black-deep title, green-forest accents
   (eyebrow, meta labels, footer, border). Solid green-dark tried
   first then rejected (too heavy for long client-facing doc).

3. **Default logo** — SVG `logo-horizontal.svg` rendered cream-toned,
   blended into bg. Switched LOGO_URL default to
   `https://zenquality.fr/assets/logo-horizontal-1024.png`.

Also added test-artifact gitignore rules for LIVRAISON.* / HANDOVER.*
project-local renders.

Verified: regenerated LIVRAISON.pdf → 164 KB, 19 pages, full content
rendered, white cover with black title + green-forest accents +
visible PNG logo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
bastien 2026-05-08 19:57:08 +02:00
parent 591a035ec6
commit b447a892b1
4 changed files with 36 additions and 26 deletions

8
.gitignore vendored
View File

@ -86,6 +86,14 @@ hooks/node_modules/
graphify-out/ graphify-out/
.ctx7-cache/ .ctx7-cache/
# /client-handover test artifacts (project-local renders)
LIVRAISON.md
LIVRAISON.html
LIVRAISON.pdf
HANDOVER.md
HANDOVER.html
HANDOVER.pdf
# Install logs # Install logs
install-*.log install-*.log

View File

@ -1216,9 +1216,12 @@ The renderer:
(pandoc > python-markdown > `npx marked`). (pandoc > python-markdown > `npx marked`).
2. Wraps the body in the ZenQuality template (cover page + branded 2. Wraps the body in the ZenQuality template (cover page + branded
typography Inter + Playfair Display, ZenQuality green palette typography Inter + Playfair Display, ZenQuality green palette
`#1A3A25 / #2D5A3D / #4A7C59 / #87A878`, cream page background `#1A3A25 / #2D5A3D / #4A7C59 / #87A878`, **white cover**
`#F5F0EB`). (`--white-pure`) with black-deep title and green-forest accents
3. Embeds the ZenQuality logo (default: `https://zenquality.fr/logo-horizontal.svg`; (eyebrow, meta labels, footer); subtle radial sage + forest tints
add depth. Cream `#F5F0EB` reserved for body code/blockquote
accents — not page bg).
3. Embeds the ZenQuality logo (default: `https://zenquality.fr/assets/logo-horizontal-1024.png`;
override with `LOGO_URL` env var to use a local file). override with `LOGO_URL` env var to use a local file).
4. Emits `LIVRAISON.html` (or `HANDOVER.html`) next to the `.md`. 4. Emits `LIVRAISON.html` (or `HANDOVER.html`) next to the `.md`.
5. Tries PDF engines in order: weasyprint > wkhtmltopdf > chromium > 5. Tries PDF engines in order: weasyprint > wkhtmltopdf > chromium >

View File

@ -75,8 +75,9 @@ html, body {
padding: 35mm 22mm 22mm 22mm; padding: 35mm 22mm 22mm 22mm;
background: background:
radial-gradient(ellipse at top right, rgba(135, 168, 120, 0.18) 0%, transparent 55%), 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%), radial-gradient(ellipse at bottom left, rgba(45, 90, 61, 0.06) 0%, transparent 55%),
var(--white-cream); var(--white-pure);
color: var(--black-deep);
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -84,16 +85,6 @@ html, body {
page: cover; 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 { .cover-header {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@ -131,7 +122,7 @@ html, body {
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.18em; letter-spacing: 0.18em;
color: var(--green-moss); color: var(--green-forest);
margin-bottom: 6mm; margin-bottom: 6mm;
} }
@ -139,7 +130,7 @@ html, body {
font-family: 'Playfair Display', Georgia, serif; font-family: 'Playfair Display', Georgia, serif;
font-size: 34pt; font-size: 34pt;
font-weight: 700; font-weight: 700;
color: var(--green-dark); color: var(--black-deep);
line-height: 1.1; line-height: 1.1;
margin: 0 0 6mm 0; margin: 0 0 6mm 0;
letter-spacing: -0.015em; letter-spacing: -0.015em;
@ -158,36 +149,42 @@ html, body {
.cover-meta { .cover-meta {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
font-size: 10.5pt; font-size: 10.5pt;
color: var(--black-soft); color: var(--black-deep);
line-height: 1.9; line-height: 1.9;
border-left: 2px solid var(--green-moss); border-left: 2px solid var(--green-forest);
padding-left: 5mm; padding-left: 5mm;
} }
.cover-meta strong { .cover-meta strong {
color: var(--green-dark); color: var(--green-forest);
font-weight: 600; font-weight: 600;
display: inline-block; display: inline-block;
min-width: 25mm; min-width: 25mm;
} }
.cover-meta a {
color: var(--black-deep);
text-decoration: underline;
text-decoration-color: rgba(45, 90, 61, 0.6);
}
.cover-footer { .cover-footer {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
font-size: 9pt; font-size: 9pt;
color: var(--gray-mid); color: var(--green-forest);
border-top: 1px solid var(--green-sage); border-top: 1px solid rgba(45, 90, 61, 0.4);
padding-top: 5mm; padding-top: 5mm;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.cover-footer a { .cover-footer a {
color: var(--green-forest); color: var(--black-deep);
text-decoration: none; text-decoration: none;
font-weight: 500; font-weight: 500;
} }
.cover-footer a:hover { color: var(--green-dark); } .cover-footer a:hover { color: var(--green-forest); }
/* ============ DOCUMENT BODY ============ */ /* ============ DOCUMENT BODY ============ */
.content { .content {

View File

@ -104,7 +104,7 @@ PROJECT_PERIOD_RESOLVED="${PROJECT_PERIOD:-—}"
PROJECT_URL_RESOLVED="${PROJECT_URL:-}" PROJECT_URL_RESOLVED="${PROJECT_URL:-}"
COVER_TITLE_RESOLVED="${COVER_TITLE:-$PROJECT_NAME_RESOLVED}" COVER_TITLE_RESOLVED="${COVER_TITLE:-$PROJECT_NAME_RESOLVED}"
COVER_SUBTITLE_RESOLVED="${COVER_SUBTITLE:-$DEFAULT_SUBTITLE}" COVER_SUBTITLE_RESOLVED="${COVER_SUBTITLE:-$DEFAULT_SUBTITLE}"
LOGO_URL_RESOLVED="${LOGO_URL:-https://zenquality.fr/logo-horizontal.svg}" LOGO_URL_RESOLVED="${LOGO_URL:-https://zenquality.fr/assets/logo-horizontal-1024.png}"
if command -v date >/dev/null 2>&1; then if command -v date >/dev/null 2>&1; then
if [ "$LANG_CODE" = "fr" ]; then if [ "$LANG_CODE" = "fr" ]; then
@ -135,7 +135,9 @@ print(markdown.markdown(
return return
fi fi
if command -v npx >/dev/null 2>&1; then if command -v npx >/dev/null 2>&1; then
npx --yes marked < "$src" # marked CLI 16.x ignores stdin and dumps its own cli.js source —
# always pass the file via -i to get correct output.
npx --yes marked --gfm -i "$src"
return return
fi fi
echo "error: no Markdown converter available (need pandoc, python3+markdown, or npx)" >&2 echo "error: no Markdown converter available (need pandoc, python3+markdown, or npx)" >&2