Skip to content
Projects

This Website

A personal data platform that doubles as the site you are reading — projects, video, and training data in one place.

Status
Building
Category
Web
Started
Sep 1, 2026
Timeline
Sep 1 – now
  • Next.js
  • TypeScript
  • React
  • Tailwind CSS
  • MDX
Private repository

Overview

This site is a record of three things I actually do: build software, publish video, and train. Most personal sites are a résumé with a nicer font. I wanted something closer to a dashboard — a place where the data updates because I am still doing the thing, not because I remembered to update a portfolio.

It is deliberately built in the open, and it is its own first case study.

Why I Built It

I had the usual scattering. Projects lived in GitHub repos with thin READMEs. Video lived on a YouTube channel with no context around it. Training numbers lived in a notes app and a scale app that do not talk to each other. None of it added up to a view of what I was actually working on.

The annoyance that started it: I could not answer "what have you been doing for the last three months?" without opening four apps and doing arithmetic.

The Idea

One site, three feeds, one changelog.

The key decision was to treat the site as a read surface over structured data rather than a set of hand-written pages. Every number on the homepage comes from a typed record somewhere. If I want the homepage to say something new, I add data, not markup.

The second decision was a hard split between what I log and what I publish. Training data is the obvious case — I want to log everything and publish almost none of it.

How It Works

Three layers, and the boundary between them is the interesting part.

content/ + data/          typed records — MDX frontmatter, TS modules
       ↓
lib/fitness/publish.ts    private → public projection (aggregate, drop, round)
       ↓
lib/data/*.ts             the only modules a page may import
       ↓
app/**                    server components; near-zero client JavaScript

The private log is marked server-only, so importing it from a client component fails the build rather than shipping a weigh-in to the browser. The public projection functions take private records and return public types, and the public types have nowhere to put a note or a meal, so a field added to a private record cannot leak by accident.

Pages are server components by default. Client components exist in exactly three places: the theme toggle, the active-navigation underline, and the range control on the weight chart.

The Build

Content. Project write-ups are MDX with typed frontmatter, parsed at build time. Editing a project means editing a file in the repo, which is the only workflow I will reliably keep up with.

Charts. Hand-written SVG, rendered on the server. A line chart and a bar chart, roughly two hundred lines between them. No charting library, no client bundle, and they inherit theme colours from CSS variables so dark mode is correct rather than inverted.

Fitness data. Currently a local module shaped exactly like the future database tables. The read API is already async, so moving to Postgres changes the imports in one file. Datasets that are empty report as empty and their sections do not render, so the site reads correctly whether a given dataset has records or not — there is no flag to keep in sync.

Problems I Ran Into

The privacy boundary had to be structural. My first pass had a single fitness module with a visibility flag on each record and filtering at the page level. That works right up until the day I forget one filter. Moving to two separate type families — where the public type physically cannot carry a note — took an afternoon and removed the entire class of mistake.

Charts were the wrong place to reach for a library. I started with a charting library, and the weight chart alone pulled in a client bundle larger than the rest of the page. For five chart types with fixed requirements, writing the SVG directly was less code than configuring the library away from its defaults.

Smoothing hid what I wanted to see. A centred moving average made the weight trend beautiful and slightly dishonest: the most recent point kept moving after the fact. I switched to a trailing average so today's number stops changing tomorrow — and then, once real weigh-ins went in, turned smoothing off below a density threshold entirely. Over a weekly log a seven-point average spans nearly two months and draws a trend that was never measured.

Even spacing lied about a gap. The first chart placed points by index, so a twelve-week gap between the May baseline and the August weigh-ins looked identical to a one-week gap. Positioning by date instead made the gap visible, which is the honest picture: those are weeks I did not record.

One rowing line was two. Plotting every 20-minute piece on a single split chart made the trend look erratic — until I noticed the sawtooth was just Wednesdays and Fridays alternating. Interval sessions drop to low output between efforts, so their average split is slower by design. They are two series, and now they are charted as two.

What I Learned

Structuring the data first made the design decisions easy. Once "current weight" was a field on a typed summary rather than a string in a component, the question stopped being "what should this card say" and became "what does this number mean", which is a much better question to be designing around.

Also: the amount of client JavaScript a site needs is usually a design decision, not a technical one.

What I Would Change

I would define the public projection types before writing a single component. I built two of the fitness pages first and then had to walk the types backwards out of the markup.

Current Status

Built and running: homepage, projects, video, the fitness dashboard, /now and /about, all server-rendered, both themes, no horizontal overflow down to 320px, and no accessibility violations under axe.

The second pass made it content-ready rather than template-shaped — one source of truth for identity, a Node LTS pin, per-dataset example fallback, and a visual split between what is measured and what is a target.

Next is the authenticated dashboard — logging weight, nutrition and workouts into Postgres and letting the public pages read the published projection from the same source. After that, pulling video metadata from the YouTube API so the video page stops being a file I edit by hand.