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).
- 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.
The photo is the identity evidence the external review flagged as the biggest
gap. One source image (Pictures/Professional Portrait with Blue Gradient
Background.png, 1254x1254), two sizes, WebP first with a JPEG fallback —
continuous tone is the case where WebP/JPEG beat PNG:
640x640 42 KB JPG / 26 KB WebP About page (190 CSS px)
128x128 3 KB JPG / 2 KB WebP author card + home hero avatar
It now appears in the About hero, the author card at the end of every post, the
home hero (both languages), and JSON-LD Person.image — the node search engines
read to associate the name with a face.
About went from 1,024 characters to five sections: identity with photo, "By the
numbers" (162 containers / 3 hosts / 71 essays / 0 hosting companies between me
and the servers), the real tech stack in four columns, the Hoelee Enterprise
block (website design and development is the main business, email hosting
secondary), and a contact split between hiring and business.
Also: the headshot's dark-blue background sits close to the page background, so
the photo, hero avatar and card avatar get a faint brand ring plus shadow.
Pictures/ keeps the converted JPG master next to the untouched PNG.
All four featured posts are case studies, so the badge repeated the same word
four times; the card grid already sits under a 'Featured' heading. Search pill
gets a little horizontal room so 'Search /' does not read as cramped.
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.
- 7 categories now listed with name, blurb, post count and a
per-category terminal-style SVG illustration (CategoryArt)
- empty categories show '0 posts · coming soon' (non-link, no soft-404)
- new zh pages: /zh/categories/ index + /zh/categories/[category]/ detail
- PostList is locale-aware (zh-CN dates, zh category links)
- zh nav '分类' now points to /zh/categories/
- category detail pages get art + description header
- language switch wired both ways (EN <-> ZH)
- Derive lang from folder (posts/zh/* -> zh, else en); remove lang: frontmatter
- Change post route to [...slug] to capture nested zh paths
- Add nav language switcher (EN <-> 中文) + /zh/ landing page
- Default theme to dark (light opt-in via toggle)
- Translate hello-world-zh placeholder into English intro post
- Update case study: docker-compose sample + real install gotchas (commit history)
- Add docs/project-state.md (Tier 1/2/3 backlog)