Jul 19, 2026 · personal · in progress
Terminal Portfolio
The platform behind this site — a single Next.js app with git-based MDX content, a scanline "membrane" home that morphs as you scroll, and an AI copilot grounded in the site's own content through a three-layer answering system.
> outcome
- Replaced ~6,400 lines of copilot server code with roughly a tenth of it, answering more questions correctly.
- Ships through a gated pipeline — audit, lint, types, tests, build — to an immutable image with SHA-tagged rollback.
- The copilot is graded by an LLM judge against adversarial cases mined from real visitor questions.
role Everything — product, design, architecture, build, and ops
- TypeScript
- Next.js
- Keystatic
- Tailwind CSS
- AI SDK
- Docker
- GitHub Actions
- Umami Analytics
> details
This site is its own case study. The current generation is a ground-up rebuild of an earlier two-app CMS platform, redesigned around one editorial principle: the content is the product, and everything else — including the AI — exists to serve the reader.
Highlights
- Rebuilt a two-app Next.js + Payload CMS platform into a single Next.js app with git-based MDX content through Keystatic — one deployment, no database, every content change a reviewable commit.
- Designed the "membrane" home: a canvas of displaced scanlines rendering a heightfield that morphs between scenes as you scroll — a face for the intro, cogs for the work, a writing page for the journal, a speech bubble for contact — whose face blinks, mouths answers as they stream, and (opt-in) speaks them aloud through browser speech synthesis.
- Built the copilot as a first-class product feature: model-driven tool calling over deterministic in-process filters, grounded citations and visible tool-call traces derived from data rather than model claims, and page-aware conversations that know what you are reading — including the 404 and error pages.
- Exposed the copilot's tools over the Model Context Protocol at
/mcp, so agents can query the portfolio as typed tool calls — one content door served three ways: HTML for humans,llms.txtfor crawlers, MCP for agents. - Added an offline AI enrichment pipeline that distills every project, article, and work experience into structured evidence cards — fingerprinted against their sources, reviewed through git diffs, with human overrides that always win.
- Closed an evaluation flywheel: an LLM judge grades answers against the corpus through the real production pipeline (its first run caught a real guardrail leak), while visitor questions and 👍/👎 ratings are mined into new eval cases.
- Kept cost control as a design pillar: signed visitor identity, per-visitor and global quotas with reservation and refund, burst limiting, and a first-turn answer cache.
- Shipped with a push-to-deploy pipeline — GitHub Actions builds a standalone Docker image, publishes to GHCR, and triggers Coolify — plus self-hosted Umami analytics with copilot usage events.
Architecture
The app is a single Next.js App Router codebase. Content lives in the repository as MDX and YAML, modeled and edited through Keystatic — locally in development, through a GitHub-backed admin in production. Every page that can be static is static; the only dynamic surface is the copilot's API route.
A deliberate structural rule keeps the codebase small: lib/content is the only door to content. Pages, feeds, the copilot's context assembly, and the enrichment script all consume the same typed accessors, so a schema change propagates everywhere or nowhere.
The Membrane
The home page splits in two: a sticky canvas pane and a scrolling column of full-viewport slides. The canvas renders horizontal scanlines displaced by a heightfield — the effect of shapes pressing through stretched cloth. Each slide registers a scene; an intersection observer morphs the field as you scroll. The face blinks on its own; its mouth moves while the copilot streams an answer, and keeps moving while an opt-in voice reads that answer aloud — both surfaces driven by one talking = streaming || speaking signal.
Through this rendering, only silhouettes and height profiles survive — a lesson learned the hard way. Isometric cubes collapse into hexagons; thin pens vanish entirely. Every scene had to be redesigned as a literal, silhouette-strong metaphor.
The Copilot
Clicking the face — or the >_ ask chip anywhere on the site — wakes the copilot: the same entity from the membrane, now with a literal voice. It answers from a three-layer system: structured human-authored fields for facts, AI-enriched evidence cards for interpretation, and capped tool calls for depth. Conversations are grounded in the page being read, down to selecting a passage and asking about it — and that includes the dead ends: on the 404 page the copilot knows which path failed and helps find what was meant, and on the error page it knows the render broke and helps the visitor continue.
Those same deterministic tools have a second consumer. They are exposed over the Model Context Protocol at /mcp, so an agent — a recruiter's assistant, say — can query the portfolio as typed tool calls and receive the exact corpus the copilot grounds on. The site is readable three ways: HTML for people, llms.txt for crawlers, MCP for agents.
Answer quality is not judged by feel. An LLM judge grades every answer against the site's content as ground truth, through the exact production pipeline — because paraphrase should pass and fabrication should fail. And because I should not be the one deciding what to test, a feedback flywheel mines real visitor questions and their 👍/👎 ratings into new evaluation cases.
The full design — why there is no vector database, how the enrichment pipeline works, the agent endpoint, the eval flywheel, and what broke along the way — is covered in the companion article below.
Delivery
Pushing to main runs lint and the unit test suite, warns on stale enrichment cards, builds a standalone Docker image, publishes it to GHCR, and triggers the deployment behind a container health check. Runtime configuration is three environment variables: one OpenAI-compatible base URL, one key, one model — provider resilience belongs to a gateway, not to this app.
Production hardening is part of the product: a strict Content-Security-Policy with a full security-header set, JSON-LD structured data, per-page Open Graph images rendered in the membrane's scanline style, a sitemap and RSS feed, an llms.txt index and an MCP endpoint so agents can read the site the same way the copilot does, and a 404 page where the membrane face offers to help find the path that didn't resolve.