Personal Tech Hub
Bilingual personal tech brand site with Pagefind search, Giscus comments, dark mode & RSS — auto-fetches GitHub Stars at build time
Situation
Needed a modern personal tech brand site that could showcase open-source projects, in-depth articles, and quick notes — all with bilingual support. The previous Hugo setup lacked TypeScript type safety and component-driven iteration.
Task
Build a performant, bilingual, modern personal tech site with:
- Project showcase, blog, and notes sections
- Chinese/English routes with content linking
- Static search, dark mode, RSS, and GitHub activity display
- Automated CI/CD deployment to GitHub Pages
Action
- Framework: Migrated from Hugo to Astro 6.x, using Content Collections for bilingual content management and Tailwind CSS 4.x for theming
- Content Architecture:
translationKeylinking zh↔en pairs; four content collections (blog/projects/notes/pages) - Integrations: Pagefind for static search, Giscus for comments, GitHub REST/GraphQL API for star counts and contribution graph
- CI/CD: GitHub Actions builds the site, generates OG images, indexes Pagefind, and deploys to GitHub Pages
- Key decisions:
- Astro over Hugo → TypeScript + component model for faster UI iteration
- CSS variable theming → zero-JS dark mode, WCAG AA compliant contrast
Result
- ✅ Bilingual site live with Lighthouse 95+ performance score, 71 pages indexed
- ✅ RSS, full-text search, dark mode, GitHub activity chart all functional
- ✅ Build time <30s (including OG image generation), FCP <1s
- 🔜 Roadmap: Plausible analytics, resume download, email newsletter
Technical Documentation
Technical Challenges Why this project? What unique problem does it solve?
Core Challenge: Keeping AI alive on a grid with accumulating obstacles
1. Corner Food Deadlock
Food lands in dead zones (surrounded by body + obstacles). AI's BFS finds a path, but the survival check says “I’ll get trapped after eating” → skip → tail-chase → food stays put → infinite loop.
Solved by: Adding a “reachable space ≥ body length” weak guarantee alongside the “can reach tail” strong guarantee. Stall deaths: 50% → 3.3%.
2. Unbounded Obstacle Growth
One permanent obstacle per level means available space shrinks linearly. At ~90 segments, 53% of games end via obstacle collision.
Solved by: Soft cap (30 obstacles, FIFO eviction) + stage milestones that clear all obstacles.
3. Batch Stats Without UI Freeze
Turbo mode runs 2000+ sync BFS steps per game. 200 consecutive games freezes the page.
Solved by: await setTimeout(0) per game to yield the main thread, plus onProgress callback for live updates. Turbo skips rendering, particles, DOM writes, and audio — single game: 10s → 0.3s.
4. State Boundaries in Single-File Architecture
Game, AI, and UI layers all live in one IIFE closure. Batch stats need logic-only execution.
Solved by: window.__game exposes read-only state + diagnostics API. ai.turbo flag controls loop branching (normal rAF+render vs sync while-loop). muted flag silences audio.
Solution Comparison & Decisions Trade-offs and rationale behind key technical choices
Key Trade-offs & Decisions
1. Survival Check: Single vs Dual Criterion
Option A: BFS to tail only (strong guarantee) → always false for corner food → deadlock.
Option B: Reachable space ≥ length only (weak) → occasionally wrong but alive.
Chosen: Dual OR combination — strong first, weak as fallback. Keeps reliability while covering dead corners. Verified: stall 50% → 3%.
2. Tail Chase: Shortest vs Longest Path
Shortest: BFS head→tail step 1, tight circles, body layout static, food safety never changes → stuck.
Longest: Flood distance map from tail, head picks furthest valid neighbor, wide loops, body layout fully evolves → food safety window opens.
Chosen: Switched to longest survival path. Max score: 920 → 1990 (+116%).
3. Obstacle Handling: Infinite vs Hard-Delete vs Soft Cap
Infinite: Grid eventually fills — mathematical inevitability.
Hard delete (clear all): Late-game suddenly easy, jarring experience.
Soft cap (30, FIFO replace): Maintains tension with a ceiling.
Chosen: Soft cap + stage clear (every +50 segments). Alternates tension and payoff.
4. Power-up AI: Target vs Passively Eat
Target: AI routes to power-ups, adds computation, needs cost-benefit analysis.
Passive: Power-ups spawn randomly, AI eats them naturally in motion, zero extra computation.
Chosen: Default “passive eat”. chaseBonus toggle available for golden bonus targeting.
Architecture Design System architecture, data flow, and core design patterns
Single-File Three-Layer Architecture + Turbo Dual Mode
┌─────────────────────────────────────────────┐ │ snake.html │ ├─────────────────┬───────────────────────────┤ │ Render Layer │ rAF loop → Canvas 2D │ │ (visual) │ Body lerp / particles / │ │ │ countdown rings / HUD │ ├─────────────────┼───────────────────────────┤ │ Game Logic │ step() / reset() / spawn │ │ (game logic) │ Collision / food / power │ │ │ Stages / levels / double │ ├─────────────────┼───────────────────────────┤ │ AI Decision │ aiDecide() 5-tier cascade │ │ (ai core) │ ①→②→③→④→⑤ │ │ │ BFS / simulate / survive / │ │ │ longest path / flood fill │ ├─────────────────┴───────────────────────────┤ │ window.__game API │ │ Turbo: skip render → sync while → batch stats│ │ Visible: rAF + render → watch AI decide │ └─────────────────────────────────────────────┘
Data Flow (per logic tick)
aiDecide() → applyAiDir() → dir update → step() collision/eat/stage → updateHUD() → next frame
Turbo Mode State Machine
Normal: state=playing + ai.turbo=false → rAF loop advances 1 step/frame + render
Turbo: batchRun() → sets ai.turbo=true + muted=true → runOneGame() sync while(state==playing) turboStep() → 0.3s/game → await setTimeout(0) yield
💡 Tip: Click each panel to expand. Uses native HTML5 <details> elements — no JavaScript needed.