docs: slim project-state.md 128KB -> 45KB by archiving completed phases; fix stale checkboxes
Deploy / build (push) Successful in 19s
Deploy / build (push) Successful in 19s
This commit is contained in:
1 parent
5ec321a2f8
commit
a96cfb73db
2 files changed
+900
-806
No files matched your search
@@ -0,0 +1,846 @@
|
||||
# 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 `3f63150` live 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-sans` and `--font-mono` upgraded to native multi-platform stack with PingFang SC / Microsoft YaHei CJK fallbacks.
|
||||
- A2 branding alignment: `<title>`, meta description, JSON-LD, and homepage hero unified to `Mr Hoelee — Full-Stack Developer & DevOps Engineer`.
|
||||
- All 251 pages cleanly verified, Gitea and GitHub remotes strictly 0/0 aligned.
|
||||
- ✅ **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.
|
||||
- **Self-fitting headline engine implemented**: dynamically measures DOM lines in headless Chrome, stepping down font size (50px → 26px) until headline fits `<= 3 lines` with no orphan/widow lines; CJK titles intelligently split at punctuation; numbers glued with NBSP.
|
||||
- Front-end homepage enhanced: `Featured` section 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 under `prefers-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.webp` thumbs `naturalWidth 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 renderer `scripts/og-gen/render_from_art.mjs` + `art-meta.json` (committed 2026-10-18; the readable version of the catalog ships outside the repo at `Desktop\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 into `Desktop\blog-og-image-redesign-brief\delivery-for-consultant-round4\` only. The repo/live site still serves the **v1** set — 72 `public/banners/*.png` + 72 `public/og/*.png` generated 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 new `ogTitle` frontmatter 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 an `ogImage:` 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 in `Pictures/` 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-LD `Person.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.txt`** generated from the collection at build time (48 KB, 142 post entries + section links) so LLM crawlers and answer engines can cite accurately; `robots.txt` now points at it.
|
||||
- Still open: the **main site** (`www.hoelee.com`) counter says `12+ Experts` / `100+ Years Of Experience` while the blog says he runs the stack solo — needs the owner's real numbers; and cross-posting to dev.to/Hashnode needs his accounts.
|
||||
- ✅ **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.js` is an **ES module** (`export { search, debouncedSearch, … }`) that defines **no `window.Pagefind`**, but `Search.astro` loaded it with a classic `<script src>` and read `window.Pagefind` → `undefined` → every query in the overlay was answered **"Search is unavailable."** Live-verified before the fix. Loader is now a dynamic `import()`; 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 in `dist/`. 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, chip `devops` → 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/`.
|
||||
- ✅ **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 now `301`s it to the new Chinese archive.
|
||||
- **`/zh/posts/` did not exist** (404) → new `src/pages/zh/posts/index.astro` (build went 163 → 165 pages).
|
||||
- **ZH nav pointed "关于" at `/zh/`** → now `/zh/about/`, and "文章" → `/zh/posts/`.
|
||||
- **`og:locale` was `en` on every Chinese page** → now `zh_CN`, with `og:locale:alternate` on both languages (EN pages advertise `zh_CN` too).
|
||||
- `hreflang` value `zh` → **`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 `Person`** gained `worksFor` (Hoelee Enterprise + `www.hoelee.com`); `image` still 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/*` to `immutable` 1-year and HTML to `must-revalidate`. Details in `ops-runbook.md`.
|
||||
- Still open for the owner (CF token limits, see below): **zone HSTS is `max-age=0`** and **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 for `og: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}.webp` with `srcset`/`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 in `ops-runbook.md`.
|
||||
|
||||
- ✅ **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.astro` said **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**: `minutesOf` split 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. New `src/lib/reading-time.ts` counts 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-world` contradicted 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 servers` was 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>` and `title`/JSON-LD were left alone (A2 identity alignment).
|
||||
- **Featured bento cards got the `srcset`/`sizes` they were missing** (`-400`/`-800` webp) — 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 `-800` thumbs 800×420, 72/72 `-400` thumbs 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:locale` and 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 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).**
|
||||
|
||||
1. **The prompt set was not usable as shipped.** `prompts_catalog.json` carried **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 with `C:/AI/ideogram4/build_captions.py` (asserts the slug set matches `src/content/posts/*.md` and that no two metaphors are identical).
|
||||
2. **GPU run.** 72 frames at 1360×768 / 20 steps / `--preset default`, ~220 s each, **4.5 h** total, resumable via `C:/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).
|
||||
3. **Caption defect found and fixed (3 of 3 pilot frames).** With the subject described twice — `high_level_description` repeating 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 in `elements[].desc` cleared it.
|
||||
4. **The "no text anywhere" rule fails on the accepted samples too.** `sample_oom_169_00001_.png` OCRs as `Kenel 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.
|
||||
5. **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.
|
||||
6. **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 shared `style_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` (`--baseline` prints the accepted samples' numbers), `qc_frames.py` (per-frame stats + unlabelled contact sheets).
|
||||
7. **Delivery shipped 2026-10-19** (`327d516` → Gitea `main`, deploy green): 72 EN + 72 ZH cards, 144 banner files, 144 thumbs rendered into `public/`; 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 200 `image/jpeg`, ZH card 200 (did not exist before), banner WebP 200, thumbs 200 from both listings, v1 `.png` 404, 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 refreshed `prompts_catalog.json` with 72 distinct entries, via `scripts/og-gen/sync_prompts_catalog.py --check`.
|
||||
8. **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:
|
||||
|
||||
1. **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.
|
||||
2. **The artwork carried HUD readouts after all** — the consultant read `60%`, `100%` and
|
||||
`00%` off `your-disk-full-alert-is-lying`, plus a `10` and a ruler line on
|
||||
`that-dying-ssd-…`. Root cause: the shared style block inherited from the accepted
|
||||
samples (`technical blueprint vector lines`, `status indicator lights`) and the
|
||||
background's `blueprint 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 flagged `00%`@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) and
|
||||
`build_captions.py` asserts a `FORBIDDEN` word list so blueprint/gauge/label language
|
||||
cannot return. Three pilot frames re-rendered at the fixed prompt: **0 OCR hits each**
|
||||
(was `SSD` 66 px, `IDENESLOES` 92 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; `|L−bg| > 16 or |∇| > 10`; 3×3 opening kills the grid lines; 31 px closing bridges the object's own dark gaps; union of components within 10 % of the largest. Stable across thresholds 8–20, 0.0 % false-positive on the left margin |
|
||||
|
||||
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 28` printed "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) and
|
||||
`my-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>.png` returns 404; `og:image` and 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:…`);
|
||||
`zh` post 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 `bbox` was 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:image` correct 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):
|
||||
1. English headline rag → **default: (C) shorten the card headline only**, via `ogTitle_en` in `art-meta.json` (article title untouched).
|
||||
2. CJK headline length cap → **default: ≤30 characters incl. punctuation** (EN ≤65).
|
||||
3. 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`), and
|
||||
- **`src/components/PostList.astro` line ~42 must change** from `.replace(/\.png$/, …)` to `/\.(png|jpe?g)$/`, or every listing thumb derives `…-800.webp` from `slug.jpg` and 404s.
|
||||
|
||||
**Order of work**
|
||||
1. Generate the 70 base frames (GPU), named `<slug>.png` under `C:/AI/ideogram4/out/`.
|
||||
2. Write 70 `evidence` strings into `art-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.
|
||||
3. Dry-run the whole set to a scratch dir:
|
||||
`node scripts/og-gen/render_from_art.mjs --all --limit 4 --out "$TMPDIR/og-check"` — expect `FIT` on all four cards.
|
||||
4. Render into `public/` (`--all`), then `uv run --with pillow python scripts/optimize-images.py` for the v1-derived WebPs **only if any v1 file survives**; the renderer already emits its own thumbs.
|
||||
5. Do the `.jpg` migration (frontmatter ×144 + `PostList.astro`) in the **same commit** as the images, or the site points at missing files.
|
||||
6. `npm run build`, then `npm run preview`-equivalent check + the four URL assertions, commit, push `origin` **and** `github`.
|
||||
|
||||
**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}.html` in an iframe (serve the repo root over http — `file://` iframes are opaque origins) and assert `title` line count ≤3, `documentElement.scrollHeight − 630 == 0`, evidence bar `scrollWidth − clientWidth == 0`.
|
||||
- Tags/banner: `python <skill>/scripts/measure_og_banner.py <slug> <repo>` → `rows==1`, `overflow==0`, banner `delta<=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.webp` returns 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 passing `00%`@0.84 as noise is exactly what let `60 %`/`100 %` ship.
|
||||
- ✅ `art-meta.json` now carries all 72 entries; `scripts/og-gen/build_art_meta.py` regenerates them and re-checks coverage against `src/content/posts/*.md` before 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.
|
||||
- [x] Headshot supplied; two derivatives built (`lee-teong-hoe-640`: 42 KB JPG / 26 KB WebP, `-128`: 3 KB / 2 KB).
|
||||
- [x] Photo renders in the About hero, the author card on every post, the home hero (EN + ZH) and JSON-LD `Person.image`.
|
||||
- [x] Faceless `og-default` replaced 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
|
||||
- [x] Pick one line: "Full-Stack Developer & DevOps Engineer" aligned across `SITE.title`, `SITE.description`, homepage hero `<h1>`, and JSON-LD `jobTitle`.
|
||||
- **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).
|
||||
- [x] Homepage `Featured` section: 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-here` or `/favorites` page — **deliberately deferred**: the homepage strip is the immediate win, and the `posts` listing plus `related` rows 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-grid` present).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
- [x] Write `src/content/posts/self-hosting-mem0.md` (category `case-studies`).
|
||||
- [x] Write Chinese twin `src/content/posts/zh/self-hosting-mem0.md` (same filename → auto language-switch).
|
||||
- [x] 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.
|
||||
- [x] 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).**
|
||||
- [x] "Replacing RDPGuard With IPBan: The Traps Nobody Documents" (EN + ZH, `devops`, 2026-09-19) — the uninstaller that unbans 12 attackers, `--install-service` doesn't exist in v4.1.0, `ExpireTime` vs `BanTime`. Both images custom.
|
||||
- [x] "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 from `major >= 8` being an invalid MySQL-vs-MariaDB test. Both images custom.
|
||||
- [x] "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-edited `Preferences`, `RestoreOnStartup` policy all verified failing); the 60-second snapshot keeper that fixed it, plus the stale `Singleton*` and `custom-cont-init.d` permission traps. Both images custom.
|
||||
- [x] "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; adds `skip_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, commit `e7e4ee5`.
|
||||
- [ ] "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:**
|
||||
1. Flip `draft: true` → `draft: false` in **both** `src/content/posts/<slug>.md` and `src/content/posts/zh/<slug>.md`.
|
||||
2. Set the real `pubDate` (currently `2026-09-20`, the draft date) in both files.
|
||||
3. Generate its images: `node scripts/og-gen/generate.mjs <slug>` and `node scripts/banner-gen/generate.mjs <slug>` (add a `TERMINALS[slug]` / `BANNERS[slug]` entry first for the custom panel).
|
||||
4. `npm run build`, commit, `git push origin main`.
|
||||
5. Verify both URLs return 200 and the language switcher links them.
|
||||
|
||||
- **Why these two:** `engineering` had only 1 post and `tutorials` only 1 — the blog was ~all `devops`/`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:** `engineering` was 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-buy` and
|
||||
`how-i-vetted-20-vps-providers-with-parallel-subagents` — those cover vendors you *pay*, this covers code you
|
||||
*depend on*. `devops` is 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 task `success`.
|
||||
- ⚠ **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; `updatedDate` keeps `lastmod` / `dateModified`
|
||||
honest (post-guideline backdating rule). Neither post carries a date-, month- or version-pinned sentence —
|
||||
verified with `grep -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 `/cv` 301→404 chain) remain unstarted; `why-your-cdp-clicks-silently-fail` counts 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 the `TERMINALS` / `BANNERS` entries for
|
||||
these two slugs were applied through throwaway `generate.local.mjs` copies (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:
|
||||
|
||||
````js
|
||||
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 task `success`,
|
||||
and `git status` clean 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 per `content-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.md` backdating rule).
|
||||
- **Why:** the blog had **zero** Prometheus/Grafana/monitoring posts while `content-guide.md` §2 lists monitoring under
|
||||
`devops`, "my most differentiated material" — and `monitoring`/`grafana no data`/`smartctl exit code` are 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 two `notes` posts link up to
|
||||
the flagship with a relative link.
|
||||
- **Why:** the search results for `synology spreadsheet api` / `spreadsheet-api docker` are 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 tag `nocodb/nocodb:2026.09.0` **and** a returned row
|
||||
timestamp `2026-09-27 01:54:16+00:00` → floor 2026-09-27.
|
||||
- `nocodb-sso-is-a-licensed-feature` — same `2026.09.0` image 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 names `Chrome 154` (≈ Oct 2026 on Chrome's
|
||||
cadence) and the post links back to `scraping-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 `updatedDate` holding the true date, so `lastmod` stays 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-page` was
|
||||
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 address `0xc2022b56…`, not dates. All cited figures were traced to source:
|
||||
`868596` / `4102 bytes` / `993568` gas / `0.000535185588997216 ETH` / block `6522146` / mint `181874`
|
||||
(deploy + mint logs), `335011` gas (`.gas-snapshot`), `2.5pt→7.5pt` + `20,225,818` bytes + `30–490pt`
|
||||
(`docs/pdf-pipeline.md`; the `27 pages` / `12,789 pages` figures live in `app/Libraries/ReportPdf.php`
|
||||
lines 21 and 196, **not** in the doc), `@Version` + `POST_VERSION_CONFLICT` in the Spring demo.
|
||||
- ⚠ **The `TERMINALS` / `BANNERS` entries 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`/`gapBelow` within 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_regex` pattern, written up as the *correct* counterpart to `authentik-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_single` leaves `internal_host` empty, so the SSO chain is textbook-correct
|
||||
while **every app path 404s** — the tell is `x-powered-by: authentik` on the 404; (2) the embedded outpost
|
||||
needs **1–2 minutes** to pick up a provider change and answers `302 → /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 `Authorization`
|
||||
header, 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 (including `POST /api/send` returning 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 `TERMINALS`
|
||||
entry, 8-row `BANNERS` entry 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/reader`
|
||||
is **alive** — 11,036★ / 5,471 forks / AGPL-3.0 / default branch `main`, 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/data` JSON → **`/data` SQLite**,
|
||||
env config → **admin-UI settings in the DB**, image moved to **`cnb.cool/hectorqin/reader:main`** (branch/commit tags,
|
||||
deliberately no `:latest`), **0 releases / 0 tags**; Docker Hub `hectorqin/reader` still **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`). **The `hectorqin-reader` skill 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:alpine` container owning the public port, the request-time
|
||||
`resolver 127.0.0.11` (so the front page survives the app being down), `client_max_body_size` preserved for uploads,
|
||||
why a proxy **body rewrite** cannot touch a client-rendered SPA, the "reload → back to the front door" injection with
|
||||
its `sessionStorage.gatePass` consume-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-06`
|
||||
so `lastmod` stays honest — and it is constrained to **link backwards only** (it cross-links
|
||||
`adding-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 --check` OK) via
|
||||
`scripts/append_generator_entries.py`: 4-line `TERMINALS` + 8-row `BANNERS` per slug.
|
||||
- **Verified after deploy:** build clean (139 pages, Pagefind 2 languages, no duplicate-id warning); `measure_og_banner.py`
|
||||
**PASS** on both (og `rows=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 entries `lastmod 2026-10-06` with 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 the `mailto:` to `email-protection`
|
||||
— expected). Commit `378d55c`, pushed Gitea + GitHub, both remotes **0/0**; follow-up `ef3f520` sharpened the wipe
|
||||
evidence after checking the actual repo (not just the API): `master` is a **single commit dated 2026-06-22** whose
|
||||
README is the word `deleted`, sharing **no common ancestor** with `main` — the API's `created_at` still 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 of
|
||||
`github.com/hectorqin/reader` (`main` @ `5c59cae`, plus the 1-commit `master`), pushed with `git push --mirror`.
|
||||
⚠ Gitea's hook **rejects `refs/pull/*`** ("hook declined") — that is expected and harmless; branches and the whole
|
||||
object history land. A future session should re-push from a fresh `git clone --mirror` to refresh it, or switch the
|
||||
repo to a Gitea pull mirror (the existing `hoelee/reader` uses 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** in
|
||||
`3e354cc`: both `.md` files `git rm`'d and its `TERMINALS`/`BANNERS` entries removed from both generators (pure
|
||||
deletions — 0 added, 17 + 5 removed, `node --check` OK, 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). Keep `pubDate` 2026-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.py` applied 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 build` clean (155 pages, no duplicate-id warning); all 8 `dist/` 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`/`BANNERS` entry sets** — the three for this batch's slugs
|
||||
(two held + one parked) rode into `2295c0f` together 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 `.md` files plus its two PNGs.
|
||||
- **To publish one:** confirm the real `pubDate` (`date`, and the newest `<time>` on the live `/posts/`), then
|
||||
`git add` the four explicit paths (post EN/ZH + og + banner) and push `origin 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 `t` shadowing the i18n `t()` function, and a `let LANG` declared *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) and `chainlink-vrf-v2-lottery-contract` (2024-12-10) — verified in the built feed: the post lands at
|
||||
position 64 of 67, monotonic, with `lastmod` = 2026-10-16 from `updatedDate`.
|
||||
- ⚠ **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.py` on EN only), og + banner generated and measured
|
||||
(og `rows=1 overflow=0`; banner 8 rows `delta=2 overflow=0`), build clean at **155 pages**, both `dist/` 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 reported `itemNotFound` *after* finishing. (2) The DSM Storage Manager
|
||||
card reads `running` 24×7 by design (`run_all_time`, `schedule_table` 168×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 hit `554 5.7.1` (no PTR, 4 RBLs clean) and the relay hit `550-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: forward `mail.hoelee.com → 41.216.187.102`
|
||||
resolves, the reverse lookup for that IP still returns nothing. (4) Four families / eight static faces embedded:
|
||||
static only because the PDF engine's parser reads `glyf`/`loca` and ignores `fvar` (variable fonts resolve to
|
||||
Thin 100 / ExtraLight 200), `font-weight: 600` rewritten to `bold` for 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 every `lastmod` is the real day.
|
||||
- **State:** EN + ZH written (`rewrap_post.py` → `wrap_lists.py` on EN only, ZH one paragraph per line); og + banner
|
||||
generated and **measured** for all four (`rows=1 overflow=0`, banner 8 rows `delta=2 overflow=0`); build clean at
|
||||
**163 pages**; 8 `dist/` 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, correct `content_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** with `lastmod` 2026-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 of `space-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-existing `og:locale = en` on 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):**
|
||||
1. **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 (EN `you/your` 12 → 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.
|
||||
2. **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 万行`).
|
||||
- **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/gen` measured afterwards: 2 title lines, `overflowY 0`, `measure_og_banner.py` PASS · 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_now` only killing when `self.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
|
||||
- [x] `/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
|
||||
- [x] `/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 `Disallow`ed 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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+54
-806
@@ -5,6 +5,11 @@ Living list of what's done and what's next for blog.hoelee.com. Work through the
|
||||
**Status key:** ✅ done · 🔵 in progress · ⬜ not started
|
||||
|
||||
> **How to resume the project:** start at the top-most ⬜ item in the **Execution Plan** (§2) below and work downward. Each step is self-contained, has a "done when" criterion, and references the governing doc. Don't jump ahead — earlier steps unlock later ones.
|
||||
>
|
||||
> **Slimmed 2026-10-19:** the completed-phase logs (Phases A–D and F, plus the §1 batch detail) moved
|
||||
> **verbatim** to `docs/handoffs/project-state-history.md`. What stays here is the live queue plus one
|
||||
> summary line per finished phase. As of 2026-10-19 the only open ⬜ items in the queue are **owner
|
||||
> actions** (Cloudflare Web Analytics, Search Console) — engineering has no unstarted step left.
|
||||
|
||||
---
|
||||
|
||||
@@ -28,238 +33,24 @@ Living list of what's done and what's next for blog.hoelee.com. Work through the
|
||||
- ✅ Knowledge guides in `docs/` + README index + `hoelee-blog` skill
|
||||
- ✅ Both repos public (Gitea + GitHub) with title/description/homepage/topics + `v1.0.0` release
|
||||
- ✅ **Live re-audit of the Sept research recorded (§2 Phase E + §4), 2026-09-29** — 53 posts × 2 languages, 127 sitemap URLs, 133/133 internal links 200
|
||||
- ✅ **B5 Baseline & Hygiene Batch, 2026-10-18 (commit `3f63150` live 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-sans` and `--font-mono` upgraded to native multi-platform stack with PingFang SC / Microsoft YaHei CJK fallbacks.
|
||||
- A2 branding alignment: `<title>`, meta description, JSON-LD, and homepage hero unified to `Mr Hoelee — Full-Stack Developer & DevOps Engineer`.
|
||||
- All 251 pages cleanly verified, Gitea and GitHub remotes strictly 0/0 aligned.
|
||||
- ✅ **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.
|
||||
- **Self-fitting headline engine implemented**: dynamically measures DOM lines in headless Chrome, stepping down font size (50px → 26px) until headline fits `<= 3 lines` with no orphan/widow lines; CJK titles intelligently split at punctuation; numbers glued with NBSP.
|
||||
- Front-end homepage enhanced: `Featured` section 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 under `prefers-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.webp` thumbs `naturalWidth 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 renderer `scripts/og-gen/render_from_art.mjs` + `art-meta.json` (committed 2026-10-18; the readable version of the catalog ships outside the repo at `Desktop\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 into `Desktop\blog-og-image-redesign-brief\delivery-for-consultant-round4\` only. The repo/live site still serves the **v1** set — 72 `public/banners/*.png` + 72 `public/og/*.png` generated 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 new `ogTitle` frontmatter 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 an `ogImage:` 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 in `Pictures/` 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-LD `Person.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.txt`** generated from the collection at build time (48 KB, 142 post entries + section links) so LLM crawlers and answer engines can cite accurately; `robots.txt` now points at it.
|
||||
- Still open: the **main site** (`www.hoelee.com`) counter says `12+ Experts` / `100+ Years Of Experience` while the blog says he runs the stack solo — needs the owner's real numbers; and cross-posting to dev.to/Hashnode needs his accounts.
|
||||
- ✅ **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.js` is an **ES module** (`export { search, debouncedSearch, … }`) that defines **no `window.Pagefind`**, but `Search.astro` loaded it with a classic `<script src>` and read `window.Pagefind` → `undefined` → every query in the overlay was answered **"Search is unavailable."** Live-verified before the fix. Loader is now a dynamic `import()`; 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 in `dist/`. 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, chip `devops` → 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/`.
|
||||
- ✅ **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 now `301`s it to the new Chinese archive.
|
||||
- **`/zh/posts/` did not exist** (404) → new `src/pages/zh/posts/index.astro` (build went 163 → 165 pages).
|
||||
- **ZH nav pointed "关于" at `/zh/`** → now `/zh/about/`, and "文章" → `/zh/posts/`.
|
||||
- **`og:locale` was `en` on every Chinese page** → now `zh_CN`, with `og:locale:alternate` on both languages (EN pages advertise `zh_CN` too).
|
||||
- `hreflang` value `zh` → **`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 `Person`** gained `worksFor` (Hoelee Enterprise + `www.hoelee.com`); `image` still 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/*` to `immutable` 1-year and HTML to `must-revalidate`. Details in `ops-runbook.md`.
|
||||
- Still open for the owner (CF token limits, see below): **zone HSTS is `max-age=0`** and **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 for `og: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}.webp` with `srcset`/`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 in `ops-runbook.md`.
|
||||
**Batch index** — full logs for every batch (measurements, commits, traps) are in
|
||||
`docs/handoffs/project-state-history.md`.
|
||||
|
||||
- ✅ **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.astro` said **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**: `minutesOf` split 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. New `src/lib/reading-time.ts` counts 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-world` contradicted 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 servers` was 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>` and `title`/JSON-LD were left alone (A2 identity alignment).
|
||||
- **Featured bento cards got the `srcset`/`sizes` they were missing** (`-400`/`-800` webp) — 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 `-800` thumbs 800×420, 72/72 `-400` thumbs 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:locale` and 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.
|
||||
- ✅ **B1 image batch** (2026-10-16) — article banner → `<picture>` WebP (70 banners 6.02 MB → 2.52 MB); related cards went text-only; list thumbs got `srcset` 400/800; desktop banner height capped.
|
||||
- ✅ **B2 broken windows** (2026-10-16) — `/posts/zh/` 403 → 301; new `/zh/posts/`; ZH nav fixed; `og:locale` `en` → `zh_CN`; `hreflang`/`<html lang>` → `zh-Hans`; a real 404 page; nginx config → host bind mount (DSM stack 247).
|
||||
- ✅ **B3 structure** (2026-10-16) — **site search had never worked** (the Pagefind ES module was read as `window.Pagefind`); archive 18,220 → 3,788 px with a client-side filter; tag pages (235 tags measured, 156 used once); EN/ZH hero + nav fixes.
|
||||
- ✅ **B4 identity & distribution** (2026-10-16) — real author photo in all five surfaces + JSON-LD `Person.image`; About 1,024 → 7,357 chars; `/llms.txt`.
|
||||
- ✅ **B5 baseline & hygiene** (2026-10-18, `3f63150`) — `/og-default.jpg` 211 KB → 45 KB carrying the headshot; banner-404 sweep to 72/72 clean; CJK system font stack; A2 branding alignment.
|
||||
- ✅ **B6 visual redesign + Ideogram 4 pipeline** (2026-10-18) — the consultant's hybrid "one art, four targets" template; bento theme tokens (a hardcoded dark scrim measured 1.96:1 text contrast in the light theme → 6.10). Phase F is its rollout.
|
||||
- ✅ **B7 review-response** (2026-10-19, `5941e85` + `5ec321a`) — About counter derived from the collection (71 → **72**); reading time is CJK-aware (ZH **1 min → 5 分钟**); ZH wording fixes; `hello-world`'s stale language policy and container count; hero leads with the capability; featured cards got the `srcset` they lacked; **48/48** mobile-overflow combinations at 0 px.
|
||||
|
||||
---
|
||||
|
||||
## 2. Execution Plan (work top → bottom, one step at a time)
|
||||
|
||||
> This plan comes from a full research pass (Sept 2026) comparing blog.hoelee.com against reference developer blogs (Simon Willison, Josh Comeau, Dan Abramov/overreacted, Julia Evans) + industry surveys. Priority is fixed: **identity & content → discovery → polish.** Don't reorder unless the user says so.
|
||||
>
|
||||
> **⚠ Re-checked against the live site 2026-09-29 (§4).** Parts of Phase A/C/D are stale — **C1, D1 and B2b are DONE.** The measured bottleneck is no longer content; it is **distribution / visibility**. Per user decision on 2026-09-29, **Phase E is inserted at the TOP of the queue — start there**, then work the still-open A/C items.
|
||||
>
|
||||
> **▶ NEXT TASK (user decision 2026-10-18): Step B6b — the 72-post artwork rollout.** It sits below in **Phase F** and outranks Phase E; it needs a GPU window, so a fresh session starts by reading Phase F, not Phase E.
|
||||
|
||||
### 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).**
|
||||
|
||||
1. **The prompt set was not usable as shipped.** `prompts_catalog.json` carried **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 with `C:/AI/ideogram4/build_captions.py` (asserts the slug set matches `src/content/posts/*.md` and that no two metaphors are identical).
|
||||
2. **GPU run.** 72 frames at 1360×768 / 20 steps / `--preset default`, ~220 s each, **4.5 h** total, resumable via `C:/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).
|
||||
3. **Caption defect found and fixed (3 of 3 pilot frames).** With the subject described twice — `high_level_description` repeating 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 in `elements[].desc` cleared it.
|
||||
4. **The "no text anywhere" rule fails on the accepted samples too.** `sample_oom_169_00001_.png` OCRs as `Kenel 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.
|
||||
5. **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.
|
||||
6. **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 shared `style_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` (`--baseline` prints the accepted samples' numbers), `qc_frames.py` (per-frame stats + unlabelled contact sheets).
|
||||
7. **Delivery shipped 2026-10-19** (`327d516` → Gitea `main`, deploy green): 72 EN + 72 ZH cards, 144 banner files, 144 thumbs rendered into `public/`; 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 200 `image/jpeg`, ZH card 200 (did not exist before), banner WebP 200, thumbs 200 from both listings, v1 `.png` 404, 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 refreshed `prompts_catalog.json` with 72 distinct entries, via `scripts/og-gen/sync_prompts_catalog.py --check`.
|
||||
8. **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:
|
||||
|
||||
1. **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.
|
||||
2. **The artwork carried HUD readouts after all** — the consultant read `60%`, `100%` and
|
||||
`00%` off `your-disk-full-alert-is-lying`, plus a `10` and a ruler line on
|
||||
`that-dying-ssd-…`. Root cause: the shared style block inherited from the accepted
|
||||
samples (`technical blueprint vector lines`, `status indicator lights`) and the
|
||||
background's `blueprint 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 flagged `00%`@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) and
|
||||
`build_captions.py` asserts a `FORBIDDEN` word list so blueprint/gauge/label language
|
||||
cannot return. Three pilot frames re-rendered at the fixed prompt: **0 OCR hits each**
|
||||
(was `SSD` 66 px, `IDENESLOES` 92 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; `|L−bg| > 16 or |∇| > 10`; 3×3 opening kills the grid lines; 31 px closing bridges the object's own dark gaps; union of components within 10 % of the largest. Stable across thresholds 8–20, 0.0 % false-positive on the left margin |
|
||||
|
||||
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 28` printed "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) and
|
||||
`my-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>.png` returns 404; `og:image` and 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:…`);
|
||||
`zh` post 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 `bbox` was 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:image` correct 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):
|
||||
1. English headline rag → **default: (C) shorten the card headline only**, via `ogTitle_en` in `art-meta.json` (article title untouched).
|
||||
2. CJK headline length cap → **default: ≤30 characters incl. punctuation** (EN ≤65).
|
||||
3. 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`), and
|
||||
- **`src/components/PostList.astro` line ~42 must change** from `.replace(/\.png$/, …)` to `/\.(png|jpe?g)$/`, or every listing thumb derives `…-800.webp` from `slug.jpg` and 404s.
|
||||
|
||||
**Order of work**
|
||||
1. Generate the 70 base frames (GPU), named `<slug>.png` under `C:/AI/ideogram4/out/`.
|
||||
2. Write 70 `evidence` strings into `art-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.
|
||||
3. Dry-run the whole set to a scratch dir:
|
||||
`node scripts/og-gen/render_from_art.mjs --all --limit 4 --out "$TMPDIR/og-check"` — expect `FIT` on all four cards.
|
||||
4. Render into `public/` (`--all`), then `uv run --with pillow python scripts/optimize-images.py` for the v1-derived WebPs **only if any v1 file survives**; the renderer already emits its own thumbs.
|
||||
5. Do the `.jpg` migration (frontmatter ×144 + `PostList.astro`) in the **same commit** as the images, or the site points at missing files.
|
||||
6. `npm run build`, then `npm run preview`-equivalent check + the four URL assertions, commit, push `origin` **and** `github`.
|
||||
|
||||
**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}.html` in an iframe (serve the repo root over http — `file://` iframes are opaque origins) and assert `title` line count ≤3, `documentElement.scrollHeight − 630 == 0`, evidence bar `scrollWidth − clientWidth == 0`.
|
||||
- Tags/banner: `python <skill>/scripts/measure_og_banner.py <slug> <repo>` → `rows==1`, `overflow==0`, banner `delta<=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.webp` returns 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 passing `00%`@0.84 as noise is exactly what let `60 %`/`100 %` ship.
|
||||
- ✅ `art-meta.json` now carries all 72 entries; `scripts/og-gen/build_art_meta.py` regenerates them and re-checks coverage against `src/content/posts/*.md` before the renderer runs.
|
||||
- Extra thumbs: the renderer emits 400 px and 800 px; nothing else consumes them.
|
||||
|
||||
**The live queue is Phase E.** Phases A–D and F are done; their logs moved to
|
||||
`docs/handoffs/project-state-history.md` on 2026-10-19. What stays below is one summary line per completed
|
||||
phase so "is this done?" is still answerable here without opening the archive.
|
||||
|
||||
### Phase E — Distribution & visibility (added 2026-09-29 from the live re-audit — DO THESE FIRST)
|
||||
|
||||
@@ -281,8 +72,8 @@ unanswerable, and the job-hunt thesis can't be verified. Decisions (2026-09-29):
|
||||
| **Umami** (self-hosted on DSM — MIT, ~1 M events/month free cloud tier) | 🔵 Optional later, only if per-event funnels or full data ownership are wanted. |
|
||||
| GA4 / Plausible CE | ❌ Skip — GA4 brings weight + a consent banner; Plausible CE needs Postgres **and** ClickHouse. |
|
||||
|
||||
- [ ] Enable CF Web Analytics for the zone and add the beacon to the base layout's `<head>` (belt-and-braces vs CF auto-injection).
|
||||
- [ ] Confirm the GSC domain property covers the blog, submit `sitemap-index.xml`, and re-check "Discovered / Indexed" counts ~a week later.
|
||||
- [ ] Enable CF Web Analytics for the zone in the Cloudflare dashboard. ⚠ There is **no repo change to make** — Cloudflare injects its own `cloudflareinsights` beacon once the site is added, so this is an owner action, not a layout edit. Re-check with `curl -s https://blog.hoelee.com/ | grep -c cloudflareinsights` → **0 = still not enabled** (measured 2026-10-19).
|
||||
- [ ] Confirm the GSC domain property covers the blog, submit `sitemap-index.xml` (it serves **200** — measured 2026-10-19), and re-check "Discovered / Indexed" counts ~a week later.
|
||||
- **Done when:** a pageview from a second device shows up in CF Web Analytics, and GSC lists the sitemap as Submitted with the post URLs Discovered.
|
||||
- **Governing doc:** `seo-reference.md`.
|
||||
|
||||
@@ -442,585 +233,32 @@ less origin traffic through the tunnel.
|
||||
- Crisp ligatures and distinct 0/O/1/l/I glyphs for technical code snippets.
|
||||
- Zero webfont transfer overhead, 0 ms FOUT/FOIT layout shifts, fully aligned with the blog's ultra-fast static philosophy.
|
||||
|
||||
### 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.
|
||||
- [x] Headshot supplied; two derivatives built (`lee-teong-hoe-640`: 42 KB JPG / 26 KB WebP, `-128`: 3 KB / 2 KB).
|
||||
- [x] Photo renders in the About hero, the author card on every post, the home hero (EN + ZH) and JSON-LD `Person.image`.
|
||||
- [x] Faceless `og-default` replaced 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
|
||||
- [x] Pick one line: "Full-Stack Developer & DevOps Engineer" aligned across `SITE.title`, `SITE.description`, homepage hero `<h1>`, and JSON-LD `jobTitle`.
|
||||
- **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).
|
||||
- [x] Homepage `Featured` section: 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-here` or `/favorites` page — **deliberately deferred**: the homepage strip is the immediate win, and the `posts` listing plus `related` rows 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-grid` present).
|
||||
|
||||
### 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.
|
||||
- [x] Write `src/content/posts/self-hosting-mem0.md` (category `case-studies`).
|
||||
- [x] Write Chinese twin `src/content/posts/zh/self-hosting-mem0.md` (same filename → auto language-switch).
|
||||
- [x] 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.
|
||||
- [x] 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).**
|
||||
- [x] "Replacing RDPGuard With IPBan: The Traps Nobody Documents" (EN + ZH, `devops`, 2026-09-19) — the uninstaller that unbans 12 attackers, `--install-service` doesn't exist in v4.1.0, `ExpireTime` vs `BanTime`. Both images custom.
|
||||
- [x] "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 from `major >= 8` being an invalid MySQL-vs-MariaDB test. Both images custom.
|
||||
- [x] "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-edited `Preferences`, `RestoreOnStartup` policy all verified failing); the 60-second snapshot keeper that fixed it, plus the stale `Singleton*` and `custom-cont-init.d` permission traps. Both images custom.
|
||||
- [x] "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; adds `skip_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, commit `e7e4ee5`.
|
||||
- [ ] "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:**
|
||||
1. Flip `draft: true` → `draft: false` in **both** `src/content/posts/<slug>.md` and `src/content/posts/zh/<slug>.md`.
|
||||
2. Set the real `pubDate` (currently `2026-09-20`, the draft date) in both files.
|
||||
3. Generate its images: `node scripts/og-gen/generate.mjs <slug>` and `node scripts/banner-gen/generate.mjs <slug>` (add a `TERMINALS[slug]` / `BANNERS[slug]` entry first for the custom panel).
|
||||
4. `npm run build`, commit, `git push origin main`.
|
||||
5. Verify both URLs return 200 and the language switcher links them.
|
||||
|
||||
- **Why these two:** `engineering` had only 1 post and `tutorials` only 1 — the blog was ~all `devops`/`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:** `engineering` was 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-buy` and
|
||||
`how-i-vetted-20-vps-providers-with-parallel-subagents` — those cover vendors you *pay*, this covers code you
|
||||
*depend on*. `devops` is 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 task `success`.
|
||||
- ⚠ **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; `updatedDate` keeps `lastmod` / `dateModified`
|
||||
honest (post-guideline backdating rule). Neither post carries a date-, month- or version-pinned sentence —
|
||||
verified with `grep -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 `/cv` 301→404 chain) remain unstarted; `why-your-cdp-clicks-silently-fail` counts 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 the `TERMINALS` / `BANNERS` entries for
|
||||
these two slugs were applied through throwaway `generate.local.mjs` copies (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:
|
||||
|
||||
````js
|
||||
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 task `success`,
|
||||
and `git status` clean 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 per `content-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.md` backdating rule).
|
||||
- **Why:** the blog had **zero** Prometheus/Grafana/monitoring posts while `content-guide.md` §2 lists monitoring under
|
||||
`devops`, "my most differentiated material" — and `monitoring`/`grafana no data`/`smartctl exit code` are 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 two `notes` posts link up to
|
||||
the flagship with a relative link.
|
||||
- **Why:** the search results for `synology spreadsheet api` / `spreadsheet-api docker` are 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 tag `nocodb/nocodb:2026.09.0` **and** a returned row
|
||||
timestamp `2026-09-27 01:54:16+00:00` → floor 2026-09-27.
|
||||
- `nocodb-sso-is-a-licensed-feature` — same `2026.09.0` image 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 names `Chrome 154` (≈ Oct 2026 on Chrome's
|
||||
cadence) and the post links back to `scraping-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 `updatedDate` holding the true date, so `lastmod` stays 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-page` was
|
||||
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 address `0xc2022b56…`, not dates. All cited figures were traced to source:
|
||||
`868596` / `4102 bytes` / `993568` gas / `0.000535185588997216 ETH` / block `6522146` / mint `181874`
|
||||
(deploy + mint logs), `335011` gas (`.gas-snapshot`), `2.5pt→7.5pt` + `20,225,818` bytes + `30–490pt`
|
||||
(`docs/pdf-pipeline.md`; the `27 pages` / `12,789 pages` figures live in `app/Libraries/ReportPdf.php`
|
||||
lines 21 and 196, **not** in the doc), `@Version` + `POST_VERSION_CONFLICT` in the Spring demo.
|
||||
- ⚠ **The `TERMINALS` / `BANNERS` entries 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`/`gapBelow` within 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_regex` pattern, written up as the *correct* counterpart to `authentik-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_single` leaves `internal_host` empty, so the SSO chain is textbook-correct
|
||||
while **every app path 404s** — the tell is `x-powered-by: authentik` on the 404; (2) the embedded outpost
|
||||
needs **1–2 minutes** to pick up a provider change and answers `302 → /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 `Authorization`
|
||||
header, 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 (including `POST /api/send` returning 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 `TERMINALS`
|
||||
entry, 8-row `BANNERS` entry 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/reader`
|
||||
is **alive** — 11,036★ / 5,471 forks / AGPL-3.0 / default branch `main`, 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/data` JSON → **`/data` SQLite**,
|
||||
env config → **admin-UI settings in the DB**, image moved to **`cnb.cool/hectorqin/reader:main`** (branch/commit tags,
|
||||
deliberately no `:latest`), **0 releases / 0 tags**; Docker Hub `hectorqin/reader` still **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`). **The `hectorqin-reader` skill 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:alpine` container owning the public port, the request-time
|
||||
`resolver 127.0.0.11` (so the front page survives the app being down), `client_max_body_size` preserved for uploads,
|
||||
why a proxy **body rewrite** cannot touch a client-rendered SPA, the "reload → back to the front door" injection with
|
||||
its `sessionStorage.gatePass` consume-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-06`
|
||||
so `lastmod` stays honest — and it is constrained to **link backwards only** (it cross-links
|
||||
`adding-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 --check` OK) via
|
||||
`scripts/append_generator_entries.py`: 4-line `TERMINALS` + 8-row `BANNERS` per slug.
|
||||
- **Verified after deploy:** build clean (139 pages, Pagefind 2 languages, no duplicate-id warning); `measure_og_banner.py`
|
||||
**PASS** on both (og `rows=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 entries `lastmod 2026-10-06` with 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 the `mailto:` to `email-protection`
|
||||
— expected). Commit `378d55c`, pushed Gitea + GitHub, both remotes **0/0**; follow-up `ef3f520` sharpened the wipe
|
||||
evidence after checking the actual repo (not just the API): `master` is a **single commit dated 2026-06-22** whose
|
||||
README is the word `deleted`, sharing **no common ancestor** with `main` — the API's `created_at` still 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 of
|
||||
`github.com/hectorqin/reader` (`main` @ `5c59cae`, plus the 1-commit `master`), pushed with `git push --mirror`.
|
||||
⚠ Gitea's hook **rejects `refs/pull/*`** ("hook declined") — that is expected and harmless; branches and the whole
|
||||
object history land. A future session should re-push from a fresh `git clone --mirror` to refresh it, or switch the
|
||||
repo to a Gitea pull mirror (the existing `hoelee/reader` uses 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** in
|
||||
`3e354cc`: both `.md` files `git rm`'d and its `TERMINALS`/`BANNERS` entries removed from both generators (pure
|
||||
deletions — 0 added, 17 + 5 removed, `node --check` OK, 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). Keep `pubDate` 2026-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.py` applied 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 build` clean (155 pages, no duplicate-id warning); all 8 `dist/` 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`/`BANNERS` entry sets** — the three for this batch's slugs
|
||||
(two held + one parked) rode into `2295c0f` together 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 `.md` files plus its two PNGs.
|
||||
- **To publish one:** confirm the real `pubDate` (`date`, and the newest `<time>` on the live `/posts/`), then
|
||||
`git add` the four explicit paths (post EN/ZH + og + banner) and push `origin 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 `t` shadowing the i18n `t()` function, and a `let LANG` declared *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) and `chainlink-vrf-v2-lottery-contract` (2024-12-10) — verified in the built feed: the post lands at
|
||||
position 64 of 67, monotonic, with `lastmod` = 2026-10-16 from `updatedDate`.
|
||||
- ⚠ **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.py` on EN only), og + banner generated and measured
|
||||
(og `rows=1 overflow=0`; banner 8 rows `delta=2 overflow=0`), build clean at **155 pages**, both `dist/` 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 reported `itemNotFound` *after* finishing. (2) The DSM Storage Manager
|
||||
card reads `running` 24×7 by design (`run_all_time`, `schedule_table` 168×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 hit `554 5.7.1` (no PTR, 4 RBLs clean) and the relay hit `550-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: forward `mail.hoelee.com → 41.216.187.102`
|
||||
resolves, the reverse lookup for that IP still returns nothing. (4) Four families / eight static faces embedded:
|
||||
static only because the PDF engine's parser reads `glyf`/`loca` and ignores `fvar` (variable fonts resolve to
|
||||
Thin 100 / ExtraLight 200), `font-weight: 600` rewritten to `bold` for 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 every `lastmod` is the real day.
|
||||
- **State:** EN + ZH written (`rewrap_post.py` → `wrap_lists.py` on EN only, ZH one paragraph per line); og + banner
|
||||
generated and **measured** for all four (`rows=1 overflow=0`, banner 8 rows `delta=2 overflow=0`); build clean at
|
||||
**163 pages**; 8 `dist/` 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, correct `content_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** with `lastmod` 2026-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 of `space-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-existing `og:locale = en` on 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):**
|
||||
1. **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 (EN `you/your` 12 → 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.
|
||||
2. **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 万行`).
|
||||
- **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/gen` measured afterwards: 2 title lines, `overflowY 0`, `measure_og_banner.py` PASS · 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_now` only killing when `self.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 (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
|
||||
- [x] `/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
|
||||
- [x] `/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 (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 `Disallow`ed 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).
|
||||
### Phase F — Artwork rollout: v2 "one art, four targets" (DONE 2026-10-19)
|
||||
- ✅ All 72 base frames rendered (1360×768) and OCR-gated; `art-meta.json` carries **72 hand-written evidence lines** (`scripts/og-gen/build_art_meta.py` regenerates them and aborts on any slug that lacks one).
|
||||
- ✅ 288 frontmatter fields migrated (`/og/<slug>.jpg`, ZH `/og/zh-<slug>.jpg`, `banner: /banners/<slug>.jpg`); the 144 v1 PNGs deleted; `PostList.astro` + `[...slug].astro` fixed for the new extensions.
|
||||
- ✅ Live: EN/ZH cards, banner WebP and both thumbs 200; v1 `.png` 404; build **251 pages**, 0 missing image refs.
|
||||
- Re-surveyed 2026-10-19: 72/72 banners 1600×900 WebP · 72/72 `-800` thumbs 800×420 · 72/72 `-400` thumbs 400×210 · 144/144 share cards 1200×630 JPEG.
|
||||
- Open, and **not** engineering: the round-5 consultant questions ("no text" acceptance line, banner framing, dark art on the light theme) — see the phase log.
|
||||
- ⚠ Overwriting art in place keeps the same URLs, so a returning reader sees the old picture from their own cache; hard-refresh before judging a restyle batch.
|
||||
|
||||
### Phase A — Identity (DONE)
|
||||
- ✅ Real headshot in the About hero, the author card on every post, the home hero (EN + ZH) and JSON-LD `Person.image`; no 404 for any derivative.
|
||||
- ✅ One branding line ("Full-Stack Developer & DevOps Engineer") aligned across `SITE.title`, meta description, JSON-LD and the homepage hero.
|
||||
- ✅ Homepage `Featured` = a 2×2 bento grid over four pinned flagship slugs (fixed slugs, not "newest 4").
|
||||
- Deliberately deferred: a separate `/start-here` page — the homepage strip was the immediate win.
|
||||
|
||||
### Phase B — Content (the long game; all batches B1–B7 landed)
|
||||
- ✅ **72 EN + 72 ZH** published posts (live figure; measured 2026-10-19), 0 drafts, every post has its twin.
|
||||
- Still open: the two gotchas nobody has written yet — *Site-to-site OpenVPN behind CGNAT* and *Fixing the WordPress /cv 301→404 chain*. Both were on the old list and neither slug exists in `src/content/posts/`.
|
||||
- The per-session topic queue is **not** kept here any more: it is re-derived from the Hermes session store each time (rules in the `hoelee-blog` skill, *Sourcing the next post*).
|
||||
|
||||
### Phase C — Discovery & structure (DONE) · Phase D — Polish (DONE / declined)
|
||||
- ✅ **C1** per-post OG images — now `/og/<slug>.jpg` + `/og/zh-<slug>.jpg` (B6b); the shared default card backs the homepage and category pages.
|
||||
- ✅ **C2 tag pages** — live at `/tags/<tag>/` above the post threshold (measured 2026-10-19: `/tags/docker/` 200). ⚠ The old `[ ]` here was stale — B3 shipped them.
|
||||
- ✅ **C3** `/categories/` lists all 7 with descriptions and SVG art; an empty category renders "0 posts · coming soon" and is deliberately **not** a link (so its route 404s until its first post).
|
||||
- ✅ **C4** `/zh/posts/` and `/zh/categories/` both exist (B2), and the ZH nav points at them. ⚠ The old "still missing" line was stale since 2026-10-16.
|
||||
- ✅ **D1** search (Pagefind; the loader bug is B3). 🔵 **D2** GSC submission → merged into Step E1, owner-gated. ⛔ **D3** newsletter/email capture — deliberately not doing until there is real traffic.
|
||||
|
||||
---
|
||||
|
||||
@@ -1107,6 +345,16 @@ and a DOM probe for the regenerated card. Spot values: `/zh/posts/` 200 · `/tag
|
||||
|
||||
---
|
||||
|
||||
## 4c. Live re-check — 2026-10-19 (after batches B5–B7, measured this session)
|
||||
|
||||
- **Posts**: 72 EN + 72 ZH on disk, 0 drafts; build **251 pages**; homepage renders `72 posts in English and 中文`, the About pages now derive the same number.
|
||||
- **Mobile**: 12 routes × 4 widths (360 / 390 / 414 / 768) = **48/48 with `scrollWidth − clientWidth == 0`**.
|
||||
- **Art**: 72/72 banners 1600×900 WebP; 72/72 `-800` thumbs 800×420; 72/72 `-400` thumbs 400×210; 144/144 share cards 1200×630 JPEG. Featured bento cards now carry `srcset` (400w/800w) and pick the 400 file at DPR 1.
|
||||
- **Reading time**: EN `5 min read` vs ZH `5 分钟` on the same post (was EN 5 / ZH 1 before the CJK fix).
|
||||
- **Not enabled** (still owner-gated, re-measured today): Cloudflare Web Analytics — `grep -c cloudflareinsights` on the homepage returns **0**. `sitemap-index.xml`, `llms.txt` and `robots.txt` all serve 200.
|
||||
- **Remotes**: Gitea and GitHub both at `5ec321a`, 0 divergence.
|
||||
|
||||
---
|
||||
## 5. Conventions (non-negotiable)
|
||||
|
||||
- Push git.hoelee.com first, then GitHub
|
||||
|
||||
Reference in new issue
Block a user