Listings render `pubDate` but sorted by `updatedDate ?? pubDate`, so any
post carrying both dates sorted by a date it never displayed. After the
backdate commit the two diverged by years and the list read out of order
("January 7, 2026" above "September 13, 2026", "March 11, 2026" below
"December 10, 2025").
Replace `sortByUpdated` with `sortForListing`, which orders by `pubDate`
and uses `updatedDate` only as a tiebreak. This matches the RSS feed,
which already sorted by `pubDate`.
Verified all three listings (EN, EN homepage, ZH) are monotonically
non-increasing across the full 2024-09 -> 2026-09 span.
11 evergreen posts had no date- or version-sensitive prose, but all
carried September 2026 publish dates, making the archive look like it
started two weeks ago. Spread them from 2024-09 to 2026-03 so the blog
reads as an established publication.
Per post-guideline.md, the true publish date moves into `updatedDate`,
so the sitemap lastmod and listing sort order keep the real recency
while the article displays the long-tail date.
Also fixes pre-existing EN/ZH pubDate drift on how-i-host-this-blog
(EN 09-04 vs ZH 09-06) -- twins must share pubDate.
Posts left untouched pin themselves in prose (e.g. "In September 2026
a Seagate IronWolf 110...", prompt-expiry dates, model release dates).
Held unpublished (draft: true). Records the u003e escaping bug:
authentik renders > in branding_custom_css as the literal text
u003e, so any child combinator produces an invalid selector that
silently matches nothing. Includes the cssRules-based debugging
order and the character safety probe.
Docs: not yet recorded in project-state.md
Two finished posts written from the numerology-report migration work, kept as
draft: true so they build no pages and appear in no listing until published.
Content bank for weeks when there is nothing fresh to write.
- migrating-codeigniter-iis-to-openlitespeed (engineering)
IIS -> OpenLiteSpeed/CyberPanel. The two fatals that only appeared once the
authentik SSO gate was gone, the docroot public/ separation, and the
loopback self-call that becomes a real outbound HTTPS request on LiteSpeed.
- upgrading-codeigniter-46-to-47 (notes)
The two fatal config properties NOT in the official upgrade guide
(permittedURIChars, jsonEncodeDepth), why Composer never merges app/Config,
and the property-diff script that finds the whole class of problem at once.
Both fill the starved engineering (1 post) and notes categories. project-state.md
records them as Step B2b with the publish checklist.
EN + ZH devops gotcha post on debugging MySQL Workbench 26.7.0's failure
to connect to MariaDB. Three patches to Oracle's bundled code, all the
same root cause: `major >= 8` is not a valid MySQL-vs-MariaDB test.
Also adds per-post OG + banner (TERMINALS/BANNERS entries).
The crawler/sitemap mechanics now live in the repo doc, not just in the
commit log: robots.txt is a build-time endpoint that must stay pure ASCII
(no charset on a text/plain response means non-ASCII renders as mojibake),
the policy is allow-all with enforcement deliberately left to Cloudflare,
why writing a real robots.txt demotes Cloudflare's placeholder from
replacement to prepend, and that lastmod/hreflang are derived so they must
never be hand-authored.
robots.txt carried a UTF-8 em dash in its header comment, which renders as
mojibake ("鈥�") in clients that read text/plain as a legacy codepage. The
file is now pure ASCII (verified with `LC_ALL=C grep '[^ -~]'`), and the
encoding trap is documented in the endpoint so it does not come back.
Policy change to allow-all: the previous version blocked CCBot, Bytespider,
Amazonbot and Applebot-Extended. Training crawlers are now welcome too - the
blog benefits from being read, and robots.txt is advisory anyway (Cloudflare
documents it as unenforced; AI Crawl Control is the enforcement layer).
Drops the Content-Signal directives along with the blocklist.
Sitemap gains both fields Google actually uses:
- lastmod on all 58 post URLs, from updatedDate ?? pubDate, read straight
from the markdown frontmatter at config-eval time (the sitemap runs in
astro:build:done, after the content collection is gone).
- xhtml:link hreflang alternates on all 77 URLs, pairing EN/ZH twins.
/posts/ is special-cased: it has no /zh/posts/ twin, the Chinese post
listing IS the /zh/ homepage, so the pair is declared rather than derived.
Verified programmatically that every emitted alternate resolves to a page
that is actually built and present in the sitemap.
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.
Cloudflare's Email Address Obfuscation rewrites literal email
addresses into [email protected] with a data-cfemail payload, which
broke the gpg --locate-keys line inside the code block - readers
copying the shell command got garbage. Replaces it with a
comment pointing at the .asc link and the published fingerprint.
Readers arriving at the onion addresses had no instructions for
opening a .onion. Adds a step-by-step Tor Browser install (download,
GPG signature verification, connect, bridge fallback) plus how to
open a v3 address: 56-char base32 rules, no typo correction, v2
retirement, and the 10-60 minute descriptor propagation window.
Mirrored to the Chinese version in the same commit.
Case study of migrating from paid RDPGuard 7.8.7 to open-source IPBan
4.1.0, covering the three undocumented traps: the uninstaller that would
have silently unbanned 12 active attackers, the non-existent
--install-service flag in v4.1.0, and ExpireTime vs BanTime.
Also fixes an og-gen defect: the tag chip hardcoded KIND='DevOps', so
every non-devops post carried a wrong label (e.g. "case-studies · DevOps"
on the STT card). KIND now derives from the post category, and all 29
existing OG images are regenerated with correct badges.
New flagship case study covering a GPU-backed whisper.cpp transcription API
reachable from Windows, iPhone, iPad and Android behind an authenticated
gateway — framed as a service offering with the office-productivity case
(roughly 5x typing throughput, unlimited, audio never leaves the premises).
Content:
- EN + ZH posts (same slug -> auto language switch)
- "Why it matters" opener, hire CTA with clickable WhatsApp + mailto
- Four documented traps: incomplete CUDA component selection, loopback bind
mistaken for a firewall problem, n8n Code nodes discarding binary + the
data0 key name, and nginx default.conf hijacking port 80
- Honest scoping of the auth model (access control, not hardened public API)
Assets:
- Custom OG image + 16:9 banner (generator entries appended, not patched
inside the template-literal maps)
Also marks Mem0 (B1) done and adds B1b to project-state.md.
- New post: 'The Corruption Came Back on Different Drives — the Cause Was
TRIM, Not the SSDs' — same 0x8941f998 zeros fingerprint on a second drive
stack (IronWolf 110 SATA pair), root cause queued TRIM (FreeBSD gag 264139),
fix diskAutotrim=off + nodiscard, scrub evidence, enterprise SATA buying
guidance. en + zh, custom OG + banner.
- Publish formerly-draft SATA cable post (draft:false) with OG + banner.
- Correct drive identity in both: ZA960NM10001 is Seagate IronWolf 110, not
'Samsung PM9A3' (PM9A3 is NVMe; FPDMA errors are SATA-only).
- Old RAID post (en+zh): cross-link to sequel, fix 'a Samsung' -> IronWolf 110.
Three new posts (EN + ZH twins, og + banner each):
- n8n-v1-to-v2-upgrade-gotchas (devops): the seven deprecations that
surfaced upgrading 1.123.x → 2.40.1, decoded from the boot log —
telemetry schema rejection, N8N_WEBHOOK_URL rename, internal runner
deprecation, task timeout 300s→60s, two compression limits, v3
storage rename, plus the DB override that silently disabled the
AI sandbox.
- self-healing-digital-goods-entitlements (case-studies): the W1–W5
NocoDB → n8n → AList entitlement lifecycle. Build-time code sharing
for n8n Code nodes, MAX-expiry semantics, dry-run safety, daily
drift repair, CORS-not-HMAC reasoning, and the public→internal
NocoDB cascading-failure fix (504 → retry storm → 503).
- running-tts-as-a-service-with-token-sidecars (ai): a year-long TTS
service built on two cron containers that refresh Azure/Google
tokens into a shared file, with the speed/voice mapping layer.
banner-gen: center terminal body vertically so line counts shorter
than the fixed 690px panel don't leave a dead void at the bottom.
Verified via DOM measurement (gapAbove 104 / gapBelow 106).
- 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)