RVM Est. 2024

Writing · 3rd August 2026 · 2 min

How this site works, and what I threw away

Notes on porting a PHP site to FastAPI, and on deleting most of it.

The previous version of this site was PHP. It worked, in the sense that a bicycle with three wheels works. Every request re-parsed a 68 KB array of blog posts and a 50 KB array of gallery items from disk — whether or not the page being rendered had anything to do with blogs or photographs — and then shipped jQuery, Bootstrap, AOS, particles.js, Typed.js, SweetAlert2, Konami.js, Font Awesome, animate.css, hover.css and, on the home page, all of three.js.

Two of those libraries were loaded twice.

What was actually wrong

The Telegram bot token was in the page source. The contact form worked by printing a <script> block containing a live bot token and chat ID, then asking the visitor’s browser to call api.telegram.org. Anyone who pressed Ctrl+U owned the bot. That is the entire story. It is fixed now: the token lives in an environment variable on the server, the browser never sees it, and the message is written to a database before any notification is attempted, so a downed Telegram never loses a message.

Nothing was cacheable. No ETags, no Cache-Control, no image dimensions, so every scroll reflowed. The gallery served full-resolution images into thumbnail-sized slots.

There was no such thing as a draft. If it was in the data file, it was live.

What replaced it

A FastAPI application, strictly type-hinted, with Pydantic v2 at every boundary and SQLAlchemy 2.0’s typed ORM underneath. Pages are rendered server-side with Jinja, which means the first paint contains the actual content instead of a spinner and a promise. The same data is available as JSON at /api/v1 for anything that wants it.

The front end has no framework. No Tailwind build, no bundler, no npm. One hand-written stylesheet of custom properties and one small module of vanilla JavaScript, both cached forever behind a content hash. The whole client payload is smaller than the old site’s icon font.

Scroll animation is IntersectionObserver, and parallax is a single requestAnimationFrame loop writing CSS custom properties. If you have prefers-reduced-motion set, none of it runs.

The parts that are jokes

The coffee counter is a formula, not a log. round(0.571 × days + 37). Nobody was ever going to log every cup, and an estimate that admits to being an estimate is more honest than a number that pretends otherwise.

The sleep and screen-time charts, on the other hand, are real. They are read straight out of the journal database I have filled in most nights since September 2024, opened read-only so this site can never write to it.

There are easter eggs. There were easter eggs before. I am not going to tell you where they are, but the console is a reasonable place to start looking, and the old arrow-key incantation still does something.

Stack, for the curious

Python 3.11, FastAPI, SQLAlchemy 2.0, Pydantic v2, Jinja2, SQLite in WAL mode, Pillow for the image pipeline, and nothing else. It runs on a single process and would be comfortable on a Raspberry Pi — which, given the rest of this site, seems like the correct place for it to end up.

Written 3 Aug 2026 · revised 24 Aug 2026

Tell me I am wrong →