From 2060aaa1661106bd9e8fc5afb9e540489bae9320 Mon Sep 17 00:00:00 2001 From: hoelee Date: Sun, 18 Oct 2026 16:33:21 +0800 Subject: [PATCH] docs(project-state): truth-up four stale statuses, record the batch blockers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured against the live site and the cron outputs today, not carried over: - A1 (author photo) and A3 (featured strip) were still ⬜ although both shipped (B4 and 6e9d58b) — marked done with the evidence, /start-here left as a deliberate deferral. - E3 is fully automated: the LinkedIn half was already a daily no_agent cron (id 35f8985f609a). 5 posts published as of today's 12:30 tick; its silence is not proof of work, so the output files are the record. - E6 closed: HSTS waived by the owner ("无视吧"), re-measured still max-age=0. Do not re-open. - E1: CF Web Analytics re-measured absent (0 cloudflareinsights hits, one Umami tag). It is an owner dashboard action — no token here can enable it and CF injects the beacon itself, so there is no repo change to make. - B6 now states the real shape of the artwork work: 0 of 72 posts rolled out, the live site still serves the v1 images, the 3 consultant questions are unanswered, the rendered OG card is measured at 558 KB PNG vs 84 KB JPEG q85, and the promised OCR pseudo-text gate does not exist yet. --- docs/project-state.md | 51 +++++++++++++++++++++++----------------- scripts/og-gen/README.md | 1 + 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/docs/project-state.md b/docs/project-state.md index b18ab1d..4612402 100644 --- a/docs/project-state.md +++ b/docs/project-state.md @@ -45,8 +45,11 @@ Living list of what's done and what's next for blog.hoelee.com. Work through the - **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//`, ZH brand links `/zh/`. - - Full catalog of 72 posts' unique physical metaphors and prompts extracted to `scripts/og-gen/prompts_catalog.json` (working-tree tooling, not committed) and `Desktop\blog-og-image-redesign-brief\03-72-Posts-Visual-Prompts-Catalog.md` (outside the repo). - - Round 4 delivery bundle packaged at `C:\Users\hoelee\Desktop\blog-og-image-redesign-brief\delivery-for-consultant-round4.zip`. + - 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. @@ -148,6 +151,7 @@ The user asked for the dashboard to be reachable remotely **without** publishing | **Tracker wired into the two sites (2026-09-29) ✅** | Website records created: `blog.hoelee.com` → `1817d8f6-0e49-4b9a-a50d-307e4f2962c7`, `www.digikedai.com` → `61b57143-f3f4-4353-9ede-9c615146de84`. Snippet added to `blog/src/layouts/BaseLayout.astro` (commit **de17bea**, pushed Gitea+GitHub, Gitea Actions → live in ~1 min; covers EN **and** ZH since both use the layout) and to `dsm-resource-management/www/src/layouts/Base.astro` (CF Pages via `./deploy.sh`, deployment `4d3d6aa6.digikedai.pages.dev`). Both tags carry `is:inline` (Astro would otherwise process the script) + `data-domains` so local dev never reports. Verified live HTML on both domains contains the `data-website-id`; verified end-to-end with a **real Chrome** (Canary CDP 9222): blog `/` + `/zh/` → 4 pageviews/2 visitors, digikedai `/` + `/products/` → 2 pageviews/1 visitor, geo `MY / MY-07 / George Town`, chrome/Windows 10; then `POST /api/websites//reset` cleared the test rows (both back to 0) | | ⚠ **Verification pitfall: headless browsers are ignored** | Umami drops bot/headless user agents — `HeadlessChrome/151` (DSM browserless) → `200 {"beep":"boop"}` and **nothing stored**; a normal Chrome UA → `200 {"cache":…}` and stored. So `beep boop` = *ignored*, not success, and an empty dashboard after a headless test is not a bug. Verify tracking with a **real** browser (Canary CDP 9222: `curl -X PUT "http://127.0.0.1:9222/json/new?"`), or at least a normal-UA curl | | Not done yet | CF Web Analytics beacon + GSC `sitemap-index.xml` submission (both independent of Umami) | +| **Re-measured 2026-10-18** | Live homepage carries **exactly one** tracker tag (`stats.hoelee.com/script.js`, `data-website-id=1817d8f6…`) and **zero** `cloudflareinsights` references. Enabling CF Web Analytics is an **owner dashboard action** — there is no API token on this host with account scope (the deploy token purges only, and `~/.secrets/cloudflare-stats-token.txt` is still an unfilled placeholder), and CF injects its beacon itself once the site is added, so there is **no repo change to make**. GSC submission likewise only answers in its own UI. | | Known trade-off (honest) | (1) SSO protects the *dashboard route*, but Umami has **no OIDC**, so after SSO the user still sees **Umami's own login page** — double login, expected, one-click after the first time (2FA is available per-account in Settings → Profile; `TWO_FACTOR_ENCRYPTION_KEY` already in the stack env). (2) `` remains a **LAN break-glass path that bypasses SSO** (still needs Umami credentials; verified closed from the internet) — delete/close it if that is not wanted. (3) If the authentik outpost goes down, the dashboard goes down with it (tracker too — but the site keeps loading, the tracker fails silently) | | Rollback | Start the gate again (`sudo /usr/local/bin/docker start umami-gateway`, or start stack 285 in Portainer) → re-point the vhost + `ReverseProxy.json` to `5410` → reload nginx. Backups of both files sit next to the originals. (The gate container is now **`Exited (0)`** — it was stopped once the outpost took over, so port 5410 is free; the stack is kept as the rollback asset) | | Note | The `hermes3` authentik API token expired mid-session (DB says `expires 2026-09-28 22:17:08Z`); the leftover self-test account was removed through authentik's own ORM (`ak shell`), so no admin-group test user is left behind | @@ -163,8 +167,7 @@ domain passes no authority and offers no click-path to the portfolio — one-way into that overhaul (together with the v2.1 audit finding that its agency framing conflicts with the job hunt). Tracked here so it isn't lost. -**Step E3 — Syndicate 2–3 flagship posts.** 🔵 dev.to half **automated 2026-10-17**; LinkedIn still manual -(report §9 "Month 2+ — promote": never executed) +**Step E3 — Syndicate 2–3 flagship posts.** ✅ Both halves automated (dev.to 2026-10-17, LinkedIn 2026-10-16; measured 2026-10-18) `seo-reference.md` already carries the syndication policy (cross-post with canonical back to the original domain); until 2026-10-17 **nothing had ever been syndicated**, so all 72 posts existed only for people who already knew @@ -185,8 +188,15 @@ the URL — the daily dev.to drip below is the first distribution channel outsid tags `ai, python, showdev, automation`, cover image re-hosted by dev.to (so the banner landed). ⚠ Quirks written into the `hoelee-blog` skill: the banner and the layout hero are frontmatter (a verbatim body copy ships imageless), relative links must be absolutised **outside** code fences only, and dev.to rejects unknown tags. -- [ ] One LinkedIn share per flagship (same photo + name identity as the blog). -- **Done when:** each dev.to copy is live with the canonical set, and each has one LinkedIn post. +- [x] ✅ **LinkedIn half is automated too (installed 2026-10-16, measured 2026-10-18).** Hermes no_agent cron + `linkedin-daily-post` (id `35f8985f609a`, `30 12 * * *`) runs `scripts/publish_next.py --commit` in + `D:/dev/hoelee-career-agent` and stays **silent unless it published / failed / ran dry** — silence is not proof + of work, so read `cron/output/35f8985f609a/*.md`: **5 posts published** as of 2026-10-18 12:30 (today's was + `read-only-nocodb-dashboard-for-a-remote-database`). Its "already published" ledger is NocoDB + `activity_events`, not a local file. So the old manual criterion is superseded: what remains is **feeding the + approved-draft queue**, not clicking share. +- **Done when:** ✅ each dev.to copy is live with the canonical set (2 of 69 so far, one per day), and the LinkedIn + drip is publishing (5 measured, daily 12:30). **Step E4 — Chinese-side routes and nav are broken.** ✅ Done 2026-10-16 (batch B2), re-verified live 2026-10-17 @@ -206,7 +216,7 @@ ZH post pages emit `` with `og:locale:al - **Done when:** ✅ measured 2026-10-17 — a ZH post page emits `og:locale=zh_CN` with `og:locale:alternate=en_US`. -**Step E6 — Cloudflare edge HTML caching.** 🔵 Cache Rule live 2026-10-17 and **measured working**; the HSTS half is still open +**Step E6 — Cloudflare edge HTML caching.** ✅ Done 2026-10-17 (Cache Rule + purge + freshness proven); HSTS waived by the owner 2026-10-18 A post page used to return `cf-cache-status: DYNAMIC`. A static site behind CF should be edge-cached — lower TTFB and less origin traffic through the tunnel. @@ -221,16 +231,14 @@ less origin traffic through the tunnel. - [x] ✅ **The deploy-side purge is proven live, not assumed.** The Gitea Actions run for `112cf7d` (task 252 / run 11313, job 22965) logs `Purging zone *** … {"success":true,"errors":[],"messages":[],"result":{"id":"…"}}` — `CF_TOKEN` and `CF_ZONE_ID` are set and the API call succeeded, so a publish is not stuck behind the 2 h Edge TTL. -- [ ] **HSTS is still `max-age=0; includeSubDomains; preload`** (re-measured 2026-10-17) — the decision (12 months, - `includeSubDomains`, **preload OFF**) is still an owner action. `max-age=0` makes the `preload` directive inert, - so nothing is broken today; the zone setting is simply not doing anything yet. +- [x] ~~HSTS is still `max-age=0; includeSubDomains; preload`~~ — **waived by the owner 2026-10-18** (「HSTS 无视吧,没有关系的」). Re-measured the same day: still `max-age=0; includeSubDomains; preload`. Nothing is broken by it (`max-age=0` makes `preload` inert, and Cloudflare's "Always Use HTTPS" already covers the http→https hop), so this is a deliberate no-action, not a gap. **Do not re-open it** unless the owner asks. - [x] ✅ **Freshness proven end-to-end, 2026-10-17.** Before the push, `/posts/` was a `HIT` still serving `last-modified 14:35:23`; **67 s after** the push the same URL served `last-modified 14:56:41` with `age=0` — the purge invalidated the 2 h-cached copy and the new build was live **inside a minute**, so the Edge TTL cannot hide a publish. ⚠ Recipe: compare the **`last-modified` header** before/after a push (nginx stamps it at deploy time) — `cf-cache-status: HIT` alone can be a 2-hour-old build, which is exactly the trap this test exists to catch. - **Done when:** a repeat request shows `cf-cache-status: HIT` ✅ **and** a deploy goes live within ~1 minute ✅ - (67 s measured) **and** HSTS carries the decided value ⬜. + (67 s measured) **and** HSTS carries the decided value ⏸ **waived 2026-10-18 by the owner** — the step is closed. **Step E7 — Content-volume guardrail.** ⬜ ongoing @@ -264,23 +272,24 @@ less origin traffic through the tunnel. ### Phase A — Identity (highest ROI, ~2–3 hrs total) -**Step A1 — Add a real author photo (headshot).** +**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. -- [ ] User provides one headshot (square, ≥800×800 for avatar; also source for og). -- [ ] Replace `avatar` letter with the photo in: nav/brand (optional), author card (About + every post), `ProfilePage` JSON-LD `image`. -- [ ] Add the photo to `og-default.png` template so the default share card has a 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; `curl` shows no 404 for the asset. +- **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 `

