Skip to content
Ben Pritchard
← All work

benpritchard.dev

This site: a portfolio and interactive CV with an AI assistant that answers questions about my work.

Year
2026
Role
Solo — design, AI-assisted build, deployment
Status
In development
Stack
  • Next.js
  • React
  • TypeScript
  • Tailwind CSS
  • MDX
  • Zod
  • Anthropic API
  • Upstash Redis
  • Docker
  • GitHub Actions
  • nginx
benpritchard.dev: cover image

The problem

A CV lists skills. A portfolio should prove them. I wanted a site that answers one question for anyone reading it, whether a client, a recruiter or a technical lead: can this person build serious software? That means the site itself has to be part of the evidence. It needs to be fast, accessible and carefully built, and every claim on it needs to trace back to real work.

Constraints

  • Self-hosted. It runs on my own DigitalOcean droplet behind nginx, deployed from CI, with no platform-specific features.
  • Content separate from code. All copy lives in data files and MDX, so adding a project or a job never means touching a component.
  • An AI feature that can't run up a bill or make things up. The chat assistant is public, so it needs hard limits on cost and abuse, and it must never invent facts about me.
  • A high quality bar. The targets are Lighthouse 95+ for performance on mobile and 100 for accessibility, best practices and SEO; WCAG 2.2 AA in both themes; and a site that still works without JavaScript.

How I used AI

I built this with Claude Code as my pair programmer, working from a written specification I wrote first: the design tokens, every page and section, the chat's behaviour and its limits, and the acceptance criteria for each build phase. My job was the spec, the design decisions and reviewing every change against them, and the AI handled much of the typing. Where something fell short, such as a layout shift or a slow interaction, I measured it, decided on the fix and checked it landed.

Approach and architecture

The site is a Next.js App Router project, almost entirely React Server Components and prerendered static pages. It builds as a standalone output in a multi-stage Docker image. GitHub Actions runs lint, typecheck, tests and the build, pushes an image tagged with the git commit, and deploys it to the droplet, so rolling back is one command. A separate CI job runs Lighthouse on consistent hardware.

Projects are MDX files with frontmatter validated by Zod at build time. An unknown skill or a missing field fails the build. The skills section on the home page is derived from project frontmatter, never written by hand, so every skill shown links to the work that used it.

Key decisions

No RAG for the chat assistant

The usual answer for "chat with my documents" is retrieval-augmented generation: embeddings, a vector store and a retrieval step. The knowledge here is about 4,000 words, which fits comfortably in a single prompt. So a build step compiles my CV, an about page and every case study into one knowledge file, which becomes the system prompt and is cached by the provider. That means no retrieval that can miss the relevant passage, no vector database to run, and lower cost per message. The trade-off is that it only works while the knowledge base stays small, and that's a limit I'm happy to revisit if it ever grows.

The system prompt restricts the assistant to my skills, experience, projects and availability, tells it to say plainly when something isn't in the knowledge base, and forbids quoting prices or committing me to anything.

Cost and abuse controls on every layer

Because the endpoint is public, every limit is enforced on the server, not just in the UI:

  • 500 characters per message, 8 user turns per conversation, and 400 output tokens per reply
  • 10 messages per hour and 30 per day per IP address, using Upstash sliding windows, with the client IP taken only from headers set by nginx
  • A global daily message cap and a kill switch, both of which fall back to static answers
  • Cloudflare Turnstile on the first message of each session
  • Suggested questions served from pre-generated, reviewed answers, with no model call
  • In production, a missing rate-limit backend makes the endpoint fail closed rather than run unlimited
  • Only aggregate counts are logged, never message contents

Each limit and the kill switch is covered by tests.

Performance as a feature

A few measured fixes made the biggest difference:

  • Scroll reveals. Animating about 20 elements, each with its own animation component, was the largest cost to the page becoming interactive. Moving to one shared observer that toggles a CSS class removed that hydration work, and the content is fully visible without JavaScript.
  • The typed headline. The hero types and deletes phrases. Moving the text beneath it through layout scored about 0.05 cumulative layout shift per change. Locking the box's height and sliding the text with a CSS transform instead brought that to about zero.
  • The bundle. The command palette and the diagram renderer load only when first needed, and Zod schemas stay in server-only modules, out of the browser bundle.

Outcome

Automated tests
53
Model calls for suggested questions
0
  • A content system where adding a project means adding one MDX file, and the build rejects invalid data
  • A chat assistant with every cost and abuse limit enforced on the server and tested
  • Light and dark themes that follow the system setting, with a manual toggle, AA contrast and keyboard navigation throughout
  • A deployment pipeline that builds in CI, deploys by image tag and rolls back in one command

What I'd do differently

  • Measure on the live site sooner. Local Lighthouse runs on Windows are noisy. Running the CI Lighthouse job from day one would have given a steadier baseline for every change.
  • Write the chat evaluation set first. A fixed set of tricky questions, such as off-topic requests, prompting it to invent facts, and price questions, is the real specification for the assistant's behaviour. Writing it before the system prompt would have shaped the prompt better.