01 payments (EN+ZH)
- idempotency gate is now a whitelist: only PENDING and a late-settled
EXPIRED may enter fulfilment. Checking only for FULFILLED left the whole
PROCESSING window open to a second queue push (consultant finding).
- k-of-n -> two-tier: both chassis anchors (SMBIOS board UUID + CPU
architectural signature) matching is sufficient; if one anchor changed,
every auxiliary digest must match. Aux dimension moved off the volatile
OS GUID onto permanent NIC MAC.
- FSM diagram: RETRY_QUEUE branches from PROCESSING, not FULFILLED
02 comfyui covers (EN+ZH)
- subscribe the WebSocket BEFORE queue_prompt, settimeout(120),
try/finally ws.close(), /history fallback on timeout: fixes the
lost-terminal-frame hang and the per-image socket leak
- document Pillow half-wave rectified Laplacian (offset=0 clamps negative
responses to 0) and why a signed float kernel would not work here
- replace the unverifiable "89 covers in 48 minutes" with the filesystem
evidence: 216 assets (108 banner+OG pairs) inside a 25.9-min window
- ZH: drop the colloquial imperative; add self-bootstrapping note
03 traefik yaegi (ZH)
- the .traefik.yml contract paragraph and scoreThreshold 120 had only
landed in the EN file; both now applied to ZH
- <summary> line counts corrected 29 -> 30 in both languages (verified
programmatically for all eight code blocks)
layout
- standing provenance note in the site footer (sanitised client
identifiers / production thresholds), per consultant Q7 ruling
/posts/ and /zh/posts/ become hybrid: three fixed-slug flagship cards above the
complete year/month archive, with the flagship block collapsing (inert, h=0) as
soon as the reader filters. /categories/case-studies/ becomes a portfolio: compact
two-column hero with a 21:9 deterministic topology banner, flagship ordering from
lib/portfolio.ts instead of pubDate, and one measured pill per card.
Also fixes what the screenshot review turned up: ZH archive rows and category chips
were printing raw English slugs, the two chip strips were unlabelled and read as one
list with a duplicated 'devops', card meta rows broke rhythm past four tags, the nav
had no current-section indicator, and mobile title links were under the 44px tap
minimum. scripts/probe-layout.mjs is the repeatable gate: 72/72 page x viewport
checks and the collapse test.
- brand: three-colour Hoelee mark (rasterised from the brand vector,
transparent, public/images/hoelee-mark-96.png) followed by the name
- section links grouped in .nav-links so the mobile header puts them on
their own row instead of wrapping past the search/lang/theme controls
- measured 320/360/390/414/600/768/1280: scrollWidth == innerWidth,
header 91.6px two rows (<600px) / 60.3px one row, mark loads at 22/26px
- Homepage (EN + ZH) featured section becomes a 2x2 Cyber-Bento grid: OG
thumbnail at 65%/140px behind a scrim, category badge + metric pill,
3-line-clamped description, arrow CTA; 1 column under 680px.
- Bento scrim/pill/dot now use theme tokens (--card-veil, --pill-bg,
--bento-visual-opacity). The original hardcoded dark blend measured
1.96:1 pill contrast on the light theme; now 6.56 dark / 6.10 light.
- Add the missing @keyframes bento-pulse (the doc claimed a pulsing dot,
none existed), disabled under prefers-reduced-motion.
- Tag chips: mono face, hairline border, 4px radius; dashed border for
non-link tags.
- ZH pages: brand links /zh/, in-post category tag links
/zh/categories/<cat>/ instead of the EN route.
- project-state: B6 shipped note with measured readings; correct the
prompts-catalog path claim (file is outside the repo).
Measured in headless Chrome at 1280px and 390px, EN + ZH: 4 cards,
2 cols / 1 col, scrollWidth-clientWidth 0, no card overflow, all four
-800.webp thumbs loaded at 800x420.
The photo is the identity evidence the external review flagged as the biggest
gap. One source image (Pictures/Professional Portrait with Blue Gradient
Background.png, 1254x1254), two sizes, WebP first with a JPEG fallback —
continuous tone is the case where WebP/JPEG beat PNG:
640x640 42 KB JPG / 26 KB WebP About page (190 CSS px)
128x128 3 KB JPG / 2 KB WebP author card + home hero avatar
It now appears in the About hero, the author card at the end of every post, the
home hero (both languages), and JSON-LD Person.image — the node search engines
read to associate the name with a face.
About went from 1,024 characters to five sections: identity with photo, "By the
numbers" (162 containers / 3 hosts / 71 essays / 0 hosting companies between me
and the servers), the real tech stack in four columns, the Hoelee Enterprise
block (website design and development is the main business, email hosting
secondary), and a contact split between hiring and business.
Also: the headshot's dark-blue background sits close to the page background, so
the photo, hero avatar and card avatar get a faint brand ring plus shadow.
Pictures/ keeps the converted JPG master next to the untouched PNG.
Found a real bug while verifying the nav search: /pagefind/pagefind.js is an ES
module that defines no window.Pagefind, but Search.astro loaded it with a classic
<script src> and read window.Pagefind — so every query answered "Search is
unavailable." (confirmed live before the fix). Now a dynamic import; the specifier
must be a variable or Rollup resolves it at build time and the build fails.
Measured before/after in a real browser:
- archive /posts/ + /zh/posts/: 18,220px -> 3,788px (71 rows at 44px, grouped by
year) with a client-side filter (title/tags/description text, category chips,
14 tag chips, zero requests). Tested: "nginx" -> 6 rows, chip devops -> 28.
- tag pages for tags with >= 3 posts. Measured 235 distinct tags (an earlier brief
claimed 74), 156 used exactly once — pages only where a topic exists: 40 tags ->
80 pages, build 165 -> 245 pages. Pills link only when a page exists.
- homepage + ZH homepage: hero (288px) with the real scale claim, three CTAs, a
fixed Featured 4 (scale -> automation -> observability -> a delivered result),
Latest 10 -> 6. ZH hero had linked "关于我" to the English /about/.
- long code blocks collapse above 420px with an Expand button + gradient, copy
button still copies the whole block (verified on the 1010px block).
- search is visible ("⌕ Search /" pill, "/" plus the existing Ctrl/Cmd+K) and the
zero-result screen offers topic chips instead of a dead end.
Docs: project-state B3 entry, ops-runbook Pagefind gotcha.
All items measured before/after with curl, on the origin and through Cloudflare:
- /posts/zh/ answered 403 (directory with no index) -> nginx 301 to the new archive
- /zh/posts/ 404'd -> new src/pages/zh/posts/index.astro (build 163 -> 165 pages)
- ZH nav pointed "关于" at /zh/ and "文章" at /zh/ -> /zh/about/ and /zh/posts/
- og:locale was "en" on every Chinese page -> zh_CN, plus og:locale:alternate on
both languages (EN pages now advertise zh_CN as well)
- hreflang "zh" -> "zh-Hans"; <html lang> on ZH pages -> zh-Hans
- no 404 page existed -> src/pages/404.astro, served by nginx with a 404 status so
crawlers skip it, offering search + archive + the 3 newest posts
- JSON-LD Person gained worksFor (Hoelee Enterprise); image still omitted until the
author photo exists rather than pointing at a placeholder
nginx: config moved from the stock image file to a host bind mount
(/mnt/user/docker/nginx-hoelee-blog/conf.d/default.conf, Portainer stack 247 on
endpoint 3), validated with nginx -t in a throwaway container before the redeploy.
It also sets /_astro/* to immutable 1-year (hashed names) and HTML to
must-revalidate; unhashed /og/*.png and /banners/*.png keep the default TTL on
purpose. Note: the public HSTS and 16-day asset TTL come from Cloudflare zone
settings, not from this nginx.
Docs: ops-runbook.md (the nginx layer + why each rule), project-state.md (B2 entry
and the two open Cloudflare items my token cannot write).
robots.txt was absent at the origin, so Cloudflare served its Free-plan
Content Signals Policy placeholder — a file with no User-agent, no Sitemap
and no directives. Adds src/pages/robots.txt.ts (origin 200 → CF merges
instead of substituting) pointing at sitemap-index.xml, disallowing
/pagefind/, and expressing search=yes / ai-input=yes / ai-train=no in line
with the two-lane SEO+GEO strategy in docs/seo-reference.md. Citation
crawlers (Google-Extended, OAI-SearchBot, PerplexityBot, ClaudeBot) stay
allowed; only training-only harvesters blocked.
hreflang was never emitted despite full EN/ZH i18n: both language versions
sat in the sitemap with no cross-reference. BaseLayout gains altLocaleUrl
and emits en/zh/x-default per page; every post pair, category pair, and
landing page now cross-links.
Also adds BlogPosting JSON-LD (@id-linked to the site-wide Person node)
so Google has a rich-result-eligible node per article, and creates the
missing /zh/about/ page — the EN about page pointed its language switch at
a 404, and /about/ was the only unpaired page after hreflang landed.
- 7 categories now listed with name, blurb, post count and a
per-category terminal-style SVG illustration (CategoryArt)
- empty categories show '0 posts · coming soon' (non-link, no soft-404)
- new zh pages: /zh/categories/ index + /zh/categories/[category]/ detail
- PostList is locale-aware (zh-CN dates, zh category links)
- zh nav '分类' now points to /zh/categories/
- category detail pages get art + description header
- language switch wired both ways (EN <-> ZH)
- Add translation field to schema + auto-match by zh/ prefix
- BaseLayout accepts per-page switchHref; post page resolves translation link
- Falls back to section landing (/ or /zh/) when no translation exists
- Derive lang from folder (posts/zh/* -> zh, else en); remove lang: frontmatter
- Change post route to [...slug] to capture nested zh paths
- Add nav language switcher (EN <-> 中文) + /zh/ landing page
- Default theme to dark (light opt-in via toggle)
- Translate hello-world-zh placeholder into English intro post
- Update case study: docker-compose sample + real install gotchas (commit history)
- Add docs/project-state.md (Tier 1/2/3 backlog)