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.