June 13, 2026
How I built this site
I wanted this site to stay simple enough that I could still upload it over FTP.
That was the constraint from the beginning. I spend a lot of my work life around cloud-based enterprise systems: AWS, Azure, CI/CD pipelines, managed services, identity layers, observability dashboards, and all the machinery that comes with building software at scale. That stuff is useful. It pays the bills. It is also not what I wanted for this site.
I wanted something that could live on an old-school Apache server. The kind of server that can run PHP and MySQL and has probably been quietly doing its job for years. No containers. No serverless functions. No deploy previews. No elaborate infrastructure. Just files on a server.
At the same time, I did not want to hand-code every repeated bit forever. I wanted posts, project cards, shared styles, metadata, and all the small conveniences that make a site pleasant to maintain. So I talked it through with AI and used it as a sounding board for the constraints.
The direction we landed on was Astro. It gives me components and a modern authoring workflow while still building down to static files. That means I can work locally with a real project structure, SCSS, reusable pieces, and generated pages, then ship the finished output like it is 2004: upload the files and let Apache serve them.
That tradeoff feels right for this place. It is modern enough to be easy to work on, but boring enough to host almost anywhere. The site can still have a little personality, a little structure, and a little automation, without turning into a cloud architecture diagram.
So that is the shape of it: Astro for building, static files for serving, FTP if I want it, and an old Apache/PHP/MySQL box still doing what it has always done.
Making one page dynamic again
- Astro builds every page to plain static HTML. There's no server-side Astro runtime on the host, just Apache and PHP.
- For the homepage, a matching
index.phpfile sits right next to Astro's builtindex.html. -
One line in
.htaccesstells Apache to prefer the PHP file when both exist:DirectoryIndex index.php index.html - Every other page has no PHP twin, so that rule just falls through to
index.htmlas normal. - The homepage's Astro template outputs literal placeholder tokens instead of real numbers, e.g.
{{VISIT_COUNT}}and{{BOT_COUNT}}. index.phpreads the built HTML off disk withfile_get_contents(), then runsstr_replace()to swap each token for the current value before echoing the page out.
SQL
CREATE TABLE visit_counter (
id TINYINT UNSIGNED NOT NULL AUTO_INCREMENT,
visitor_type ENUM('human', 'bot') NOT NULL,
hit_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uniq_visitor_type (visitor_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO visit_counter (visitor_type, hit_count) VALUES
('human', 0),
('bot', 0); - Two rows, one per visitor type, instead of two separate tables.
- On every homepage request, PHP runs
UPDATE visit_counter SET hit_count = hit_count + 1 WHERE visitor_type = ?for whichever type the request matched. That single statement is atomic in MySQL, so two visitors hitting the page at the same instant can't stomp on each other's increment. - Right after that, a
SELECT visitor_type, hit_count FROM visit_counterreads both current totals back into a PHP array. - Those two numbers are what get handed to the
str_replace()step described above, swapped into{{VISIT_COUNT}}and{{BOT_COUNT}}before the page is echoed out.