johnfx.me portfolio screenshot

Overview

I wanted a personal portfolio that I actually owned end-to-end — no Squarespace, no static site generator, no framework I didn't fully understand. So I built one from scratch. The constraint I set for myself was: no build step, no magic, nothing I couldn't explain in a sentence.

The result is a ~50-line Express.js server. Content is read from disk on every request, so I can edit a Markdown file or update site.json and see it live immediately — no rebuild, no cache to bust, no restart. Every page template is a plain JavaScript template literal function. There's nothing between what I write and what gets served.

The design

The homepage sections are fully data-driven. Each section is defined in site.json with a pointer to a JavaScript module in src/sections/, and the server assembles the page by looping through the config and calling each renderer. Adding a new section to the site means creating one file and adding one line of JSON — nothing else changes.

src/sections/
├── hero.js         # Landing / intro
├── about.js        # Bio + photo
├── things.js       # Project cards
├── experience.js   # Skills
└── links.js        # Contact

I like this pattern because it keeps the concerns separated without any framework overhead.

Stack

Hosting & Infrastructure

I self-host on a virtual machine in Akamai (Linode) cloud. Each service runs in an isolated Docker container, with Traefik as the reverse proxy — handling TLS termination, routing, and automatic Let's Encrypt certificate renewal via Cloudflare DNS challenge. Cloudflare also sits in front as a proxy for DDoS protection and CDN distribution.

CI/CD is GitHub Actions: pushing to main builds a multi-platform image (amd64 + arm64), pushes to GHCR, then SSHes into the server to pull and restart.

Built with Claude Code

This project was also an experiment in working with Claude Code — figuring out where to be hands-off and where to stay in the loop. I found that the right balance is to let the agent handle implementation freely, but to stay deliberate about architecture: the decisions that determine how everything else fits together.

The data-driven section pattern is a good example. I pushed back until the agent landed on a design I was happy with — one that would stay maintainable and extensible without me touching code every time I want to change the page. That kind of guided architecture is where I think AI-augmented development gets interesting.

I also tried to build with an AI-first mentality from the start. The docs/adding-an-entry.md file is a set of instructions written for AI agents in other repos — so any Claude Code session that has access to a project and this portfolio can generate a ready-to-publish entry without any manual steps. Setting up the right foundations early makes the AI a lot more useful later.

Links