Skip to content
Cx330xu

Personal Tech Hub

Bilingual personal tech brand site with Pagefind search, Giscus comments, dark mode & RSS — auto-fetches GitHub Stars at build time

Active
AstroTypeScriptWeb

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: translationKey linking 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=truerunOneGame() 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.

Comments