`, 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.** +**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). -- [ ] Add a "Start here" (or "Featured") section on the homepage surfacing 2–3 flagship case studies. -- [ ] Optionally add a `/favorites` or `/start-here` page (defer the dedicated page until ≥6 strong posts; the homepage strip is the immediate win). -- **Done when:** homepage shows a featured/start-here strip above or beside "Latest posts". +- [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) @@ -894,7 +903,7 @@ not needed), cross-checked against the local repo at `D:/dev/hoelee-blog`. - Ideogram 4 pipeline running locally (`1360x768`, ar 16:9). - Outside consultant reviewed Round 1..4. Frosted Bento layout + blueprint grid + headline auto-fitting (`FIT px`) verified on real Chrome Headless. - Delivery package Round 4 zipped (`delivery-for-consultant-round4.zip`, 2.4 MB). - - Ready for 72-post batch run once consultant finalises the 3 minor typography questions. + - Ready for the 72-post batch run — **blocked on**: the consultant's answers to the 3 typography questions, the `og:image` format decision (measured: 558 KB PNG → 84 KB JPEG q85), a GPU window for the ~70 missing base-art frames, and the promised OCR pseudo-text gate. Method: `curl` from the Windows host, the unRaid nginx access log **read over the Portainer API** (no SSH to that host), diff --git a/scripts/og-gen/README.md b/scripts/og-gen/README.md index e45a910..ae6aa3f 100644 --- a/scripts/og-gen/README.md +++ b/scripts/og-gen/README.md @@ -42,6 +42,7 @@ No entry → a generic default terminal. - PNG (not JPG) is intentional: for flat UI/terminal art, PNG is smaller and sharper than JPG (see commit history). - The `logo-square.png` referenced lives at `public/logo-square.png`. + --- ## v2 pipeline ("one art, four targets") — `render_from_art.mjs`