Files
hoelee-blog/docs/ops-runbook.md
T
hoelee f364084284
Deploy / build (push) Successful in 56s
perf(images): WebP banners, text-only related cards, resized list thumbnails
Measured before/after with headless Chrome + CDP (not by eye):

- article banner: <picture> WebP q78 with the PNG as fallback, because the PNG
  must stay for og:image (social scrapers do not reliably render WebP cards).
  70 banners: 6.02 MB -> 2.52 MB (42%). JPEG measured *larger* than PNG here
  (q85 = 129%) — the art is flat terminal-style with sharp text edges.
- post page related cards: dropped four 1200x630 PNGs (183 KB) to display
  526x276 where the embedded code is illegible; now text-only cards with a
  category badge, date and reading time.
- list thumbnails (home + archive): new /og/thumb/<name>-{400,800}.webp served
  via srcset/sizes. Rows display at ~398px but were pulling full 1200x630 PNGs.
  Homepage thumbnails ~450 KB -> 62 KB; archive (71 rows) ~3.2 MB -> 436 KB.
- desktop banner capped at 720px centred: 67% -> 45% of a 900px viewport,
  keeping the art intact instead of cropping it.

Post page totals: images 277 KB -> 38 KB; page height 5758 -> 5065 (desktop)
and 9423 -> 8807 (mobile). Build: 163 pages.

Script: scripts/optimize-images.py; policy: docs/design-guide.md §3; runbook
step added to docs/ops-runbook.md.
2026-10-16 17:11:27 +08:00

86 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ops & Deploy Runbook — blog.hoelee.com
How this blog is built, deployed, and kept alive. Read when debugging CI/CD, deployment, or hosting.
---
## Pipeline
```
Markdown (src/content/posts) → Astro build (dist/) → Gitea Actions → act_runner on unRaid
→ nginx container → Cloudflare tunnel → Cloudflare CDN → blog.hoelee.com
```
- **Repo:** `git.hoelee.com/hoelee/hoelee-blog` (push here first, then GitHub — standing convention).
- **Runner:** `act_runner` on unRaid (verified working: Gitea 1.27.3 + runner 3.3.2, PAT-secret pipeline).
---
## Write & publish a post
1. Create `src/content/posts/<slug>.md` with frontmatter (see `content-guide.md` §6).
2. Push to `main`.
3. CI builds `dist/` and deploys to the nginx container automatically.
- **Before pushing a post that adds art**, generate its in-page derivatives:
`uv run --with pillow python scripts/optimize-images.py <slug>`
(banner `.webp` + `/og/thumb/<name>-{400,800}.webp`). The PNGs from
`banner-gen` / `og-gen` remain the `og:image` source of truth — see
`design-guide.md` §3 for the format policy and measured sizes.
4. Verify live: `curl -I https://blog.hoelee.com/<slug>` → expect 200.
---
## Build pitfalls (from prior Astro work — check these first)
- **No JSX helper components in frontmatter** — inline JSX directly in `.map()` calls, or the build fails with `Expected ">" but found "class"`.
- **Import depth** — `src/pages/*.astro` use `../layouts/…`; subdirs use `../../`. Wrong level = `Could not resolve`.
- **Footer must render AFTER `<main>`** — a shared header+footer component renders footer above content otherwise.
- **`npm install` scripts blocked** can leave esbuild's binary missing → check `npm warn install-scripts` at install time.
---
## Verify before shipping a layout change
Headless-Chrome screenshot + vision check on **both** 390px and 1280px:
```bash
CHROME="/c/Program Files/Google/Chrome/Application/chrome.exe"
"$CHROME" --headless --disable-gpu --window-size=1280,2400 \
--screenshot="$(cygpath -w $LOCALAPPDATA/Temp/blog.png)" \
--virtual-time-budget=4000 "http://localhost:4321/"
```
Overflow diagnosis (real numbers, not screenshot guessing): check `scrollWidth > innerWidth` via CDP `Runtime.evaluate`.
---
## Hosting notes
- Static output = trivial to serve from any nginx/OpenLiteSpeed dir. Cloudflare in front gives CDN + HTTPS + DDoS.
- Cloudflare Pages **git integration supports GitHub/GitLab only — not Gitea**. Current setup (Gitea → self-hosted runner → nginx) sidesteps this entirely and is the reason the pipeline is the way it is.
- Full self-hosting also avoids Cloudflare Pages' 25MB/file, 20k-files limits — a decision already baked into this architecture.
---
## Health checks
- `curl -I https://blog.hoelee.com/` → 200, correct `content-type`.
- `curl https://blog.hoelee.com/sitemap.xml` → lists all published posts.
- `curl https://blog.hoelee.com/rss.xml` → non-empty.
- DNS: `blog.hoelee.com` resolves through Cloudflare (proxy enabled).
## Pitfall: premature poll caches a 16-day 404
The origin serves `Cache-Control: max-age=1382400` (16 days), and Cloudflare caches
negative (404) responses too. If you curl a freshly-deployed URL **before the CI deploy
finishes**, Cloudflare caches that 404 for 16 days — the page itself returns 200 but its
`og`/`banner` PNGs 404 with `cf-cache-status: HIT`.
- **Detect:** the bare URL 404s but `?v=<timestamp>` (cache-buster) returns 200 → origin is
fine, edge cache is stale.
- **Fix:** purge the Cloudflare zone cache. Zone-level token (`Zone.Cache Purge` perm, lives
in the cloudflare-pages-deploy skill) → `POST /zones/8c8b2359766ac853602b91dd851c630e/purge_cache`
with `{"purge_everything":true}`. See that skill for the exact curl.
- **Prevent:** after `git push`, `sleep` ~60–90s before the first bare-URL check, or validate
with `?cb=$(date +%s)` first so you never register a 404 into edge cache.