The consultant's review of the shipped set found two defects, both ours, and both fixed at the source rather than in the output: 1. The banner/thumb crop re-framing detected only the glowing core of the object. Column brightness measures 2-6 levels above the background on dark glass, so it boxed 8 % of the frame for an object group spanning 35 %+. The detector is now a background-plane deviation plus the largest connected components (`plan_banner_crops.py`, detector in `probe_band_crop.py`), 0 % false-positive on the frame's own negative space, stable across thresholds. The crop plan is frozen as `banner-plan.json` and consumed by the renderer; a thin subject is centred at 53.5 % of the banner width with the shortfall filled by the art's own edge pixels, one row at a time (a single averaged fill left a visible tonal step, tried and rejected). Measured on the delivered 72: 0 clipped, silhouette median 47.0 %, centre median 53.5 % (`verify_banner_crops.py`). 2. `art-meta.json` pinned two slugs to the hand-accepted sample frames — the very frames that carried the HUD readouts — which also kept them out of the crop plan. Pins removed. Prompt side: 9 frames still printed glyphs at the consultant's bar (>=12 px and >=2 characters). Every offending noun is measured and now asserted against: a literal `marked at 28` printed "28"; `storefront kiosk` printed "PRINATA"; `voucher card` printed "Voucher"; `market booths` printed "10MARKET"; `glass button` printed "OC"; `gateway pylon` printed "GATELY". 50 forbidden words, 0 hits across 72 captions, and all 9 frames re-rendered clean. Also: YAML escapes are honoured in the frontmatter reader and straight quotes become curly ones (the ZH cards shipped a literal backslash), the evidence line is capped at 65 characters, and the light theme gives dark artwork a hairline border plus a soft shadow (consultant option A) so it reads as a dark display case instead of a black slab. OCR gate at the consultant's bar: 72/72 frames clean (16 carry sub-threshold single glyphs, exempt as geometry noise).
og-gen — per-post OG image generator
Generates a branded 1200×630 social-share image for a blog post, matching the
site's cobalt-blue #295cff branding, the logo-square.png mark, and the
"terminal / debugging" visual style.
Usage
node scripts/og-gen/generate.mjs <post-slug>
Reads frontmatter (title, category, tags) from
src/content/posts/<slug>.md, fills template.html, renders with headless
Chrome, and writes public/og/<slug>.png.
The post's frontmatter needs ogImage: /og/<slug>.png so the blog serves it
(layout already renders og:image + width/height; PostList shows it as a
card thumbnail).
Customizing the terminal box
Each post shows a "terminal" panel (the red error / green fix lines). Add an
entry keyed by slug in TERMINALS inside generate.mjs, using these tokens:
<div class="line"><span class="prompt">$</span><span class="cmd">command</span></div>
<div class="line"><span class="prompt"> </span><span class="err">error line</span></div>
<div class="line"><span class="prompt">$</span><span class="cmd">fix</span><span class="fix">→ done ✓</span></div>
No entry → a generic default terminal.
Requirements
- Google Chrome at
C:\Program Files\Google\Chrome\Application\chrome.exe(override withCHROME_PATHenv var). - Node 20+.
Notes
- PNG (not JPG) is intentional: for flat UI/terminal art, PNG is smaller and sharper than JPG (see commit history).
- The
logo-square.pngreferenced lives atpublic/logo-square.png.
v2 pipeline ("one art, four targets") — render_from_art.mjs
The consultant-approved redesign replaces the generated terminal panels with one zero-text base-art frame per post, composited into four assets:
| Target | Path | Size |
|---|---|---|
| hero banner | public/banners/<slug>.webp (+ .jpg fallback from frontmatter) |
1600×900 |
| social card | public/og/<slug>.jpg (EN), public/og/zh-<slug>.jpg (ZH) |
1200×630 |
| bento/list thumb | public/og/thumb/<slug>-800.webp |
800×420 |
| small thumb | public/og/thumb/<slug>-400.webp |
400×210 |
The card is template-v2.2.html (frosted bento + blueprint grid + full-width evidence
bar) filled from the post's own frontmatter plus art-meta.json; the self-fitting
headline script reports FIT <n>px through <title>, and a render that does not
settle fails instead of shipping a silently-shrunken card.
# metadata first: art path (default <art-dir>/<slug>.png) + a HAND-WRITTEN evidence line
$EDITOR scripts/og-gen/art-meta.json
node scripts/og-gen/render_from_art.mjs --only <slug>[,<slug>] --out /tmp/og-check
node scripts/og-gen/render_from_art.mjs --all # writes into public/
--format jpg(default) orpng. JPEG is the default because it is measured: a composited card renders 558 KB as PNG and lands at 84 KB at mozjpeg q85 (256-colour PNG = 240 KB). The default site card already ships as JPG for the same reason.evidenceis required and never derived — it is editorial text, and inventing one from the description is the "invented specificity" failure the blog avoids. A missing entry aborts with the slug list.ogTitle_en/ogTitle_zhshorten only the card's headline (the article keeps its real title) — the consultant's answer to the English headline-rag problem.- Reproducibility check: re-rendering the two sample posts produced banners and 800px thumbs byte-identical to the consultant's accepted delivery (sha256 16e6a7b0…, f3b29f38…, c4e28291…, 8095ec6c…).
- ⚠ Changing
ogImagefrom.pngto.jpgalso needsPostList.astro's thumb helper updated (/\.(png|jpe?g)$/), or the listing derives…-800.webpfromslug.jpgand every thumb 404s.