90 KiB
project-state.md — archived detail (verbatim, nothing reworded)
Split out of docs/project-state.md on 2026-10-19, when that file was slimmed from ~128 KB / 1,118
lines to a readable queue. Everything below is copied exactly as it stood in the old file:
- the §1 batch logs (B1–B7) with their per-item measurements,
- the full logs of Phase A (identity), Phase B (content), Phase C (discovery), Phase D (polish) and Phase F (the artwork rollout).
docs/project-state.md now keeps the live queue (Phase E), one summary line per completed phase, and the
snapshots. Where the old text is known to be stale the main file says so — read the main file first.
§1 batch detail (B1–B7) — verbatim
-
✅ B5 Baseline & Hygiene Batch, 2026-10-18 (commit
3f63150live on production):- Default OG card upgraded: external delivery design with real author headshot integrated, optimised via mozjpeg q85 to 45 KB JPG (
public/og-default.jpg, down from 211 KB PNG), with PNG retained for backwards compatibility. - 全站 72 篇 banner 404 卫生彻底清零:为
scraping-bot-walled-marketplace-warm-browser-session补齐 terminal banner WebP 与 PNG 资产。 - E9 modern system typography stack deployed:
--font-sansand--font-monoupgraded to native multi-platform stack with PingFang SC / Microsoft YaHei CJK fallbacks. - A2 branding alignment:
<title>, meta description, JSON-LD, and homepage hero unified toMr Hoelee — Full-Stack Developer & DevOps Engineer. - All 251 pages cleanly verified, Gitea and GitHub remotes strictly 0/0 aligned.
- Default OG card upgraded: external delivery design with real author headshot integrated, optimised via mozjpeg q85 to 45 KB JPG (
-
✅ B6 Visual Redesign with External Consultant & Ideogram 4 Pipeline, 2026-10-18 (Round 4 completed):
- Outside design consultant review finalised: 70% Dark Cyber-Bento + 30% Blueprint schematic; 100% committed to Hybrid Paradigm ("One Art, Four Targets") — pure visual art for hero banners & list thumbnails (eliminating title duplication & bilingual collisions), programmatically composited typography for social share cards.
- Frosted Bento Container Card (
template-v2.2.html) deployed to solve layout emptiness:- 736×536 frosted acrylic card (
backdrop-filter: blur(16px), gradient top highlight); - 2-line article description added to eliminate vertical voids;
- 100% full-width terminal evidence bar (
$ ...); - CSS-drawn 36px blueprint grid + top-left radial ambient glow restores industrial depth;
- 3D base art scaled to 116% and shifted inward to bridge the central divide.
- 736×536 frosted acrylic card (
- Self-fitting headline engine implemented: dynamically measures DOM lines in headless Chrome, stepping down font size (50px → 26px) until headline fits
<= 3 lineswith no orphan/widow lines; CJK titles intelligently split at punctuation; numbers glued with NBSP. - Front-end homepage enhanced:
Featuredsection upgraded to 2×2 Cyber-Bento Grid (with metric pills and pulsing green status dots); bilingual navigation brand link fixed to/zh/; in-post category tags fixed to/zh/categories/. - Shipped to production 2026-10-18 (commit
6e9d58b), after a measured pass on both themes: the bento scrim and metric pill were hardcoded to a dark blend, which on the light theme produced a muddy grey band and 1.96:1 pill text contrast; both are now theme tokens (--card-veil,--pill-bg,--bento-visual-opacity), and the "pulsing" dot finally has an actual@keyframes bento-pulse(it had none, and it is disabled underprefers-reduced-motion). Measured in headless Chrome, 1280px and 390px, EN + ZH: 4 cards,2 cols / 1 col,documentElement.scrollWidth − clientWidth = 0, no card overflow, all four-800.webpthumbsnaturalWidth 800— contrast dark pill 6.56 / badge 4.83 / h3 11.86 / desc 5.78, light pill 6.10 / badge 4.85 / h3 16.36 / desc 6.74 (WCAG AA ≥ 4.5). Tag chips restyled to mono + hairline border (4px radius) site-wide; ZH post pages link/zh/categories/<cat>/, ZH brand links/zh/. - Full catalog of 72 posts' unique physical metaphors and prompts extracted to
scripts/og-gen/prompts_catalog.json, with the batch rendererscripts/og-gen/render_from_art.mjs+art-meta.json(committed 2026-10-18; the readable version of the catalog ships outside the repo atDesktop\blog-og-image-redesign-brief\03-72-Posts-Visual-Prompts-Catalog.md). - Batch rollout: 0 of 72 posts. Everything above is template-level: the two sample posts (
running-production-infrastructure-solo,docker-reported-exit-0-the-oom-killer-disagreed) are rendered intoDesktop\blog-og-image-redesign-brief\delivery-for-consultant-round4\only. The repo/live site still serves the v1 set — 72public/banners/*.png+ 72public/og/*.pnggenerated by the terminal-panel generators on 2026-10-06…10-16 — so nothing on the site changed appearance from this work yet. - Three consultant questions are still unanswered (they gate the batch; the ask is written verbatim in
delivery-for-consultant-round4\00-第四轮-实施反馈与3个待定细节.md§二): (1) English headline rag — (A) keep the stepped shape, (B) drop 3–5px for equal-width lines, (C) shorten the OG-only title via a newogTitlefrontmatter field, (D) other; (2) the hard CJK length cap for OG titles (currently assumed ≤30 chars incl. punctuation, EN ≤65); (3) whether list thumbnails should keep 100% right-focus or hold back to 85% for breathing room at ~398px display width. - Asset weight measured 2026-10-18 (must be decided before the batch). A composited OG card is 558 KB as rendered PNG vs the 42 KB average of today's 72 cards; ×144 (EN+ZH) that is ~84 MB in the repo and a much heavier share payload. Same card re-encoded: 256-colour PNG 240 KB, 128-colour PNG 194 KB, mozjpeg q85 84 KB, q78 70 KB. So the batch should ship JPEG for
og:image(the precedent already set by/og-default.jpg, 45 KB) — which means anogImage:path change on 144 frontmatter files, or a deliberate decision to keep PNG at ~240 KB and eat ~35 MB. - Known gap in the plan: the OCR pseudo-text gate on the base art (regenerate a frame when any readable character is detected) was promised to the consultant and is not implemented.
-
✅ B4 identity & distribution batch, 2026-10-16 (fourth batch) — the photo and the About rewrite:
- Real author photo (source
Professional Portrait with Blue Gradient Background.png, 1254×1254, kept untouched inPictures/with a converted JPG master beside it). Two derivatives, WebP first with a JPEG fallback:lee-teong-hoe-640(42 KB JPG / 26 KB WebP) and-128(3 KB / 2 KB). Appears in the About hero, the author card on every post, the home hero (both languages) and JSON-LDPerson.image— the node search engines use to bind name to face. - About page 1,024 → 7,357 characters, in five sections: identity + 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 (WhatsApp for enquiries). Chinese twin rewritten to match.
- The headshot's dark-blue background sat close to the page background, so the photo/ghero/avatar get a faint brand ring + shadow (measured: the photo reads as a deliberate element instead of a floating head).
/llms.txtgenerated from the collection at build time (48 KB, 142 post entries + section links) so LLM crawlers and answer engines can cite accurately;robots.txtnow points at it.- Still open: the main site (
www.hoelee.com) counter says12+ Experts/100+ Years Of Experiencewhile the blog says he runs the stack solo — needs the owner's real numbers; and cross-posting to dev.to/Hashnode needs his accounts.
- Real author photo (source
-
✅ B3 structure batch, 2026-10-16 (third batch from the external design review) — all numbers measured in a real browser (CDP), not by eye:
- Found and fixed a real bug: site search never worked.
/pagefind/pagefind.jsis an ES module (export { search, debouncedSearch, … }) that defines nowindow.Pagefind, butSearch.astroloaded it with a classic<script src>and readwindow.Pagefind→undefined→ every query in the overlay was answered "Search is unavailable." Live-verified before the fix. Loader is now a dynamicimport(); the specifier must be a variable (const url = '…'; import(/* @vite-ignore */ url)) because Rollup resolves a literal path at build time and fails on a file that only exists indist/. After: query "docker" → 12 hits, first = the Docker OOM post; ZH "容器" also returns hits. - Archive
/posts/+/zh/posts/: 18,220px → 3,788px (71 rows at 44px, grouped by year) with a client-side filter — title/tag/description text search + category chips + 14 tag chips, zero network requests (tested: "nginx" → 6 rows, chipdevops→ 28 rows, reset → 71). - Tag pages: 235 distinct tags measured (the earlier brief said 74 — wrong), 156 of them used exactly once; pages are generated only for tags with ≥ 3 posts (40 tags → 80 pages, build 165 → 245 pages). Tag pills on posts and listings link only when a page exists; one-off tags stay plain text.
/tags/docker/→ 25 posts. - Homepage + ZH homepage: hero (288px desktop) with the real scale claim (162 containers / 3 hosts), 3 CTAs (case studies ↓ / hiring profile / SME work), Featured 4 fixed slugs (scale → automation → observability → a delivered business result) and Latest trimmed 10 → 6.
- Long code blocks collapse at >420px with an "Expand (N lines) ↓" button and a gradient mask; the copy button still copies the full block (verified on
how-i-host-this-blog: 1010px block). - Search is now visible (nav pill "⌕ Search /" plus
/as a shortcut next to the existing Ctrl/Cmd+K) and the zero-result screen offers topic chips + the archive instead of a dead end (verified with a query that matches nothing). - ZH nav bug fixed in passing: the ZH hero linked "关于我" to the English
/about/.
- Found and fixed a real bug: site search never worked.
-
✅ B2 broken-windows batch, 2026-10-16 (second batch from the external design review) — every item verified before/after with
curl:/posts/zh/answered 403 (a directory with no index) → nginx now301s it to the new Chinese archive./zh/posts/did not exist (404) → newsrc/pages/zh/posts/index.astro(build went 163 → 165 pages).- ZH nav pointed "关于" at
/zh/→ now/zh/about/, and "文章" →/zh/posts/. og:localewasenon every Chinese page → nowzh_CN, withog:locale:alternateon both languages (EN pages advertisezh_CNtoo).hreflangvaluezh→zh-Hans, and<html lang>on ZH pages →zh-Hans.- No 404 page →
src/pages/404.astro(status stays 404, branded page with the search overlay, archive links and the 3 newest posts). - JSON-LD
PersongainedworksFor(Hoelee Enterprise +www.hoelee.com);imagestill absent on purpose until the author photo exists. - nginx config moved from the stock image file to a host bind mount at
/mnt/user/docker/nginx-hoelee-blog/conf.d/default.conf(Portainer DSM stack 247, endpoint 3), validated in a throwaway container before the redeploy; the conf also sets/_astro/*toimmutable1-year and HTML tomust-revalidate. Details inops-runbook.md. - Still open for the owner (CF token limits, see below): zone HSTS is
max-age=0and HTML is not edge-cached — both are Cloudflare settings my token cannot write. The deploy workflow already purges the edge after each deploy once a Cache Rule exists.
-
✅ B1 image batch, 2026-10-16 (first batch from the external design review) — measured with a real browser before/after, not by eye:
- Article banner →
<picture>with WebP q78 + PNG fallback: 70 banners 6.02 MB → 2.52 MB (42%). The PNG stays forog:image(social scrapers do not reliably render WebP cards). JPEG was measured worse than PNG on this art (q85 = 129%). - Related-post cards on a post page → text-only (category badge + title + date + read time). They used to load four 1200×630 PNGs (183 KB) to display 526×276 where the art's code is illegible anyway.
- List thumbnails (home + archive) →
/og/thumb/<name>-{400,800}.webpwithsrcset/sizes(400w 5.8 KB, 800w 16.1 KB). Homepage thumbnails ~450 KB → 62 KB; archive (71 rows) ~3.2 MB → 436 KB. They were fetching full 1200×630 PNGs for a 398px slot. - Desktop banner height capped (720px, centred): 67% → 45% of a 900px viewport, art intact rather than cropped.
- Measured post page: images 277 KB → 38 KB, height 5,758 → 5,065px desktop / 9,423 → 8,807px mobile. Script:
scripts/optimize-images.py; format policy:design-guide.md§3; runbook step inops-runbook.md.
- Article banner →
-
✅ B7 review-response batch, 2026-10-19 — an outside content/UX review of the live site was reconciled claim-by-claim against the repo; only the measured defects were acted on.
- About counter was hand-typed and had drifted:
about.astrosaid 71 write-ups while the homepage rendered 72. Both About pages now count from the collection (getCollection('posts', isEn|isZh).length), so the number cannot drift again. - Reading time was English-only maths:
minutesOfsplit the body on whitespace and divided by 200, so a Chinese post (no interior spaces) showed 1 min against 5 min for its English twin. Newsrc/lib/reading-time.tscounts CJK ideographs at ~400/min plus Latin words at 200/min; pure-ASCII bodies are unchanged (same formula), the ZH post now reads 5 分钟, and the ZH article header no longer says "min read". - ZH copy defects:
主陆开发者→中国大陆开发者;基元律动值得麻烦的原因→为什么值得考虑基元律动(the English twin already said why it is worth the hassle). hello-worldcontradicted the site: it still declared English primary with selective Chinese, and quoted a~140-container homelab. Both language twins now say every post exists in both languages and quote 162 containers across three self-managed hosts.0 hosting companies between me and the serverswas loose (the VPS is rented from a provider) →0 managed hosting layers — I run all three hosts myself(EN + ZH).- Hero sub-paragraph now leads with the capability, not the container count; the number follows as evidence.
<h1>andtitle/JSON-LD were left alone (A2 identity alignment). - Featured bento cards got the
srcset/sizesthey were missing (-400/-800webp) — they were downloading the 800px file for a 226–341px slot. At DPR1 all widths now pick-400; card geometry measured unchanged (226/227/310/341 px). - Measured, not assumed: 12 routes × 4 widths (360/390/414/768) = 48/48 with zero horizontal overflow (
scrollWidth − clientWidth == 0); art uniformity re-surveyed — 72/72 banners 1600×900 WebP, 72/72-800thumbs 800×420, 72/72-400thumbs 400×210, 144/144 share cards 1200×630 JPEG. Build 251 pages, unchanged. - Deliberately not done, because the review's premise was stale or wrong:
hreflang/canonical/og:localeand per-post OG images already ship (72/72 + 72/72), and the art system was already unified by B6b (round 7) — the reviewer was reading screenshots from before that rollout for the visual half.
- About counter was hand-typed and had drifted:
Phase F — Artwork rollout: v2 "one art, four targets" — verbatim (DONE 2026-10-19)
Phase F — Artwork rollout: v2 "one art, four targets" (NEXT; user decision 2026-10-18)
Step B6b — Replace all 72 posts' artwork with the consultant-approved v2 set. ✅ Done and live 2026-10-19 (327d516)
Everything below is measured, not planned. A fresh session needs no other context to continue.
| Piece | State |
|---|---|
| The renderer | scripts/og-gen/render_from_art.mjs (committed a41448b) — one zero-text base frame → banner 1600×900 (banners/<slug>.webp + .jpg fallback), social card 1200×630 (og/<slug>.jpg EN, og/zh-<slug>.jpg ZH), thumbs og/thumb/<slug>-{800,400}.webp |
| The layout | scripts/og-gen/template-v2.2.html — frosted 736×536 bento card, blueprint grid, full-width evidence bar, self-fitting headline that reports FIT <n>px through <title>; a card that never settles fails the run |
| Per-post inputs | scripts/og-gen/art-meta.json — art (default <art-dir>/<slug>.png), evidence (required, hand-written), optional ogTitle_en/zh, optional tags |
| Prompts | scripts/og-gen/prompts_catalog.json — 72 entries, regenerated 2026-10-19 after the metaphor rewrite (the shipped file had only 21 distinct metaphors for 72 posts: 16 shared one visual, 15 another, 10 another) |
| Base art on disk | 72 of 72 — C:/AI/ideogram4/out/<slug>.png, 1360×768, produced 2026-10-19 (batch 4.5 h, ~220 s/frame, 20 steps) |
| Evidence lines written | 72 of 72 (scripts/og-gen/build_art_meta.py holds all 72, each quoting a number/log line/command from its own post) |
| Rendered targets | 72 EN cards + 72 ZH cards + 72 banner .jpg + 72 banner .webp + 144 thumbs, all in public/; the 72 v1 public/og/*.png and 72 v1 public/banners/*.png were deleted |
| Frontmatter | 288 fields migrated by scripts/og-gen/migrate_to_v2_assets.py --write (144 files: EN ogImage: /og/<slug>.jpg, ZH ogImage: /og/zh-<slug>.jpg, both banner: /banners/<slug>.jpg) |
| Component changes | PostList.astro thumb helper now derives from the base slug (a ZH card is zh-<slug>.jpg while the thumb is <slug>-800.webp); [...slug].astro <picture> regex accepts .jpg |
| Live site | serving v2 — deploy green; EN/ZH cards, banner WebP and both listings' thumbs all asserted 200, v1 .png now 404 |
| Reference delivery | C:/Users/hoelee/Desktop/blog-og-image-redesign-brief/delivery-for-consultant-round4/ (+ 00-第四轮-实施反馈与3个待定细节.md for the three open questions) |
Rollout log — 2026-10-19 (every number measured this session).
- The prompt set was not usable as shipped.
prompts_catalog.jsoncarried 21 distinct metaphors for 72 posts (16 posts shared "concentric acrylic rings", 15 shared "rackmount hardware", 10 shared "diagnostic probe"), so the batch would have produced visibly duplicated art across a third of the site. Rewrote one concrete object + state per post → 72/72 unique, and generated the 72 structured JSON captions withC:/AI/ideogram4/build_captions.py(asserts the slug set matchessrc/content/posts/*.mdand that no two metaphors are identical). - GPU run. 72 frames at 1360×768 / 20 steps /
--preset default, ~220 s each, 4.5 h total, resumable viaC:/AI/ideogram4/run_batch.py --all(skips slugs whose frame exists). QC: 72/72 present, all exactly 1360×768, left-60 % mean luminance 10.2–23.1 (the space the card's text sits on). - Caption defect found and fixed (3 of 3 pilot frames). With the subject described twice —
high_level_descriptionrepeating the metaphor — the model drew a labelled diagram with a pseudo-title in the corner (Oypert,Oyristes tool,CONTRACT). One short noun phrase in the HLD + the subject only inelements[].desccleared it. - The "no text anywhere" rule fails on the accepted samples too.
sample_oom_169_00001_.pngOCRs asKenel L.aser@0.874 plus six more hits. So the gate judges legibility, not hit count: a glyph box under ~14 px on the 1360×768 frame is invisible at banner (1600×900) and thumb (398 px) size. - The model renders the prompt's own label nouns as text. Measured:
access-denied barrier→ "ACCCSS-DENIED",attachment clip→ "Attachment Clip",eight labelled ports→ a 389 px glyph,id-stamped cards→ "ATACHMENT CLIP",233 fresh rivets→ a row of numbers,SSD slab→ a 66 px "SSD",PLUGIN cartridge→ "PLUGIN". Rewriting the metaphor to describe shape/material/state only fixed every one of them on re-render. - Result after three correction rounds (final gate, 2026-10-19 04:17): 53 of 72 frames carry no ≥14 px OCR detection at all; 18 carry a single-character/geometry false positive only (
O,T,司,00%— spot-checked by eye, they are edge and highlight misreads); 0 carry a legible word or phrase. Ten frames that had real pseudo-words were re-rolled (four of them twice), and every one came in at or below the accepted reference. The last two leaks were the model spelling an English noun phrase straight out of the sharedstyle_description(sleek developer tooling aesthetic→ "devalopertoolt"), so the style block is now free of generic noun phrases. Gate + tooling:C:/AI/ideogram4/ocr_gate.py(--baselineprints the accepted samples' numbers),qc_frames.py(per-frame stats + unlabelled contact sheets). - Delivery shipped 2026-10-19 (
327d516→ Giteamain, deploy green): 72 EN + 72 ZH cards, 144 banner files, 144 thumbs rendered intopublic/; the 144 v1 PNGs deleted; 288 frontmatter fields migrated; the two components fixed.npm run build→ 251 pages, 216 distinct image references, 0 missing, 0 stale.png; live assertions afterwards: EN card 200image/jpeg, ZH card 200 (did not exist before), banner WebP 200, thumbs 200 from both listings, v1.png404, and the ZH listing's thumbs resolve from the base slug (the old helper would have 404'd every one of them). The repo also carries the art inputs —scripts/og-gen/metaphors.json,captions/<slug>.json(72), a refreshedprompts_catalog.jsonwith 72 distinct entries, viascripts/og-gen/sync_prompts_catalog.py --check. - Art weight: 13.4 MB → 15.4 MB (+2.0 MB), and it buys 72 ZH cards that did not exist before. Per file: EN card 67 KB JPEG (v1 was 71 KB PNG), banner 40 KB JPEG + 30 KB WebP, thumb 7 KB WebP.
Round-6 — the consultant's review of the shipped set (2026-10-19). Two defects were real and both were ours, not the model's:
- ZH cards shipped a literal
\escape —你的\"磁盘将满\"告警在说谎. The renderer's frontmatter parser stripped only the outer quotes and left YAML's\"in the string. Fixed by unescaping YAML escapes and converting straight quotes to “ ” pairwise. - The artwork carried HUD readouts after all — the consultant read
60%,100%and00%offyour-disk-full-alert-is-lying, plus a10and a ruler line onthat-dying-ssd-…. Root cause: the shared style block inherited from the accepted samples (technical blueprint vector lines,status indicator lights) and the background'sblueprint grid crosshairs and delicate vector coordinate lines, plus one metaphor that literally said "percentage gauge". The first delivery's own claim that the art was text-clean was wrong: the gate flagged00%@0.84 at the old bar and it was classified as geometry noise. The gate now runs the consultant's bar (glyph box ≥ 12 px and ≥ 2 alphanumeric/percent characters; single glyphs exempt) andbuild_captions.pyasserts aFORBIDDENword list so blueprint/gauge/label language cannot return. Three pilot frames re-rendered at the fixed prompt: 0 OCR hits each (wasSSD66 px,IDENESLOES92 px,00%15 px), then all 72 regenerated.
Rulings adopted: banner/thumb reframing (option B — subject targeted at 45–50 % of the
banner width, thumbs cut from the banner; measured 45–47 % for 5 of 8 sampled frames,
35–44 % for two, ~14 % for one genuinely slender subject — widening the caption bbox does
not enlarge the subject, measured), light-theme hairline + soft shadow (option A), and
the evidence line capped at 65 characters. Cards are unchanged apart from the quote fix.
Style is unchanged (option: keep the realistic 3D machinery look).
Round-6b — the banner crop algorithm, rewritten after review (2026-10-19). The
consultant looked at the probe sheet and found the detection box on the glassware frame
covered only the local highlight. Correct, and the cause was structural, not a threshold:
| attempt | measured |
|---|---|
| column mean (what shipped in round 6) | dark glass sits 2–6 levels above background → boxed the glow only: 8 % of the frame for an object group spanning 35 %+ |
| per-row background sample + global threshold | a left-margin sample cannot describe a horizontal gradient → 39 % on one frame, 69 % on the next |
| deviation from an estimated background plane + connected component (now) | downsample 16×, median-filter the object away, upsample; ` |
The closing kernel mattered: at 9 px two frames split into components and the largest one boxed a sliver (4.2 % and 5.9 % of frame width for ~39 % objects). Zoom is computed on the silhouette (target 47 % of banner width, centre 53.5 %), the detected box is padded 4 % first, and the window is no longer clamped into the scaled frame — clamping is what glued a thin subject to the right edge. Shortfall is filled by extending the canvas with the art's own edge pixels one row at a time (a single averaged fill was tried and left a visible tonal step down the right edge).
Detection lives in Python (C:/AI/ideogram4/plan_banner_crops.py → out/banner-plan.json,
frozen into the repo by sync_prompts_catalog.py, consumed by render_from_art.mjs), and
verify_banner_crops.py re-detects the subject on the finished banners rather than
trusting the plan. Measured on the delivered set: 72 banners, 0 clipped, silhouette median
47.0 %, centre median 53.5 % (two outliers reported: hello-world 64 % — the focal cursor
is small but its platform spans the frame; my-backup-was-using-someone-elses-api-quota 25 %).
Also fixed here: art-meta.json no longer pins docker-reported-exit-0-the-oom-killer-disagreed
and running-production-infrastructure-solo to sample_oom_169_00001_.png /
sample_prod_169_00001_.png — those two frames were the HUD-carrying ones the consultant
flagged, and the pins also kept those slugs out of the crop plan.
Round-7 — delivered (2026-10-19, Gitea f391682 + 4f60cdc, pushed to GitHub too).
Final numbers, all measured on the delivered artifacts:
- OCR gate (the consultant's bar: glyph box ≥ 12 px and ≥ 2 alphanumeric/percent
characters): 72/72 frames clean; 16 frames carry sub-threshold single-glyph hits,
exempt as geometry noise. Nine frames failed the first pass after the prompt rewrite and
were re-rendered: every offending noun is now in the assertion list (a literal
marked at 28printed "28";storefront kiosk→ "PRINATA";voucher card→ "Voucher";market booths→ "10MARKET";glass button→ "OC";gateway pylon→ "GATELY"). - Banner crops: 72 banners, 0 with the subject touching an edge, silhouette width
median 47.0 % (min 39.9 %, max 63.6 %), centre x median 53.5 %. Two composition
outliers, both reported rather than hidden:
hello-world(64 % wide, centre 63 % — an open box whose platform spans the frame) andmy-backup-was-using-someone-elses-api-quota(25 % — genuinely small object). - Live assertions (blog.hoelee.com): EN card, ZH card, banner webp and 400 px thumb are
byte-identical to the local files (sha256), 24 assets sampled across 8 posts with 0
mismatches; the retired
/og/<slug>.pngreturns 404;og:imageand the<picture>source resolve to the new.jpg/.webp; the light-theme rule is live in the built CSS (html:not([data-theme=dark]) … border-color:#0f172a1f; box-shadow:…);zhpost titles now print “ ” both on the card and in the HTML. - Two slugs no longer point at the consultant's accepted sample frames (
sample_oom_…,sample_prod_…) — those were the HUD-carrying frames.
Two observations for the owner (both now decided by the consultant's round-5 ruling):
- The hero banner's dead black left half is fixed by the renderer's reframing (above); what remains is that a genuinely slender subject (a thin cylinder) still reads small — that is the base frame's composition, and widening the caption
bboxwas measured not to change it. - Several frames are intentionally very dark; at full banner width the subject can read as small.
Owner decisions after round 7 (asked and answered 2026-10-19, no re-render needed):
- The hero banner stays textless. Asked point-blank whether the 1600×900 hero should carry the
headline (the base art's left 60 % reads like space reserved for text), the owner chose to keep it as
is: the title is the page's HTML
<h1>, and the text lives on the two share cards (EN + ZH) only. Do not burn a headline into the hero — baked-in text is unselectable, unindexable and needs a re-render per language. - "This post was never re-done" was a browser cache, not a missing artifact. The owner opened
/posts/five-step-ai-roadmap-what-i-actually-run/and reported it as not-done. Every artifact was present and live: five files 200 and byte-identical to disk (sha256 same), banner measured 47.5 % silhouette / 53.2 % centre,og:imagecorrect per language, both remotes at 0 unpushed. v1 and v2 serve the same URLs for a slug, so the browser answered from its own cache — a hard refresh showed the new art. Tell the owner to hard-refresh when reporting a restyle batch; if a swap must stick for returning readers, change the filename or add a version query instead of overwriting the path.
Three decisions still open (asked of the consultant in round 4 §二; the owner may simply pick the defaults):
- English headline rag → default: (C) shorten the card headline only, via
ogTitle_eninart-meta.json(article title untouched). - CJK headline length cap → default: ≤30 characters incl. punctuation (EN ≤65).
- List thumbnail crop → default: keep 100% right-focus; switch to 85% only if the 3-line bento cards read cramped at ~398 px.
Format decision — measured, take it unless the owner objects. A composited card renders 558 KB as PNG vs a 42 KB average today; ×144 that is ~84 MB of repo and a heavy share payload. Same card: 256-colour PNG 240 KB, mozjpeg q85 84 KB, q78 70 KB. --format jpg is the renderer default. ⚠ Shipping .jpg means:
ogImage:changed on 144 frontmatter files (/og/<slug>.jpg, ZH/og/zh-<slug>.jpg), andsrc/components/PostList.astroline ~42 must change from.replace(/\.png$/, …)to/\.(png|jpe?g)$/, or every listing thumb derives…-800.webpfromslug.jpgand 404s.
Order of work
- Generate the 70 base frames (GPU), named
<slug>.pngunderC:/AI/ideogram4/out/. - Write 70
evidencestrings intoart-meta.json— draft them from the posts, then have the owner approve; never invent a number. The renderer aborts and lists every slug that lacks one. - Dry-run the whole set to a scratch dir:
node scripts/og-gen/render_from_art.mjs --all --limit 4 --out "$TMPDIR/og-check"— expectFITon all four cards. - Render into
public/(--all), thenuv run --with pillow python scripts/optimize-images.pyfor the v1-derived WebPs only if any v1 file survives; the renderer already emits its own thumbs. - Do the
.jpgmigration (frontmatter ×144 +PostList.astro) in the same commit as the images, or the site points at missing files. npm run build, thennpm run preview-equivalent check + the four URL assertions, commit, pushoriginandgithub.
How to prove it worked (do not eyeball it — vision reads were unavailable on 2026-10-18)
- Reproducibility: re-render the two sample slugs and their banner + 800 px thumb must be byte-identical to the accepted delivery (sha256
16e6a7b0,f3b29f38,c4e28291,8095ec6c). - Card layout: probe
.og/gen/<slug>-{en,zh}.htmlin an iframe (serve the repo root over http —file://iframes are opaque origins) and asserttitleline count ≤3,documentElement.scrollHeight − 630 == 0, evidence barscrollWidth − clientWidth == 0. - Tags/banner:
python <skill>/scripts/measure_og_banner.py <slug> <repo>→rows==1,overflow==0, bannerdelta<=3— it only covers the tag row and terminal panel, not the headline. - After deploy: every
/og/<slug>.jpg,/og/zh-<slug>.jpg,/banners/<slug>.{webp,jpg},-800.webpreturns 200 with cache-busting (Cloudflare caches a negative 404 for 16 days).
Known gaps in the plan — updated 2026-10-19
- ✅ The OCR pseudo-text gate promised to the consultant is implemented:
C:/AI/ideogram4/ocr_gate.py, calibrated against the two accepted samples (--baseline). The consultant's bar (round 5) is glyph box ≥ 12 px on the 1360×768 frame AND ≥ 2 consecutive alphanumeric/percent characters; a single glyph (O,T,门) is exempt geometry noise. Judge legibility, never raw hit count — the accepted reference has a readable phrase in it, and passing00%@0.84 as noise is exactly what let60 %/100 %ship. - ✅
art-meta.jsonnow carries all 72 entries;scripts/og-gen/build_art_meta.pyregenerates them and re-checks coverage againstsrc/content/posts/*.mdbefore the renderer runs. - Extra thumbs: the renderer emits 400 px and 800 px; nothing else consumes them.
Phase A — Identity — verbatim (DONE)
Phase A — Identity (highest ROI, ~2–3 hrs total)
Step A1 — Add a real author photo (headshot). ✅ Done 2026-10-16 (batch B4) The #1 gap vs. every reference blog: the avatar is a letter "M" placeholder and there's no photo anywhere on the site. Every credible personal dev blog has a human face.
- Headshot supplied; two derivatives built (
lee-teong-hoe-640: 42 KB JPG / 26 KB WebP,-128: 3 KB / 2 KB). - Photo renders in the About hero, the author card on every post, the home hero (EN + ZH) and JSON-LD
Person.image. - Faceless
og-defaultreplaced twice since: the outside delivery's card now carries the same headshot (/og-default.jpg). - Governing doc:
design-guide.md§2 (author box), §5 (E-E-A-T name+photo consistency). - Done when: ✅ a real face renders in the author card on About + every post; no 404 for any derivative.
- ⚠ This checkbox stayed ⬜ for two days after the work shipped — the doc is a claim, the page is the fact. Re-checked 2026-10-18.
Step A2 — Align the homepage title & hero framing. ✅ Done 2026-10-18
- Pick one line: "Full-Stack Developer & DevOps Engineer" aligned across
SITE.title,SITE.description, homepage hero<h1>, and JSON-LDjobTitle. - Done when: ✅ homepage title, meta description, and hero all say the same thing about who Hoelee is.
Step A3 — Add a "Start here" / featured posts route. ✅ Homepage strip done 2026-10-18 (6e9d58b); dedicated page still deferred on purpose
New visitors land on reverse-chronological "Latest posts" with no guidance to the best content (Julia Evans' Favorites, Josh Comeau's featured posts both solve this).
- Homepage
Featuredsection: a 2×2 Cyber-Bento grid over four pinned flagship slugs, each with its OG thumbnail, category badge and a measured metric pill (3 Hosts · 162 Containers,11 Cron Jobs · 8 Pure Loops, …). EN + ZH twins, 1 column under 680px. /start-hereor/favoritespage — deliberately deferred: the homepage strip is the immediate win, and thepostslisting plusrelatedrows already cover discovery; revisit only if the four flagships stop being the strongest content.- Done when: ✅ homepage shows a featured/start-here strip above "Latest posts" (verified in the built HTML: 4
bento-card,featured-bento-gridpresent).
Phase B — Content — verbatim (the long game; batches B1–B7 all landed)
Phase B — Content (80% of value; the long game)
Step B1 — Write the 2nd flagship case study: "Self-Hosting a Mem0 Memory Stack". ✅ Done 2026-09-16 The Mem0 flagship is already the single highest-value unwritten post in the backlog.
- Write
src/content/posts/self-hosting-mem0.md(categorycase-studies). - Write Chinese twin
src/content/posts/zh/self-hosting-mem0.md(same filename → auto language-switch). - Follow the "hard job → post" template (§4 content-guide) + open with "why it matters" + end with hire CTA (§8 post-guideline).
- Governing doc:
content-guide.md§4/§8,post-guideline.md§8. - Done when: ✅ both EN + ZH pages live, language-switch works, hire CTA present.
Step B1b — Write the self-hosted STT case study. ✅ Done 2026-09-19
self-hosted-speech-to-text-api.md (EN + ZH): whisper.cpp on GPU + n8n auth gate +
nginx gateway, with the four build traps and the "5x faster than typing" business case.
- EN + ZH posts, custom OG + banner, hire CTA.
- Done when: ✅ both pages build, language-switch verified, images generated.
Step B2 — Write 2–3 short "gotcha" posts (Google-friendly, compound over time).
- "Replacing RDPGuard With IPBan: The Traps Nobody Documents" (EN + ZH,
devops, 2026-09-19) — the uninstaller that unbans 12 attackers,--install-servicedoesn't exist in v4.1.0,ExpireTimevsBanTime. Both images custom. - "When Your Database Client Lies to You: Patching Workbench 26 for MariaDB" (EN + ZH,
devops, 2026-09-19) — a client whose error handler crashed while reporting its own errors, masking every real failure; three patches to Oracle's bundled code, all stemming frommajor >= 8being an invalid MySQL-vs-MariaDB test. Both images custom. - "Why Chrome Forgets Its Tabs in a Container — And How I Fixed It" (EN + ZH,
devops, 2026-09-29) — the container kills the browser, so it never sees a clean exit and no restore mechanism fires (flag, hand-editedPreferences,RestoreOnStartuppolicy all verified failing); the 60-second snapshot keeper that fixed it, plus the staleSingleton*andcustom-cont-init.dpermission traps. Both images custom. - "The Forward-Auth Gate That Verified Perfectly — and Wasn't Live" (EN + ZH,
devops, 2026-09-29) — the B2 forward-auth item, delivered with a better villain than Traefik: the gate passed every local check (302 → outpost, branded login page) while the public URL served the app directly, because that hostname is served by a Cloudflare tunnel rule that bypasses nginx; addsskip_path_regex(SSO for the UI, open API), the "count before you substitute" revert trap (11 vhosts share the outpost port) and the same-second reload race. Custom OG + banner, hire CTA, commite7e4ee5. - "Site-to-site OpenVPN behind CGNAT"
- "Fixing the WordPress /cv 301→404 chain" (from own audit)
- Governing doc:
content-guide.md§3 (post type #3),post-guideline.md. - Done when: ≥2 gotcha posts live (these are
notes/devops, no Chinese translation required per §8). - ⚠ Note:
post-guideline.md§8 (newer) says every post gets a ZH twin — the "no Chinese required" note above is stale. The RDPGuard post was published EN + ZH.
Step B2b — Draft bank. ✅ Both published 2026-09-27 (EN + ZH, with OG + banner, verified 200 on 2026-09-29) — migrating-codeigniter-iis-to-openlitespeed and upgrading-codeigniter-46-to-47. The publish recipe below stays valid for the next draft. Remaining draft: authentik-css-greater-than-bug (E8).
Two finished posts (EN + ZH, each with frontmatter pointing at OG + banner paths) sitting in the repo but
not built or listed — draft: true excludes them from all listings and generates no pages.
| Slug | Category | Status | Assets |
|---|---|---|---|
migrating-codeigniter-iis-to-openlitespeed |
engineering |
drafted, unpublished | OG + banner PNGs not yet generated |
upgrading-codeigniter-46-to-47 |
notes |
drafted, unpublished | OG + banner PNGs not yet generated |
To publish one later:
- Flip
draft: true→draft: falsein bothsrc/content/posts/<slug>.mdandsrc/content/posts/zh/<slug>.md. - Set the real
pubDate(currently2026-09-20, the draft date) in both files. - Generate its images:
node scripts/og-gen/generate.mjs <slug>andnode scripts/banner-gen/generate.mjs <slug>(add aTERMINALS[slug]/BANNERS[slug]entry first for the custom panel). npm run build, commit,git push origin main.- Verify both URLs return 200 and the language switcher links them.
- Why these two:
engineeringhad only 1 post andtutorialsonly 1 — the blog was ~alldevops/case-studies. These put PHP/CodeIgniter (the actual day-job stack) on the blog, which is what a PHP full-stack recruiter searches for. - Governing doc:
content-guide.md§3/§4,post-guideline.md§8. - Done when: both are published live with EN+ZH, custom OG + banner, and verified 200.
Step B2c — (unplanned) Publish "No API for Browser Translation" — the English-mode story. ✅ Done 2026-09-29
adding-english-mode-to-a-chinese-only-web-app (category engineering, EN + ZH): the <html lang="en"> lie that
suppressed the browser's translate prompt, the fact that no browser-translate API exists, and the gateway-injected
dictionary + floating EN button (871 labels, 77–88% measured coverage). Custom OG + banner, hire CTA, commit 8a9ba6a.
- Why:
engineeringwas the thinnest published category, and the story is a rare, searchable gotcha with hard numbers. - Done when: ✅ both pages 200, language switch links both ways, sitemap hreflang pair, RSS entry, OG + banner served.
Step B2d — (unplanned) Publish "How I Vet an Open-Source Dependency Before Betting On It". ✅ Done 2026-09-29
vetting-an-open-source-dependency-before-you-bet-on-it (category devops, EN + ZH): the six checks that
corrected six assumptions from a 1,306-line architecture spec — repo vital signs via the GitHub API, grepping for
the feature instead of reading for it (OIDC/SSO/SAML → 0 hits), reading a feature doc's target branch
(white-label lives on multi-tenant, not main), reading the data model rather than the feature list
(percent/fixed vs five assumed rule types), the Community-vs-Enterprise tier gate (API tokens free, SSO paid),
and whether the money rail works in-country (selfhost + manual payouts, not Stripe Connect). Custom OG + banner,
hire CTA, commit 8711136.
- Why: forms a due-diligence cluster with
how-to-verify-a-hosting-provider-before-you-buyandhow-i-vetted-20-vps-providers-with-parallel-subagents— those cover vendors you pay, this covers code you depend on.devopsis the most differentiated category. - Done when: ✅ both pages 200, language switch links both ways, OG + banner served (1200×630 / 1600×900),
listing order monotonic on
/posts/,/and/zh/, Gitea Actions tasksuccess. - ⚠ Overlap to watch: two parallel in-flight posts cover adjacent material
(
nocodb-sso-is-a-licensed-feature,authentik-forward-auth-gate-wasnt-live). If those publish, add cross-links so the trio reads as a series rather than repetition.
Step B2f — (unplanned) Publish the agent-safety + CDP-automation pair. ✅ Done 2026-09-29
Two posts from the DigiKedai voucher-automation session, both backdated into the archive's empty 2025
stretch (EN + ZH, custom OG + banner, commit 170f5cc):
| Slug | Category | pubDate | updatedDate | What it is |
|---|---|---|---|---|
i-let-an-agent-manage-my-shopee-vouchers |
ai |
2025-07-08 | 2026-09-29 | the audit → propose → auto design, the mode: propose gate, and the list-lag mistake that created an unplanned RM140 voucher |
why-your-cdp-clicks-silently-fail |
devops |
2025-03-19 | 2026-09-29 | stale rect from scroll-behavior: smooth, coordinate clicks dropped on a hidden window, JS-dispatched events — and how to tell the three causes apart |
- Why 2025 dates: 2025-01 → 2025-10 was completely empty in the archive (nearest neighbours:
2024-09-24 and 2025-11-12). Backdating fills a real gap;
updatedDatekeepslastmod/dateModifiedhonest (post-guideline backdating rule). Neither post carries a date-, month- or version-pinned sentence — verified withgrep -nE "20[0-9]{2}|January|…|tonight|this week"before moving the date. - ⚠ B2's gotcha list is still open: the three bullets there (Traefik forward-auth, OpenVPN behind CGNAT,
WordPress
/cv301→404 chain) remain unstarted;why-your-cdp-clicks-silently-failcounts as an extra. - ⚠ Generator entries deliberately NOT committed. Another session had uncommitted edits in
scripts/og-gen/generate.mjs/scripts/banner-gen/generate.mjs, so theTERMINALS/BANNERSentries for these two slugs were applied through throwawaygenerate.local.mjscopies (deleted afterwards) and the images were committed as static files. Re-add the four entries below the next time one of these images must be regenerated, or when the next post needs a panel in the same style:
TERMINALS['i-let-an-agent-manage-my-shopee-vouchers'] = `
<div class="line"><span class="prompt">$</span><span class="cmd">python voucher_watch.py --auto</span></div>
<div class="line"><span class="prompt"> </span><span class="err">Confirm clicked · voucher not in the list — the list was 10 min stale</span></div>
<div class="line"><span class="prompt"> </span><span class="cmd">mode=propose</span><span class="fix">→ waiting for price confirmation ✓</span></div>`;
TERMINALS['why-your-cdp-clicks-silently-fail'] = `
<div class="line"><span class="prompt">$</span><span class="cmd">cdp click '.picker-item input'</span></div>
<div class="line"><span class="prompt"> </span><span class="err">clicked @591,361 · nothing happened · document.hidden=true</span></div>
<div class="line"><span class="prompt"> </span><span class="cmd">bringToFront + dispatch MouseEvent</span><span class="fix">→ picker opens ✓</span></div>`;
BANNERS['i-let-an-agent-manage-my-shopee-vouchers'] = {
titlebar: 'unraid — shopee voucher watch',
lines: [
{ t: 'cmd', text: 'python voucher_watch.py --auto' },
{ t: 'dim', text: 'vouchers have no draft state — Confirm = live + escrow' },
{ t: 'err', text: 'Confirm clicked · not in list · judged "refused" · clicked twice more' },
{ t: 'ok', text: 'it existed — the voucher list was ~10 min behind' },
{ t: 'err', text: 'cost: one unplanned RM14/29 voucher · exposure RM140' },
{ t: 'cmd', text: 'policy: mode=propose · cap RM300/month' },
{ t: 'ok', text: 'PROPOSAL RM9.60 / min RM18 × 20 → WAITING FOR PRICE' },
{ t: 'hl', text: 'agent-created vouchers since the gate: 0 · 4 live' },
],
flow: [
{ n: '1', label: 'read-only audit' },
{ n: '2', label: 'propose params' },
{ n: '3', label: 'human confirms' },
{ n: '4', label: 'create once ✓' },
],
};
BANNERS['why-your-cdp-clicks-silently-fail'] = {
titlebar: 'canary — seller centre via CDP',
lines: [
{ t: 'cmd', text: "click '.picker-item.end-picker input'" },
{ t: 'err', text: 'clicked @591,361 · picker never opened' },
{ t: 'dim', text: 'scroll-behavior: smooth → rect read mid-animation' },
{ t: 'ok', text: 'scrollBehavior=auto + behavior:instant → rect is real' },
{ t: 'err', text: 'document.hidden=true → coordinate clicks dropped' },
{ t: 'cmd', text: "['mousedown','mouseup','click'].forEach(dispatchEvent)" },
{ t: 'ok', text: 'picker opens every time · Confirm lands ✓' },
{ t: 'hl', text: '3 causes · 1 injected listener tells them apart' },
],
flow: [
{ n: '1', label: 'arm listener' },
{ n: '2', label: 'elementFromPoint' },
{ n: '3', label: 'JS-dispatch click' },
{ n: '4', label: 'opens every time ✓' },
],
};
- Done when: ✅ both pages 200 (EN + ZH), language switch links both ways, OG + banner served
(1200×630 / 1600×900), listing order monotonic on
/posts/,/and/zh/, Gitea Actions tasksuccess, andgit statusclean of other sessions' files.
Step B3 — Adopt the "hard job → post" habit.
Every solved problem becomes a notes entry the same week.
- Revisit cadence target: 2 posts/month → 1/week (
content-guide.md§5). - Done when: 3 consecutive months hit the 2-posts/month floor.
Step B2e — (unplanned) Two posts out of the SSO / forward-auth session. ✅ Done 2026-09-29 Same working session that wired (and then deliberately rolled back) an authentik forward-auth gate in front of a self-hosted app produced two posts:
| Slug | Category | What it argues | Commit |
|---|---|---|---|
authentik-forward-auth-gate-wasnt-live |
devops |
the gate verified perfectly from the host while the public URL bypassed it — two ingress layers per hostname; verify from outside and read which software answered (x-powered-by) |
e7e4ee5 |
nocodb-sso-is-a-licensed-feature |
notes |
the OIDC env vars are real and enforced at boot, but the feature is Business+; on an unlicensed build an unauthenticated GET /auth/oidc throws and exits(1); MySQL meta blocks licensing; the "drop-in" community fork is abandoned (0.255.2, 2024-10-29) |
2d0de72 |
- Both EN + ZH, custom OG + banner, hire CTA; both link to each other (one-way: the NocoDB post links to the gate post).
- Why: the trap is rare and genuinely searchable (
authentik forward auth,nocodb sso self-hosted), and both are first-person debugging stories with measured evidence — the moat percontent-guide.md§7. - Done when: ✅ 4 pages 200 with expected content, language switch links both ways, 4 images served as
image/png.
Step B2f — (unplanned) Four monitoring posts out of one Prometheus/Grafana session. ✅ Done 2026-09-29
One working session that unified monitoring across unRaid + Synology DSM + a VPS produced four posts. All four are
backdated into the 2026-03-25 → 2026-09-04 archive gap (that stretch had no posts) with updatedDate: 2026-09-29
holding the real date, so the sitemap lastmod stays honest and listings still sort by pubDate:
| Slug | Category | pubDate | What it argues |
|---|---|---|---|
smartctl-exit-code-32-skips-the-disks-that-matter |
notes |
2026-04-14 | smartctl's exit status is a bitfield, not a boolean: rc=32 means "SMART OK, attributes were below threshold in the past". An if ! smartctl guard skipped 2 of 4 SSDs — exactly the marginal ones. Fix: mask the informational bits (32/64), export rc as a metric. |
why-your-grafana-dashboard-shows-no-data |
devops |
2026-05-17 | A template variable defined as label_values(...{nodename=~"$nodename"}) filters on itself → 0 options → $node empty → every panel No data while all targets are up. Also: why hand-substituting variable values during verification hides exactly this bug, and $__all ≠ .* in automated panel checks. |
your-disk-full-alert-is-lying |
devops |
2026-06-24 | Percentage thresholds on multi-TB volumes fire while 500 GB remains; 92% "memory used" with 4.8 GB available is cache, not pressure. Alert on consequences: bytes free, MemAvailable, steal >50%. Includes the "keep the comparison in the threshold condition" rule and the mount-selector exclusions. |
one-prometheus-for-unraid-synology-and-a-vps |
case-studies |
2026-07-29 | The flagship: node_exporter vs cAdvisor coverage matrix; name!="" for cAdvisor's non-container cgroups; "total storage" counting one NAS volume three times (/volume1, /opt, CIFS re-mount) and the dedup selector; a KVM guest exporting no CPU frequency at all (textfile collector, distinct metric name, merged with or); a container reporting its own ID as nodename. Result: 7 targets, 47 cores / 158 GHz / 142 GB / 64 TB / 155 containers on one screen. |
- All four EN + ZH, custom OG + banner, hire CTA naming "self-hosted monitoring pipelines"; no post carries an absolute
date or "recently/as of" phrasing, which is what made the backdating safe (per
post-guideline.mdbackdating rule). - Why: the blog had zero Prometheus/Grafana/monitoring posts while
content-guide.md§2 lists monitoring underdevops, "my most differentiated material" — andmonitoring/grafana no data/smartctl exit codeare heavily searched by exactly the audience this blog targets. - Done when: ✅ 8 pages 200 with expected content, language switch links both ways, 8 images served as
image/png, archive order still monotonic on/posts/, the homepage and/zh/.
Step B2g — (unplanned) Three posts out of the Synology Office / spreadsheet-API session. ✅ Done 2026-09-29
One session spent making a Synology NAS read, write and chart spreadsheets produced three posts. All three are
backdated into the 2024-09-24 → 2025-11-12 gap — the widest stretch in the archive with no posts — with
updatedDate: 2026-09-26 holding the real date, so lastmod stays honest and listings still sort by pubDate:
| Slug | Category | pubDate | What it argues |
|---|---|---|---|
synology-spreadsheet-api-is-a-container |
devops |
2025-08-20 | The flagship. Enumerating the DSM gateway (1,515 APIs) showed no cell-level Office endpoint, so I concluded the NAS had no spreadsheet API — wrong: it ships as the container synology/spreadsheet-api (image tag ↔ Office version table). Plus two diagnostics that lied (synopkg is_onoff reporting a running package as "not turned on"; ps without sudo on DSM listing only your own processes, which made a live stack look dead), a required AUTH_SECRET whose absence crashes with a minified stack trace, a 401 with provably correct credentials, 2FA that can never authenticate, 403 vs 404 semantics, a personal My Drive unreachable by any service account, and the verified fix — read/write/CSV/.xlsx with Synology's own engine evaluating the formulas, then a chart out the far end. |
synology-api-401-with-the-correct-password |
notes |
2025-09-10 | The two causes of a 401 when the password is right: host must be an FQDN whose certificate the proxy accepts (a bare LAN IP fails its TLS handshake, and a failed handshake is reported identically to a bad password), and a 2FA account can never sign in (AuthorizationBody has no OTP field). Includes the three-command triage that separates them, and why a token that worked yesterday returns 401 today — it's bound to the DSM session, not just to a 28-day clock. |
reading-a-containers-own-api-docs |
notes |
2025-10-01 | Extract a container's contract from the artifact instead of the vendor's page: --entrypoint cat the bundled OpenAPI spec, --entrypoint grep the bundle for the env-var contract and the defaults, read Env/Entrypoint/Cmd from the registry config blob without pulling a byte, decode the real listening port from /proc/net/tcp, and run detached to read startup logs without hanging the shell. |
- All three EN + ZH, custom OG + banner (centering measured, not eyeballed: gapAbove/gapBelow 47/49, 76/78,
106/108,
delta=2px,overflow=0), hire CTA naming self-hosted integrations; the twonotesposts link up to the flagship with a relative link. - Why: the search results for
synology spreadsheet api/spreadsheet-api dockerare Synology's own Hub page, a German how-to and two MCP wrappers — nothing covers the failure modes, and the "vendor tool told me the wrong thing" shape matches the blog's strongest existing genre (patching-workbench-26-for-mariadb,when-smart-says-healthy-but-your-raid-is-corrupting-data). - Done when: ✅ 6 pages 200 with expected content, language switch links both ways, 6 images served as
image/png, archive order still monotonic on/posts/, the homepage and/zh/.
Step B2h — (unplanned) Audit + backdate of the six posts that landed on 2026-09-29. ✅ Done 2026-09-29
One publishing day put six EN posts on the same pubDate, so /posts/ opened with a single-day dump. Each was
checked for date-, month- and version-pinned prose before its frontmatter was touched; two carried no pins and were
moved into the archive's empty months with updatedDate: 2026-09-29 holding the real date:
| Slug | Category | pubDate → updatedDate | Why it was safe / blocked |
|---|---|---|---|
adding-english-mode-to-a-chinese-only-web-app |
engineering |
2025-06-11 → 2026-09-29 | no absolute date, month name, version or relative-time phrasing anywhere in EN or ZH; fills the empty 2025-06 |
authentik-forward-auth-gate-wasnt-live |
devops |
2026-08-12 → 2026-09-29 | no pins. nocodb-sso-is-a-licensed-feature links back to it, so it must stay dated earlier than 2026-09-29 (it does), and the "licensing story in its own write-up" forward reference now reads as weeks rather than months. Fills the empty 2026-08 |
- The four that had to stay on 2026-09-29, and the exact sentence that pins them:
read-only-nocodb-dashboard-for-a-remote-database— image tagnocodb/nocodb:2026.09.0and a returned row timestamp2026-09-27 01:54:16+00:00→ floor 2026-09-27.nocodb-sso-is-a-licensed-feature— same2026.09.0image tag → floor 2026-09-01.vetting-an-open-source-dependency-before-you-bet-on-it— quotes the GitHub API as "last push 2026-09-10" → floor 2026-09-11.why-chrome-forgets-its-tabs-in-a-container— the setup table namesChrome 154(≈ Oct 2026 on Chrome's cadence) and the post links back toscraping-bot-walled-marketplace-warm-browser-session(2026-09-13) as something already written → floor 2026-09-13.
- Still empty, and therefore the spare slots for the next batch: 2024-10 → 2025-02 (five months) and 2025-04/05.
- Done when: ✅ build clean,
lastmod= 2026-09-29 for all four URLs (EN + ZH), listing order still monotonic on/posts/, the homepage and/zh/, both article pages render the historical date.
Step B2i — (unplanned) The web3 category, plus two engineering posts. ✅ Done 2026-09-29
web3 had zero posts and no route at all: /categories/ rendered its card with the label
"0 posts · coming soon" as a non-link, and /categories/web3/ returned 404 (Astro only emits a
category detail route once the category has posts). A Gitea sweep found seven real web3 repos whose
commits date to 2024-08-15 → 2024-08-19, which is also why those posts could be backdated honestly.
Five posts shipped (EN + ZH, custom OG + banner, hire CTA):
| Slug | Category | pubDate | updatedDate | Source repo |
|---|---|---|---|---|
fully-on-chain-svg-nfts |
web3 |
2024-10-08 | 2026-09-29 | foundry-nft — MoodNft.sol, DeployMoodNft.s.sol |
why-my-on-chain-nft-art-changed-on-windows |
web3 |
2026-08-19 | — | foundry-nft — the .gitattributes fix commit |
chainlink-vrf-v2-lottery-contract |
web3 |
2024-12-10 | 2026-09-29 | hardhat-smartcontract-lottery — Raffle.sol |
verifying-a-pdf-report-page-by-page |
engineering |
2026-09-28 | 2026-09-29 | numerology-report — docs/pdf-pipeline.md |
jpa-version-field-lost-update |
engineering |
2026-08-19 | — | springboot-hoelee-demo — @Version |
- Why these dates: the two 2024 posts fill the empty 2024-10 and 2024-12 archive months with the real
work date and
updatedDateholding the true date, solastmodstays honest. The two 2026-08-19 posts use the real work date rather than joining the 2026-09-29 pile-up.verifying-a-pdf-report-page-by-pagewas moved one day to 2026-09-28 for the same reason — it was the fifth post landing on 2026-09-29. - ⚠ Date pins were re-checked before moving any date. The only
202[0-9]hits in the two backdated posts are inside the contract address0xc2022b56…, not dates. All cited figures were traced to source:868596/4102 bytes/993568gas /0.000535185588997216 ETH/ block6522146/ mint181874(deploy + mint logs),335011gas (.gas-snapshot),2.5pt→7.5pt+20,225,818bytes +30–490pt(docs/pdf-pipeline.md; the27 pages/12,789 pagesfigures live inapp/Libraries/ReportPdf.phplines 21 and 196, not in the doc),@Version+POST_VERSION_CONFLICTin the Spring demo. - ⚠ The
TERMINALS/BANNERSentries for all five slugs ARE committed this time (unlike B2f, which had to hide them in this file because a parallel session held uncommitted generator edits). Both generators were verified clean and the diffs purely additive (30/0 and 100/0) before editing. - Banner centering measured, not eyeballed: all five at 8 rows,
gapAbove/gapBelowwithin 2px,overflow=0,scrollHeight == clientHeight == 636. - Done when: ✅ 2 new category routes (
/categories/web3/,/zh/categories/web3/), build 127 pages clean, all 10 post URLs + 10 images 200, language switch both ways, listing order monotonic on/posts/,/,/zh/.
Step B2j — (unplanned) One post out of the analytics/SSO session, plus the tracker wired into three sites. ✅ Done 2026-09-30
The analytics session (Step E1) produced one flagship post and the material to justify it:
| Slug | Category | pubDate | Commit | Live |
|---|---|---|---|---|
one-hostname-public-tracker-sso-dashboard |
devops |
2026-09-30 | 54d8cce |
EN + ZH + OG + banner all 200 |
- Angle: "public ingest, private dashboard" on one hostname — the authentik proxy provider +
skip_path_regexpattern, written up as the correct counterpart toauthentik-forward-auth-gate-wasnt-live(which documented the gate that was never live). The two posts cross-reference the same instance from opposite directions, which is exactly the compounding an authentik series is supposed to do. - Two traps are the post's core value (both cost real time in this session): (1) cloning a working
provider that used
mode=forward_singleleavesinternal_hostempty, so the SSO chain is textbook-correct while every app path 404s — the tell isx-powered-by: authentikon the 404; (2) the embedded outpost needs 1–2 minutes to pick up a provider change and answers302 → /flows/-/default/authentication/plus 404s until it does, which reads as "the wiring is broken". - Also in the post: why nginx basic auth can never gate this app (the browser carries one
Authorizationheader, so the app's own bearer token displaces Basic and every API call 401s), the honest trade-offs (double login — no OIDC in the app; dashboard is as available as the outpost; one LAN break-glass port), and the public-internet verification table (includingPOST /api/sendreturning an app-level 400 rather than a gate 403 — proof the request reaches the app). - Generators committed with the post (both clean, diffs additive-only 5/0 and 21/0): 3-line
TERMINALSentry, 8-rowBANNERSentry with the 5-step flow. - Verified: build clean (129 pages, Pagefind 2 languages), 14 content assertions on the deployed pages (framing, both traps, CTA, language switch both ways, tracker tag, og + banner), listing order monotonic with the new post newest (2026-09-30), Gitea Actions run 122 success, EN/ZH/OG/banner 200 within a minute.
- ⚠ Not done: no synthetic pageview this time — the real-Chrome CDP window (9222) was closed by the time the post shipped, and the house rule is to verify analytics with a real browser rather than a headless one. The page carries the tracker (asserted in the HTML), so the first real visit is what will show up.
Tracker now wired into three sites (E1 progress, 2026-09-30) — the analytics deployment is no longer "installed, not collecting":
| Site | website ID | Where the snippet lives | Deploy path |
|---|---|---|---|
blog.hoelee.com |
1817d8f6-0e49-4b9a-a50d-307e4f2962c7 |
src/layouts/BaseLayout.astro (EN + ZH share it) |
Gitea Actions, ~1 min |
www.digikedai.com |
61b57143-f3f4-4353-9ede-9c615146de84 |
dsm-resource-management/www/src/layouts/Base.astro |
npm run build + ./deploy.sh (CF Pages) |
www.sifumail.com |
9d76b8aa-486a-4e4a-b344-f1f93e9ba292 |
all 4 pages' <head> (static site, no shared template) |
scp to the VPS docroot /docker/web/www.sifumail.com/public/ |
⚠ Verification pitfall (cost me a false alarm): Umami drops bot/headless user agents. The same payload
returns 200 {"beep":"boop"} and stores nothing from HeadlessChrome, but 200 {"cache":"…"} and stores
from a normal Chrome UA — so "0 pageviews after a headless test" is not a bug. Verify with a real browser.
Step B2k — (unplanned) Two posts out of the reader/book.hoelee.com session: the upstream rewrite, and the front-door pattern. ✅ Done 2026-10-06
| Slug | Category | pubDate | Live |
|---|---|---|---|
the-upstream-was-deleted-then-came-back-rewritten |
devops |
2026-10-06 | EN + ZH + OG + banner all 200 |
putting-a-front-door-on-an-app-you-cant-modify |
devops |
2025-06-24 (backdated) | EN + ZH + OG + banner all 200 |
- Why these two, from the reader's own setup (stack 136) rather than a generic idea: the existing coverage of that
project was the translation post (
adding-english-mode-…) and the TTS post. The untouched material was (a) the dependency's death-and-resurrection and (b) the gateway/front-door packaging pattern. - ⚠ The upstream facts changed while the skill still said "dead". Re-verified live 2026-10-06:
github.com/hectorqin/readeris alive — 11,036★ / 5,471 forks / AGPL-3.0 / default branchmain, last push 2026-10-02 — but it is a from-scratch rewrite: the git history was re-initialised 2026-09-16 (chore: 初始化仓库) and holds 233 commits; Kotlin/Spring+Vert.x → TypeScript/Node, port 8080 → 5888,/storage/dataJSON →/dataSQLite, env config → admin-UI settings in the DB, image moved tocnb.cool/hectorqin/reader:main(branch/commit tags, deliberately no:latest), 0 releases / 0 tags; Docker Hubhectorqin/readerstill 404. Upstream's own P0 notes admit the image-pull + container upgrade drill was never run and legacy-data auto-migration is unimplemented. The running deployment is unchanged (liangnianzhi/reader.hectorqin:latest-20250525). Thehectorqin-readerskill was corrected in this session — a future session must not repeat "upstream is dead, there is no upgrade". - Post 1 writes that up as "dead vs rewriting" with the reusable checks (repository health ≠ artifact health; no tags ⇒ every upgrade is a SHA; mirror source and image; compare the shape before the features; read the maintainer's "not done yet" list; AGPL-3.0 if you run it for paying users).
- Post 2 is the gateway pattern: a second
nginx:alpinecontainer owning the public port, the request-timeresolver 127.0.0.11(so the front page survives the app being down),client_max_body_sizepreserved for uploads, why a proxy body rewrite cannot touch a client-rendered SPA, the "reload → back to the front door" injection with itssessionStorage.gatePassconsume-step, the app's own CSS hook for whitelabelling, and the silent-failure warning (the injections depend on the literals<html lang="en">and</head>). - ⚠ Backdating rule applied to post 2 only. Post 1 is date-pinned (its facts are Sept–Oct 2026) so it ships on the
real date. Post 2 went into the empty 2025-06-25 → 2025-07-07 window at
2025-06-24,updatedDate: 2026-10-06solastmodstays honest — and it is constrained to link backwards only (it cross-linksadding-english-mode-…, dated 2025-06-11); a forward link to a later-dated post would contradict the backdate. - Generators committed with the posts (both were clean,
deletions: 0,node --checkOK) viascripts/append_generator_entries.py: 4-lineTERMINALS+ 8-rowBANNERSper slug. - Verified after deploy: build clean (139 pages, Pagefind 2 languages, no duplicate-id warning);
measure_og_banner.pyPASS on both (ogrows=1 overflow=0 missingHash=0; banner 8 rows,delta=2,overflow=0); 8 live URLs 200 with the right content types; sitemap 139 URLs, all four new entrieslastmod 2026-10-06with hreflang alternates; EN and ZH listing rows monotonic with the new posts at #2 (2026-10-06) and #54 (2025-06-24, between 2025-07-08 and 2025-06-11); language switch links both ways; CTA links present (Cloudflare rewrites themailto:toemail-protection— expected). Commit378d55c, pushed Gitea + GitHub, both remotes 0/0; follow-upef3f520sharpened the wipe evidence after checking the actual repo (not just the API):masteris a single commit dated 2026-06-22 whose README is the worddeleted, sharing no common ancestor withmain— the API'screated_atstill reads 2021-08-13 and would have misled a reader who trusted it. - Upstream source mirrored to Gitea (private) the same session:
hoelee/reader-2026-rewrite— full clone ofgithub.com/hectorqin/reader(main@5c59cae, plus the 1-commitmaster), pushed withgit push --mirror. ⚠ Gitea's hook rejectsrefs/pull/*("hook declined") — that is expected and harmless; branches and the whole object history land. A future session should re-push from a freshgit clone --mirrorto refresh it, or switch the repo to a Gitea pull mirror (the existinghoelee/readeruses the 8h auto-sync mirror feature). - ⚠ Reminder for the next session: the assertion method matters — hrefs must be checked against raw HTML, not the
tag-stripped text (a first pass reported the CTA and cross-links "missing" because tag-stripping removes
href).
Step B2l — (unplanned) Two posts written from the 2026-10-09 → 10-16 sessions. ✅ Published 2026-10-16 (7b001dc)
Both were written, built and image-complete, then held for the user's read and published the same day on his
go-ahead: 8 URLs (EN / ZH / og / banner × 2) verified 200, /posts/ order still monotonic, lastmod =
2026-10-16 on both. A third post written in the same batch (the-dialog-opened-in-7-seconds-my-script-waited-5,
category ai) was dropped and deleted — see the note under the table.
⚠ The 7b001dc message claims "build 159 pages"; the real count was 153 — both posts had draft: false from the
start, so publishing added no pages. The number in that message is wrong; the deploy itself was verified by URL.
| Slug | Category | Proposed pubDate |
Source session (Hermes id) | What it argues |
|---|---|---|---|---|
calibrating-intel-quick-sync-hevc-quality |
tutorials |
2026-10-14 | 20261014_132458_102ffe |
The full QSV calibration: ICQ 18–22 inflate thin sources (size gate 176.8/141.8/107.6%), the usable range starts at 24, the quality cliff sits between 28 and 30, excerpts rank quality but never speed (fast == medium byte-for-byte, 168,435,266 B), the four traps (/lib mount, rgb24 rate control, SKIP already HEVC hvc1, one QSV job at a time) |
docker-reported-exit-0-the-oom-killer-disagreed |
devops |
2026-10-16 | 20261016_034103_770729 |
"Container stopped unexpectedly" while docker inspect reports ExitCode 0 / OOMKilled=false — the truth was 6 cgroup OOM kills in dmesg (uptime stamps matching FinishedAt), driven by a crawler flood (25,976 IPs / 50k lines, 33,636×403 + 16,082×429); fix = 512M → 2g + memswap_limit: 2g, labelled a stopgap; plus the "my own bare curl got 403" verification trap |
-
Deleted (do not resurrect):
the-dialog-opened-in-7-seconds-my-script-waited-5(the auto-LinkedIn composer race). Written 2026-10-16, briefly parked as a draft, then deleted outright on the user's instruction in3e354cc: both.mdfilesgit rm'd and itsTERMINALS/BANNERSentries removed from both generators (pure deletions — 0 added, 17 + 5 removed,node --checkOK, 0 references left to the slug, the other three intact). Do not bring this topic back unless the user asks for it. -
⚠ Backdating verdict for this batch (do not re-litigate): neither held post may carry a historical date.
docker-reported-exit-0-…: its evidence is dated —Oct 12,Oct 16, the times 02:45 / 02:58 / 03:39 / 05:33 / 07:37 / 07:57, "two weeks of uptime", "18 minutes after the restart". Any older date makes the post contradict its own table. Ships on the real date (2026-10-16),updatedDate= the day it is actually pushed.calibrating-intel-quick-sync-hevc-quality: the prose carries no calendar date, but two checkable pins fix it to 2026-10-14: (a) it names ffmpeg 9.0.2, and ffmpeg.org lists 9.0.2 as released 2026-09-18 (9.0 on 2026-08-04), so nothing before that date is honest; (b) the premise — a shipping QSV tier whose defaults mean "uncalibrated" — only existed from the tier landing (Batch 4b-1, 2026-10-14 11:41) to the calibration that evening (Batch 4b-2). KeeppubDate2026-10-14,updatedDate= the real push day.- Deep backdating (2024/2025 archive gaps) therefore needs evergreen posts only — see B2m for which queued topics qualify.
-
State of each: EN + ZH written;
rewrap_post.py→wrap_lists.pyapplied to the EN files (ZH untouched, one paragraph per line); OG + banner generated and measured (measure_og_banner.py→ PASS on all four pairs);npm run buildclean (155 pages, no duplicate-id warning); all 8dist/pages verified locally (language switch both ways, content assertions from whitespace-normalised tag-stripped text, listing order monotonic on/posts/,/and/zh/). -
⚠ The generators already carry all four
TERMINALS/BANNERSentry sets — the three for this batch's slugs (two held + one parked) rode into2295c0ftogether with the authentik entry (git stages whole files). They are inert until a slug is actually generated, so publishing each post needs only its two.mdfiles plus its two PNGs. -
To publish one: confirm the real
pubDate(date, and the newest<time>on the live/posts/), thengit addthe four explicit paths (post EN/ZH + og + banner) and pushorigin main; verify the four URLs 200.
Step B2n — (unplanned) The silent-JavaScript-failure post, backdated into the 2025-02 gap. 🔵 held for the read
| Slug | Category | pubDate |
updatedDate |
Source |
|---|---|---|---|---|
two-ways-a-javascript-file-stops-working-and-looks-fine |
engineering |
2025-02-20 | 2026-10-16 | hevc-transcode docs/OPERATIONS.md §10.33 |
- What it argues: a parameter named
tshadowing the i18nt()function, and alet LANGdeclared after its first call (TDZ) — two unrelated one-line causes that each aborted the whole script while the page kept rendering the inline default-language text, so nothing looked wrong. The fix is asserting on effects in a real browser (typeof t === 'function', every inline handler resolves — 219 attributes / 217 named calls), plus a pseudo-locale (?lang=pseudo, strings stretched 1.6×) that catches untranslated nodes and overflow. - Why the 2025-02-20 date: the prose carries no calendar date and no checkable version pin, which is the
condition for backdating (see B2m's note). It fills the empty stretch between
why-your-cdp-clicks-silently-fail(2025-03-19) andchainlink-vrf-v2-lottery-contract(2024-12-10) — verified in the built feed: the post lands at position 64 of 67, monotonic, withlastmod= 2026-10-16 fromupdatedDate. - ⚠ Consequence of the deep date: no internal links. Every post it would naturally reference is dated later, and
a backdated post may only link backwards — so it ships as a stand-alone page. If the cross-links are worth more
than the gap (e.g. to
it-reported-success-nothing-had-changed, 2026-10-08, the same "trust the signal, not the intent" theme), move the date into the recent window instead; that is a one-line frontmatter change in two files. - State: EN + ZH written (
rewrap_post.py→wrap_lists.pyon EN only), og + banner generated and measured (ogrows=1 overflow=0; banner 8 rowsdelta=2 overflow=0), build clean at 155 pages, bothdist/pages asserted locally (language switch both ways, content, date 2025-02-20, sitemap hreflang pair). Not committed, not pushed — the working tree also carries the two generator entries for this slug (additive: 5 / 17 lines, deletions 0).
Step B2o — (unplanned) The four surviving candidates, written and dated. ✅ Published 2026-10-16 (08fa7a9)
| Slug | Category | pubDate |
Source |
|---|---|---|---|
my-backup-was-using-someone-elses-api-quota |
notes |
2026-10-15 | Hermes 20261015_170438_0e4312 |
space-reclamation-wasnt-stuck-it-was-waiting |
devops |
2026-10-14 | Hermes 20261014_203035_c6b198 |
adding-a-domain-to-someone-elses-mail-server |
notes |
2025-05-06 | Hermes 20261014_092537_4759b3 |
embedding-fonts-so-a-pdf-looks-the-same-everywhere |
engineering |
2025-01-22 | numerology-report docs/v2/FONTS.md + Hermes 20261011_085128_453ab1 |
- What each argues. (1) A 1 TB restic seed stalled at zero bytes because rclone's shared OAuth client was
rate-limited (
consumer: projects/202264815644,quota_limit_value: 840000) — own the client, Desktop-app type, Publish the app or the refresh token dies every 7 days, and the callback accepts exactly one request; the old side gave back 1.012 TiB → 3.800 GiB while rclone reporteditemNotFoundafter finishing. (2) The DSM Storage Manager card readsrunning24×7 by design (run_all_time,schedule_table168×1) — free space actually came back +69.0 / +46.8 GiB at the 09:29/09:34 snapshot rotations while the recycle bin held 5 GiB and the afternoon wrote −47.2 GiB; retention 1 means a delete frees only at tomorrow's rotation. (3) Inbound needs four records and worked first try; outbound hit554 5.7.1(no PTR, 4 RBLs clean) and the relay hit550-From header sender domain not verified; PTR is one per IP, not per domain, forwarding needs Sieve and still cannot leave the box, so the design that survives is a receiving-side POP pull — verified now: forwardmail.hoelee.com → 41.216.187.102resolves, the reverse lookup for that IP still returns nothing. (4) Four families / eight static faces embedded: static only because the PDF engine's parser readsglyf/locaand ignoresfvar(variable fonts resolve to Thin 100 / ExtraLight 200),font-weight: 600rewritten toboldfor the PDF side only, GB2312 ∪ Big5 subsetting silently cut real name characters (犇 骉 頔 祎 廸 玧 祾 赟 堃), and a glyph lint that fails the build. - Dates and their justification. The two 2026 posts are pinned by their own evidence (the seed's quota errors and
today's samples) and ship on their real work dates; 2026-10-14 now carries two posts (this one and the QSV
calibration) — within the E7 limit of ~3–5. The two 2025 dates are backdating: both have no calendar date, no
quoted log timestamp and no checkable version pin in the prose (verified: the mail post's only month reference was
removed, the fonts post cites no engine version). They fill 2025-05 (between 2025-03-19 and 2025-06-11) and
2025-01 (between 2024-12-10 and 2025-02-20) — both confirmed in the built feed, monotonic, at positions 64 and 67.
updatedDate= 2026-10-16 on all four, so everylastmodis the real day. - State: EN + ZH written (
rewrap_post.py→wrap_lists.pyon EN only, ZH one paragraph per line); og + banner generated and measured for all four (rows=1 overflow=0, banner 8 rowsdelta=2 overflow=0); build clean at 163 pages; 8dist/pages asserted (language switch both ways, dates, content, sitemap hreflang, images). - ✅ Published the same day on the user's go-ahead (
08fa7a9, 18 files): all 16 live URLs verified 200 (4 posts × EN / ZH / og / banner, correctcontent_type), live feed monotonic at positions #2 (2026-10-15), #3–#4 (2026-10-14), #65 (2025-05-06) and #68 (2025-01-22), sitemap 163 URLs withlastmod2026-10-16 on all four EN/ZH pairs, and the two ZH pages carry the correct Chinese<title>+ self-canonical + hreflang pair. Note for the next session: the EN and ZH pages ofspace-reclamation-…happen to be byte-length identical (20492 B) while differing in content — that is a coincidence, not a broken route (md5 differs, ZH contains 空间回收 and not the English title). The pre-existingog:locale = enon every ZH page is Step E5, untouched here. - Internal links: the two recent posts link backwards (to
running-production-infrastructure-solo, 10-06;it-reported-success-nothing-had-changed, 10-08;your-disk-full-alert-is-lying, 06-24). The two backdated posts carry no prose links — every post they would naturally cite is dated later, and a backdated post may only link backwards. Their "related posts" rows are the theme's automatic ones, which every backdated post already has.
Step B2p — (unplanned) The commit-churn post. ✅ Published 2026-10-18 (0d73842), after the owner's read and a voice rewrite
| Slug | Category | pubDate |
Source |
|---|---|---|---|
why-500000-lines-of-commit-churn-only-left-230000-in-production |
engineering |
2026-10-18 | Hermes session 2026-10-17 |
- What it argues: thirty days of autonomous-agent work across five self-hosted Gitea repos = 865 commits, 518,233 additions, 130,892 deletions — and production kept 235,000 lines. Verification gates and pruning, not generation speed, are what survive.
- The owner's read changed two things (both before publishing):
- Voice. The draft opened "When managers or recruiters ask how productive an engineer is…" and kept addressing
the reader in the second person (
you have to move beyond the chat box,If your Git log shows…) — it read like a LinkedIn think-piece rather than a log entry, and it framed the post as written for recruiters. Rewritten in the first person (ENyou/your12 → 2, both in the CTA; ZH 你 5 → 2), headings changed from essay titles to actions (What the agents actually touch,What changed in how I work). All numbers, the table and the claims are unchanged — this was a voice edit, not a new draft. - Title.
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 万行).
- Voice. The draft opened "When managers or recruiters ask how productive an engineer is…" and kept addressing
the reader in the second person (
- Two real defects found while fixing the card: the draft said "four automated gates" and then listed three
(now fixed to three); and
og-gen's title splitter broke at the word-count midpoint, which put the break at…Lines in / 30 Days— the splitter now prefers a sentence boundary near the middle and falls back to the midpoint (.og/genmeasured afterwards: 2 title lines,overflowY 0,measure_og_banner.pyPASS · tags 1 row · banner delta 2). - State: EN + ZH written and rewritten; og PNG regenerated for the new title, thumbs + banner WebP re-derived
(
optimize-images.py), build clean,/posts/order monotonic with the post on top at 2026-10-18. - ⚠ The four files were held untracked for two days on purpose (publication gate). Nothing else is held now.
Step B2m — The next-session topic queue (user decision 2026-10-16). ⬜
From the same sweep, explicitly not to be written: the Photoshop "closes every five minutes" post (#6 in the sweep) and the Shopee abnormal-status / voucher-card post (#12). Everything below is queued, in the user's requested order of interest, each with its source session:
⛔ Dropped by the user on 2026-10-16 — do not write these, do not publish them. Of the seven candidates below, only #2 (rclone/Drive), #4 (NAS space reclamation), #6 (mail domain/PTR) and #7 (embedded fonts) survive; they are written and dated in Step B2o. The other three are dead: #1 The Packaged App 404'd Its Own CSS, #3 The UI Said "Idle" / the savings ledger and #5 The Scan Failed Because an HTML Error Page Was Parsed as JSON. The same instruction killed the four candidates that surfaced later the same day and were never entered here — the pause-button hole (
pause_nowonly killing whenself.running and self.pid), the ten rows that passed a gate that did not exist yet, one-file-one-worker, and the empty-path 400. Do not resurrect any of them without a fresh request from the user.
Which of these can carry a deep historical date (the user's 2026-10-16 question). Deep backdating needs a post whose facts are undated — no calendar date, no quoted log timestamp, and no version whose release date is checkable. In this queue that is only two: the mail-server/PTR post (concepts, no events) and the PDF font-embedding post (four font families, no dates). Both fit the big empty stretches — 2024-09-25 → 2025-03-18 (six months) and the near-empty 2025-07 → 2025-12 — with
updatedDate= the real date. The other five are recent-window only: the NAS memory and idle-guard posts quote timestamped samples, the rclone one rests on a dated quota event, the scan failure and the router-401 posts both quote a runtime/version state.
| Topic | Category | Source | The hook |
|---|---|---|---|
| Moving a 1.5 TB cloud backup OneDrive → Google with rclone | notes |
20261015_170438_0e4312 |
seed stalled at 0 bytes/90 s with 159 rateLimitExceeded; gcloud holds only a TTS service account so the console is the critical path; the credential must be a Desktop app client for rclone's redirect |
| The UI said "idle", the memory guard had refused 83 times | devops |
20261015_232854_2d9fc2 |
lanes blocked 83× (34-minute waits) reported as idle; and the "savings" ledger lying — SUM(size − out_size) with rounded estimates, 632 of 688 rows size ≈ out_size, lifetime total fell 37.0 → 12.46 GB while the row count grew 591 → 670 |
| Your NAS didn't hang — 121 GiB came back while you weren't looking | devops |
20261014_203035_c6b198 |
5-minute free-space samples: two snapshot rotations +69.0 / +46.8 GiB, recycle bin held only 5 GiB, then −47 GiB of new writes; regrowth + snapshot retention ≠ a stuck job |
| The scan failed because an HTML error page was parsed as JSON | devops |
20261015_200429_37ec35 |
SyntaxError: Unexpected token '<'; the real cause was a CJK path printed to a GBK console → UnicodeEncodeError killed 3 jobs at their own first log line |
| Adding a domain to someone else's mail server | notes |
20261014_092537_4759b3 |
PTR is per IP, not per domain; forward-confirmed rDNS uses postfix's own HELO name; a new domain still needs MX/SPF/DKIM/DMARC + a verified SMTP2GO sender; forwarding still dies on p=reject |
The router that 401s /v1/models and serves chat completions with no key |
ai |
20261015_210913_47cca4 |
two Google-login connections (7 + 11 live models), REQUIRE_API_KEY=false with an empty key table, a dead gemini-3.1-pro alias |
| Embedding fonts so a PDF looks the same on any machine | engineering |
20261011_085128_453ab1 |
four families + weights fixed in-repo instead of trusting the server or the reader's machine ⚠ pull the final implementation detail from the session/docs first — the session moved on mid-topic |
| Two ways a JS file stops working silently | engineering |
hevc-transcode docs/OPERATIONS.md §10.33 |
a parameter named t shadowing the i18n t() function, and a let LANG declared after its first use (TDZ) — both abort the script while the page keeps rendering the old text, so it looks like nothing happened; the fix is asserting on effects (typeof t === 'function', key count > 0, ?lang=en really swaps), never on intent. No date or version pins — also a candidate for an archive gap. |
| The packaged app 404'd its own CSS | devops |
hevc-transcode docs/OPERATIONS.md §10.31 |
package-data listed only web/*.html, so the installed copy shipped without app.css / app.js / the i18n packs and the page 404'd its own assets (only a local checkout worked); plus the deploy-order trap — a running process reads the installed copy, so the order must be "install the Python half → restart at an item boundary → swap the page". |
Housekeeping (not posts): E1 CF Web Analytics + GSC sitemap, E3 syndicate 2–3 flagships, A1 author photo, E8 og-default.png face + the one banner-less post |
— | — | these still outrank writing anything new, per the 2026-09-29 audit |
Phase C — Discovery & structure — verbatim
⚠ The [ ] marks on C2 (tag pages) and C4 (/zh/posts/) here are stale — both shipped in
batch B2/B3. The corrected status lives in docs/project-state.md.
Phase C — Discovery & structure (Tier 2)
Step C1 — Per-post custom OG images. ✅ Done (verified live 2026-09-29)
Every published post serves its own /og/<slug>.png (1200×630) generated by scripts/og-gen/generate.mjs, with the
category chip derived from frontmatter. Two leftovers only: the shared og-default.png still backs the homepage and
category pages (no face — folded into A1 / E8), and one post's banner 404s (E8).
Step C2 — Tag pages (/tags/[tag]/ archive pages for fine-grained discovery + internal linking).
- Add tag archive routes (tags currently render as labels only).
- Done when: clicking a tag on any post opens a working
/tags/<tag>/page.
Step C3 — Categories page shows all 7 categories (not just those with posts), with "0 posts / coming soon" for empty ones — signals intended coverage. ✅ Done 2026-09-13
/categories/lists all 7 categories with name, description, per-category terminal-style SVG illustration (CategoryArt/Grid components) and a post count; empty ones show "0 posts · coming soon" as a non-link.- Done when: ✅ all 7 render with a placeholder for empty ones + descriptions + illustrations.
Step C4 — Dedicated /zh/posts/ and /zh/categories/ archive pages. 🔵 In progress
/zh/categories/index +/zh/categories/[category]/detail pages live (zh nav "分类" points there; PostList is locale-aware with zh-CN dates)./zh/posts/archive still missing — zh nav "文章" falls back to/zh/landing. ⚠ Live-measured 2026-09-29: the ZH nav's 「关于」 is also wrong (points at/zh/, not/zh/about/), and/zh/about/has zero inbound links — see Step E4, which folds into this one.- Done when: zh nav links to real
/zh/posts/+/zh/categories/archives.
Phase D — Polish / later — verbatim (DONE)
Phase D — Polish / later (Tier 3)
Step D1 — Search. ✅ Done (verified live 2026-09-29)
Pagefind is shipped: /pagefind/pagefind.js → 200, a search toggle in the nav on both locales, and the shards are
correctly Disallowed in robots.txt. (Originally deferred until >20 posts.)
Step D2 — Google Search Console submission. → merged into Step E1 (2026-09-29). A google-site-verification TXT record already exists on hoelee.com, so the GSC domain property exists; what is unconfirmed is whether the blog's sitemap was ever submitted.
Step D3 — Newsletter / email capture — only after real traffic exists (agree: do NOT add yet).