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.
5.1 KiB
Design Guide — what the blog UI must have & must avoid
Read this before touching theme, layout, typography, color, or SEO markup. Priority order is fixed: reading experience → SEO → brand → everything else.
1. Non-negotiables (reading experience first)
| Rule | Spec |
|---|---|
| Measure (line length) | 65–75 characters per line. Single biggest readability win. |
| Body typography | 16–17px, line-height: 1.6–1.7, a clean sans (Inter / system-ui / Source Sans). |
| Headings | A character font (Plus Jakarta Sans / Space Grotesk) or keep Inter for minimalism. |
| Code | Proper monospace (JetBrains Mono / Fira Code) + distinct background block + syntax highlighting + copy button + language label. Non-negotiable for technical posts. |
| Light + dark mode | Both. Dev audience splits ~50/50. |
| Color | 2-accent palette (primary + semantic green/red for correct/wrong in tutorials). Avoid pure black / pure white. |
2. Required components
- Sticky header — name/logo + nav (Blog / Categories / About / RSS).
- Article header — title, pub date, reading time, category + tags, author card (photo, 1–2 line bio, LinkedIn + GitHub + git.hoelee.com links).
- Author box / ProfilePage schema on every post (see SEO below).
- Related posts (3 cards) — internal linking + keeps readers on-site.
- RSS link in footer — how the technical audience follows me.
- Custom og:image per post — 1200×630, never the reused logo.
3. Responsive & media
-
Mobile-first: design 390px, then desktop 1280px.
-
Astro Image for automatic optimization/WebP (Core Web Vitals win).
-
Image format policy (measured 2026-10-16 — do not guess this one):
Asset Served as Why og:image/twitter:imagePNG (1200×630) Social scrapers (WhatsApp / LinkedIn / FB / Telegram) do not reliably render WebP share cards. Compatibility beats bytes here. Article banner in-page WebP q78 + PNG fallback via <picture>6.02 MB → 2.52 MB across 70 banners (42%); JPEG would be larger (q85 = 129% of PNG) because the art is flat terminal-style with sharp text edges. List/related thumbnails WebP q72 at 400w + 800w ( /og/thumb/<name>-400.webp)Rows display at ~398px; they used to fetch the 1200×630 PNG (~45 KB). 400w is 5.8 KB, 800w is 16.1 KB. The homepage went from ~450 KB of thumbnails to ~62 KB. Real photos (author, hardware) WebP q82 + JPEG fallback Continuous-tone images are the opposite case from the flat art above. Regenerate all derivatives after adding a post:
uv run --with pillow python scripts/optimize-images.py. -
Declare
og:image:width/heightexplicitly — mis-declared dimensions get preview cards rejected by WhatsApp/Telegram/LinkedIn. -
Favicon: reference all sizes (
.ico+ 32×32 + 192×192 PNG + apple-touch 180×180). "Favicon not showing" is usually browser cache, not a missing asset —curlthe live files before "fixing".
4. Theme / stack decisions (settled)
- Astro 5 static (already scaffolded). Rationale: TypeScript/React skill surface, true static output, low attack surface, first-class i18n routing, and the repo itself is a portfolio artifact.
- Theme base: AstroPaper-style minimalism (light/dark, fuzzy search, RSS + sitemap defaults). Don't rebuild from scratch.
- Skip for v1: WordPress-headless (two systems), Next.js SSR/ISR (heavier than needed), self-built blog engine (the classic time sink).
5. SEO checklist (2026 — two lanes)
Optimize for both classic SEO and GEO (getting cited by Google AI Overviews / ChatGPT Search / Perplexity).
- E-E-A-T first: real
Person/ProfilePageschema around the author bio, withsameAs→ LinkedIn, GitHub, git.hoelee.com. - Per-post meta: unique title (≤60 chars), description (≤155), canonical, og:image (1200×630) +
twitter:card. - Sitemap.xml + RSS + robots.txt — generated automatically.
- Core Web Vitals: static site → 90+ LCP/CLS/INP easy. Verify with headless-Chrome screenshot + Lighthouse on 390px and 1280px.
- Internal linking: related-posts module + link pillar posts (case studies) from every tutorial.
- Question-shaped headings: write H2/H3 as the actual question a learner types. Rewarded by Google and AI engines.
- Name consistency: same name ("Lee Teong Hoe" / "Mr Hoelee") + photo + handles across blog, LinkedIn, GitHub, git.hoelee.com. Fragmented identity is the #1 personal-brand SEO killer.
6. What to AVOID in design
| Anti-pattern | Why |
|---|---|
| Heavy JS / client-side rendering | Static output is the point — don't reintroduce runtime cost. |
| Reused logo as og:image | Already an issue on hoelee.com. Custom per post. |
| Anonymous/absent author card | Kills E-E-A-T. |
| Wall-of-text without code blocks | Technical posts need formatted, copyable code. |
| No RSS / sitemap | Both are free in Astro; omitting them is pure loss. |
| Orphaned pages / thin content | hoelee.com already has ~400; keep this sitemap clean. |
| Over-styled "agency look" | This is a personal engineering blog — credibility over flash. |