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.
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.
Stack
Next.js 16
·React 19
·TypeScript
·Tailwind CSS
·PostgreSQL + pgvector
·Docker
·Playwright
Category
Full-stack
Why This Exists
Playground & production
Not a template
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.
Over-engineered on purpose
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
By the numbers
Pages
17
API Routes
17
Test Files
63
Languages
2
WCAG Level
AAA
i18n Keys
565
Across the Stack
Seven dimensions
FE Frontend
Interface & motion
Next.js 16
·React 19
·TypeScript
·Tailwind CSS 4
·Framer Motion 12
·next-intl · 565×2 keys
BE Backend
APIs & contracts
17 API routes
·RFC 9457 errors
·SSE streaming
·rate limits + quotas
·OpenAPI spec
DB Database
Persistence
PostgreSQL 18 · self-hosted
·11 tables
·pgvector(1024)
·_zh i18n columns
·nightly pg_dump
AI Ask
RAG over the site
LiteLLM gateway
·voyage-3.5-lite · 1024-dim
·pgvector · top-5 cosine
·forced [n] citations
·live pipeline panel
UI Design
System & a11y
OKLCH color system
·WCAG AAA · 13:1+
·3 themes
·Cormorant · Geist · JetBrains Mono
QA Testing
Quality gates
Playwright E2E · 4 shards
·Vitest unit suite
·axe-core a11y
·Lighthouse CI
OPS DevOps
The layer that ships everything
Docker
·Dokploy
·Traefik
·GHCR image
·Cloudflare R2
GitHub Actions
lint
→typecheck
→build
→test
→e2e ×4
→health-gated deploy
See It Live
Desktop · Mobile
● ● ●misoto22.com/ask
Desktop — /ask demo room, live retrieval pipeline
Mobile — responsive layout
How Ask Works
RAG pipeline
The site answers questions about me from its own content, and every guardrail — per-IP rate limit, input validation, a global daily quota — runs before any paid call.
1
CorpusBlog MDX · portfolio DB · profile facts
2
ChunkHeading-based · 300–800 tokens
3
EmbedVoyage 3.5-lite · 1024 dims
4
Storepgvector — same Postgres as the site
5
RetrieveTop-5 cosine similarity · 0.2 floor
6
GroundNumbered excerpts · forced [n] citations
7
GenerateSelf-hosted LiteLLM gateway · SSE stream
8
AnswerReasoning filter · live citations panel
What It Does
Three pillars
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
BlogMDX + syntax highlighting · slug-based categories, subcategories, tags · archive · TOC with active state · share button
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.
Live site
Want the full walkthrough?
Happy to talk through the architecture, the decisions, and the trade-offs behind this build.