Personal Website
A bilingual developer portfolio that treats personal infrastructure as seriously as production — a self-hosted Docker stack, an AI assistant grounded in the site's own content, WCAG AAA accessibility, and a CI pipeline running 222 E2E tests plus Lighthouse audits.
- Role
- Solo build · design → deploy
- Year
- 2025
- Type
- Portfolio · Web + AI
- Status
- Live

A portfolio that treats personal infrastructure as seriously as production.
17 pages, 17 API routes, and an AI assistant that answers questions about me from the site's own content — all solo-built, bilingual, and running on my own server. A blog, a photography gallery, privacy-first analytics, and WCAG AAA accessibility, shipped through a health-gated deploy pipeline.
Why This Exists
Built from scratch as a playground for learning Next.js App Router, server components, and i18n — then it kept growing into a self-hosted platform with its own deploy pipeline and an AI assistant. It is also the production site that represents me to employers.
A RAG assistant with a visible retrieval pipeline, self-hosted analytics, E2E tests for every page, axe-core accessibility audits, Lighthouse CI, health-gated deploys — because the site itself is the portfolio piece.
At a Glance
Across the Stack
Frontend
Interface & motionBackend
APIs & contractsDatabase
PersistenceAsk
RAG over the siteDesign
System & a11yTesting
Quality gatesDevOps
07 · The layer that ships everythingSee It Live


How Ask Works
The site answers questions about me, grounded in its own content. An indexing script chunks blog posts, project / experience / education data, and profile facts along headings into 300–800-token pieces, embeds them with Voyage 3.5-lite, and stores the vectors in the same Postgres that serves the site. At question time every guardrail runs before any paid API call — per-IP rate limit, input validation, a global daily quota — then the question is embedded, the top 5 chunks are retrieved by cosine similarity, and a grounding prompt forces the model to answer only from numbered excerpts with [n] citations, streamed token-by-token over SSE.
Corpus
Blog MDX · portfolio DB · profile facts
Chunk
Heading-based · 300–800 tokens
Embed
Voyage 3.5-lite · 1024 dims
Store
pgvector — same Postgres as the site
Retrieve
Top-5 cosine similarity · 0.2 floor
Ground
Numbered excerpts · forced [n] citations
Generate
Self-hosted LiteLLM gateway · SSE stream
Answer
Reasoning filter · live citations panel
What It Does
A full-stack portfolio with a blog, photography gallery, project showcases, an AI assistant grounded in the site's own content, privacy-first analytics, and production-grade infrastructure — all bilingual.
Content
- Blog
- Photography
- Project Showcases
AI & UX
- Ask — AI Site Q&A
- Command Palette (Cmd+K)
- Themes & Keyboard
Infrastructure
- Privacy-First Analytics
- Security
- SEO
Trade-offs
Self-hosted over Vercel + Supabase
The site started on Vercel and Supabase, then deliberately migrated to a self-hosted stack: Dokploy + Traefik on my own VPS, Postgres in the same compose file, GHCR images, nightly pg_dump backups with 14-day retention. More to operate — but the deploy pipeline, backups, and caching behaviour become part of what the portfolio demonstrates.
pgvector in the app database, not a vector DB
The RAG corpus is ~70 chunks. A dedicated vector database would be pure overhead — pgvector keeps retrieval one SQL query away from the rest of the data, and at this size an exact cosine scan is sub-millisecond with 100% recall. No vector index until the corpus passes ~10k chunks.
An LLM gateway between site and model
The app calls a self-hosted LiteLLM proxy through one stable model alias instead of any provider directly. Swapping the upstream model is a gateway config edit, not an app redeploy; provider keys live in the gateway; and a streaming filter strips think-blocks, so reasoning models drop in without UI changes.
Retrieval threshold tuned by failure, not theory
The similarity floor sits at 0.2 because 0.35 dropped a real answer (a graduation question scored ~0.34) while off-topic noise scored ~0.39. The vector threshold only screens obvious noise — the grounding prompt, which forces [n] citations and allows "I don't know", is the real relevance gate.
WCAG AAA, not just AA
All color tokens are AAA compliant (13:1+ contrast ratios in both themes). The OkLCh color space keeps perceptual consistency across light/dark modes. Every page runs axe-core checks in the E2E suite — accessibility regressions fail the build.
_zh suffix columns over separate translation tables
With only 2 languages, a full translation table with foreign keys is overhead. Nullable _zh suffix columns on each table keep queries simple — one fetch, one locale check, no JOINs.
Want the full walkthrough?
Happy to talk through the architecture, the decisions, and the trade-offs behind this build.


