The YuE2 and subtitles posts shipped text-only: render_from_art.mjs only READS
ogImage/banner, and after the 2026-10-19 bulk migration no script had to write
them, so both pages advertised /og-default.jpg and rendered no hero banner.
- new metaphors (in build_captions.py, frozen here via sync_prompts_catalog.py),
captions, art-meta evidence, banner plan and the five derived files per slug
- frontmatter: ogImage + banner in EN and ZH for both posts
- scripts/check-assets.mjs is now step 1 of npm run build, so CI fails when a
published post lacks a language-correct og card, banner, banner .webp or
400/800 list thumb. It reported 16 problems on the unfixed tree, all here.
Six photo cards in 2 columns (3 rows) at desktop widths; phones collapse to a
single full-width column and still show only the first four. Drops the 900px
two-column step (now the base) and the cramped-card type overrides that only
existed for the 2-up phone grid.
Related posts were 4 text-only cards. They now carry the post's language-specific
ogImage (/og/<slug>.jpg for EN, /og/zh-<slug>.jpg for ZH), so an English page never
shows Chinese art. Grid is 3 across above 900px, 2 across below, and phones show the
first 4 only; read-time is dropped under 600px so the date fits.
Verified in the built HTML + headless Chrome at 1264/884/500px: 3/2/2 columns,
6/6/4 visible cards, no horizontal overflow, all six images decoded.
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).
Also carries scripts/og-gen/sync_prompts_catalog.py, the check that keeps the
repo's art inputs (metaphors.json, captions/, prompts_catalog.json) in sync with
the authoring tool dir after any metaphor change.
Every post now carries one unique local-ComfyUI/Ideogram 4 base frame, composited
into EN + ZH share cards, a banner and list thumbs.
- the shipped prompts_catalog held only 21 distinct metaphors for 72 posts (16
shared one visual), so all 72 were rewritten as unique object+state metaphors
and 10 frames re-rolled after OCR found pseudo-text rendered at legible size
- render_from_art.mjs emits JPEG by default (558 KB PNG -> 84 KB mozjpeg q85);
the ZH 1200x630 card did not exist before this batch
- 288 frontmatter fields migrated; the 144 v1 PNGs are gone
- PostList's thumb helper keys off the base slug (a ZH card is zh-<slug>.jpg while
the thumb is <slug>-800.webp, which 404'd the whole ZH listing)
- [...slug].astro's <picture> regex accepts .jpg
- the repo now carries the art inputs (metaphors.json, captions/<slug>.json,
refreshed prompts_catalog.json) so a frame is reproducible from git alone
The owner is continuing the artwork batch in a new session with a GPU window,
so the resume kit now exists in the repo instead of in this session.
- New §2 "Phase F — Artwork rollout" placed ABOVE Phase E and flagged as the
NEXT task, with Step B6b: what is already committed (render_from_art.mjs,
template-v2.2, prompts_catalog.json), the measured state (2 of 72 base frames,
2 of 72 evidence lines, live site still v1), the three open consultant
questions with proposed defaults, and the format decision with its numbers
(558 KB PNG vs 84 KB mozjpeg q85, x144).
- Calls out the two migration traps that make a half-done batch break the site:
ogImage on 144 frontmatter files, and PostList.astro's thumb helper regex.
- Documents the order of work, the reproducibility proof (byte-identical
banner/thumb on the two sample slugs, four sha256s), the measurement method
for card layout, and the two known gaps (OCR pseudo-text gate missing,
evidence lines unwritten).
- §4b gains today's measured values: bento live on both homepages, light-theme
contrast fixed, post live at 250 sitemap URLs, cron facts (dev.to 2/70,
LinkedIn 5 published).
The commit-churn post the owner read and approved today, published in the
first-person voice he asked for.
- Voice rewrite before publishing: the draft opened on "when managers or
recruiters ask how productive an engineer is" and addressed the reader in the
second person throughout (you/your 12 -> 2, both in the CTA; ZH 你 5 -> 2).
It now reads as a log entry: "Last week I did something I almost never bother
to do: I counted my own commits." Headings moved from essay titles to actions.
Numbers, the 5-repo table and every claim are unchanged.
- Title changed at the owner's request:
"Why 500,000 Lines of Commit Churn Only Left 230,000 Lines in Production"
-> "I Committed 518,000 Lines in 30 Days. Production Kept 235,000."
(ZH: 30 天提交 51.8 万行,生产环境只留下 23.5 万行), pubDate 2026-10-18.
- Fixed two real defects found while making the card: the draft claimed "four
automated gates" and listed three; and og-gen split titles at the word-count
midpoint, breaking at "…Lines in / 30 Days" — the splitter now prefers a
sentence boundary near the middle and falls back to the midpoint.
- og PNG regenerated for the new title, thumbs and banner WebP re-derived.
Measured: card 2 title lines, overflowY 0, measure_og_banner PASS (tags 1 row,
banner delta 2), build clean, /posts/ monotonic with this post on top.
Also queued the post at the head of the dev.to drip (queue 69 -> 70).
Measured against the live site and the cron outputs today, not carried over:
- A1 (author photo) and A3 (featured strip) were still ⬜ although both shipped
(B4 and 6e9d58b) — marked done with the evidence, /start-here left as a
deliberate deferral.
- E3 is fully automated: the LinkedIn half was already a daily no_agent cron
(id 35f8985f609a). 5 posts published as of today's 12:30 tick; its silence is
not proof of work, so the output files are the record.
- E6 closed: HSTS waived by the owner ("无视吧"), re-measured still max-age=0.
Do not re-open.
- E1: CF Web Analytics re-measured absent (0 cloudflareinsights hits, one Umami
tag). It is an owner dashboard action — no token here can enable it and CF
injects the beacon itself, so there is no repo change to make.
- B6 now states the real shape of the artwork work: 0 of 72 posts rolled out,
the live site still serves the v1 images, the 3 consultant questions are
unanswered, the rendered OG card is measured at 558 KB PNG vs 84 KB JPEG q85,
and the promised OCR pseudo-text gate does not exist yet.
- Homepage (EN + ZH) featured section becomes a 2x2 Cyber-Bento grid: OG
thumbnail at 65%/140px behind a scrim, category badge + metric pill,
3-line-clamped description, arrow CTA; 1 column under 680px.
- Bento scrim/pill/dot now use theme tokens (--card-veil, --pill-bg,
--bento-visual-opacity). The original hardcoded dark blend measured
1.96:1 pill contrast on the light theme; now 6.56 dark / 6.10 light.
- Add the missing @keyframes bento-pulse (the doc claimed a pulsing dot,
none existed), disabled under prefers-reduced-motion.
- Tag chips: mono face, hairline border, 4px radius; dashed border for
non-link tags.
- ZH pages: brand links /zh/, in-post category tag links
/zh/categories/<cat>/ instead of the EN route.
- project-state: B6 shipped note with measured readings; correct the
prompts-catalog path claim (file is outside the repo).
Measured in headless Chrome at 1280px and 390px, EN + ZH: 4 cards,
2 cols / 1 col, scrollWidth-clientWidth 0, no card overflow, all four
-800.webp thumbs loaded at 800x420.
Found a real bug while verifying the nav search: /pagefind/pagefind.js is an ES
module that defines no window.Pagefind, but Search.astro loaded it with a classic
<script src> and read window.Pagefind — so every query answered "Search is
unavailable." (confirmed live before the fix). Now a dynamic import; the specifier
must be a variable or Rollup resolves it at build time and the build fails.
Measured before/after in a real browser:
- archive /posts/ + /zh/posts/: 18,220px -> 3,788px (71 rows at 44px, grouped by
year) with a client-side filter (title/tags/description text, category chips,
14 tag chips, zero requests). Tested: "nginx" -> 6 rows, chip devops -> 28.
- tag pages for tags with >= 3 posts. Measured 235 distinct tags (an earlier brief
claimed 74), 156 used exactly once — pages only where a topic exists: 40 tags ->
80 pages, build 165 -> 245 pages. Pills link only when a page exists.
- homepage + ZH homepage: hero (288px) with the real scale claim, three CTAs, a
fixed Featured 4 (scale -> automation -> observability -> a delivered result),
Latest 10 -> 6. ZH hero had linked "关于我" to the English /about/.
- long code blocks collapse above 420px with an Expand button + gradient, copy
button still copies the whole block (verified on the 1010px block).
- search is visible ("⌕ Search /" pill, "/" plus the existing Ctrl/Cmd+K) and the
zero-result screen offers topic chips instead of a dead end.
Docs: project-state B3 entry, ops-runbook Pagefind gotcha.
All items measured before/after with curl, on the origin and through Cloudflare:
- /posts/zh/ answered 403 (directory with no index) -> nginx 301 to the new archive
- /zh/posts/ 404'd -> new src/pages/zh/posts/index.astro (build 163 -> 165 pages)
- ZH nav pointed "关于" at /zh/ and "文章" at /zh/ -> /zh/about/ and /zh/posts/
- og:locale was "en" on every Chinese page -> zh_CN, plus og:locale:alternate on
both languages (EN pages now advertise zh_CN as well)
- hreflang "zh" -> "zh-Hans"; <html lang> on ZH pages -> zh-Hans
- no 404 page existed -> src/pages/404.astro, served by nginx with a 404 status so
crawlers skip it, offering search + archive + the 3 newest posts
- JSON-LD Person gained worksFor (Hoelee Enterprise); image still omitted until the
author photo exists rather than pointing at a placeholder
nginx: config moved from the stock image file to a host bind mount
(/mnt/user/docker/nginx-hoelee-blog/conf.d/default.conf, Portainer stack 247 on
endpoint 3), validated with nginx -t in a throwaway container before the redeploy.
It also sets /_astro/* to immutable 1-year (hashed names) and HTML to
must-revalidate; unhashed /og/*.png and /banners/*.png keep the default TTL on
purpose. Note: the public HSTS and 16-day asset TTL come from Cloudflare zone
settings, not from this nginx.
Docs: ops-runbook.md (the nginx layer + why each rule), project-state.md (B2 entry
and the two open Cloudflare items my token cannot write).
Measured before/after with headless Chrome + CDP (not by eye):
- article banner: <picture> WebP q78 with the PNG as fallback, because the PNG
must stay for og:image (social scrapers do not reliably render WebP cards).
70 banners: 6.02 MB -> 2.52 MB (42%). JPEG measured *larger* than PNG here
(q85 = 129%) — the art is flat terminal-style with sharp text edges.
- post page related cards: dropped four 1200x630 PNGs (183 KB) to display
526x276 where the embedded code is illegible; now text-only cards with a
category badge, date and reading time.
- list thumbnails (home + archive): new /og/thumb/<name>-{400,800}.webp served
via srcset/sizes. Rows display at ~398px but were pulling full 1200x630 PNGs.
Homepage thumbnails ~450 KB -> 62 KB; archive (71 rows) ~3.2 MB -> 436 KB.
- desktop banner capped at 720px centred: 67% -> 45% of a 900px viewport,
keeping the art intact instead of cropping it.
Post page totals: images 277 KB -> 38 KB; page height 5758 -> 5065 (desktop)
and 9423 -> 8807 (mobile). Build: 163 pages.
Script: scripts/optimize-images.py; policy: docs/design-guide.md §3; runbook
step added to docs/ops-runbook.md.
Re-checked the Sept research against the live site and the repo:
- 53 posts x 2 languages, 127 sitemap URLs, 133/133 internal links 200
- Mark C1 (per-post OG), D1 (search), B2b (draft bank) as done — they were stale as open
- New Phase E (top of the queue): E1 analytics (CF Web Analytics + GSC), E2 main-site ->
blog link (deferred to the www.hoelee.com overhaul), E3 dev.to syndication, E4 ZH nav +
/zh/posts/, E5 og:locale, E6 Cloudflare HTML caching, E7 volume guardrail, E8 hygiene
- New section 4: measured evidence table; conventions section renumbered
Five posts with custom OG + banner and hire CTAs. Three are web3, giving
that category its first posts — /categories/web3/ previously 404'd while the
index advertised it as '0 posts - coming soon'.
Backdated where the work genuinely is older: the foundry-nft and
hardhat-smartcontract-lottery commits date to 2024-08-15..19, so those two
posts fill the empty 2024-10 and 2024-12 archive months with updatedDate
holding the real date. The two 2026-08-19 posts use their real work date.
TERMINALS/BANNERS entries for all five slugs are committed this time (both
generators were clean; diffs verified purely additive).