How this site is built

Astro compiled to static HTML, built by CI, deployed to a static host. No server, no database, no framework shipped to the browser.

This site replaces a Jekyll blog I set up in 2022 and then stopped touching. The new one is deliberately boring, which is the point.

The stack

Astro builds Markdown into static HTML. There is no client-side framework — the JavaScript that ships is a few lines for the theme toggle, plus the front door, which I will come back to. Posts are Markdown files with typed frontmatter, so a typo in a date or a missing description fails the build instead of rendering as undefined in production.

Hosting

The built output is committed nowhere and uploaded nowhere by hand. A push runs the build in CI and publishes the dist/ directory. The whole deployment is a static file tree, which is the cheapest thing on the internet to serve and the least interesting thing to attack.

One wrinkle worth knowing if you ever move a static site between hosts: how the host resolves a URL naming a directory rather than a file. Astro’s build.format: 'directory' emits blog/index.html instead of blog.html, and most static hosts will serve that for a request to /blog. Hosts that serve objects strictly by key will not, and answer 404 or 403 instead. The fix is either this file layout or a rewrite rule at the edge that appends index.html. Choosing the layout means the site stays portable and the edge stays empty:

// The rewrite you need only if your host won't do the above for you.
function handler(event) {
  var request = event.request;
  var uri = request.uri;

  if (uri.endsWith('/')) {
    request.uri = uri + 'index.html';
  } else if (!uri.split('/').pop().includes('.')) {
    request.uri = uri + '/index.html';
  }

  return request;
}

A custom domain on GitHub Pages is one file — a CNAME in the build output — and it is worth knowing what that file does before you add it. It doesn’t just attach the domain, it makes GitHub redirect the github.io address to it. So a DNS record pointing at the wrong host doesn’t cost you the custom domain, it costs you both addresses: the one that’s broken and the one that would have worked. Mine spent a few days pointing at github.com instead of github.io, which is exactly the kind of typo that reads as correct every time you look at it.

The sequel, for anyone stuck on the same thing: clearing the custom domain in the repository settings did nothing, because the setting is populated from the CNAME file in the configured publishing source — and mine was still a leftover gh-pages branch from 2022 that had one. I was fighting a file, not a field.

The front door

The home page greets you with an Enter button you cannot click with a mouse. It is called Fitts’s Revenge.

Fitts’s Law says the time to acquire a target falls out of its size and its distance — both of which a designer controls, and neither of which a keyboard has ever cared about. So the button declines to be a target at all, and hands the page to Tab and Enter instead.

The reason it exists is the role reversal. People turn up at a product every day and discover the mouse simply will not do it, while a keyboard or a screen reader gets through fine. Most visitors never feel that, because for them pointing always works. Here, for about thirty seconds, it doesn’t.

The first version merely flinched when the cursor got close, which turned out to be trivially beatable — you just approach slowly. So it now runs a loop that keeps the button parked on whichever part of the screen is furthest from where your cursor is heading, and teleports if you get inside 150 pixels. Two things made it actually work. Aiming at your predicted position, not your current one, so it dodges where you’re going. And a grid of candidate positions rather than simple “move away from the cursor” repulsion, because repulsion can be walked into a corner and pinned.

I checked it by replaying the maths headlessly against a simulated cursor that chases the button at speeds from a slow creep to a violent flick. That caught a bug I would never have found by hand: judging danger purely by predicted cursor position reads a fast, direct approach as heading past me, no danger, and the pursuer could sit inside the button 7% of the time. Danger is now the nearer of where you are and where you’re going.

The constraint that mattered: making it impossible is only acceptable if the ways through don’t require aim. Keyboard activation is detected and never dodges. prefers-reduced-motion turns the whole act off and gives you an ordinary button. Escape works. A skip link appears once it’s clear you’re being toyed with. Without JavaScript there’s no door at all.

That isn’t a caveat bolted onto the joke, it is the point. A page that shut out the keyboard too wouldn’t be saying anything about accessibility. It would just be inaccessible.

The view counter

There may or may not be a number under each post, and which one you get depends on where you’re reading this.

A static host has nowhere to record a view, so the front end treats the counter as optional in the strict sense: one build-time value names the endpoint, and with it empty no request is made and every count stays hidden — which looks the same as a page nobody has opened. Nothing in src/ names a backend, so moving the counter, or removing it, never edits a component.

The design constraint I cared about was that the site must not need it. A view count is decoration. If the request fails, the page is exactly what it was without it. That is also why one request returns every count at once: an index page listing twenty cards shows twenty numbers without twenty requests, and without counting a view for each post it merely links to.

Cost

Static hosting for a personal site fits inside a free tier on every provider I looked at, and a counter would too. The domain is the only line item.

The previous version of this site ran on a plan that involved an always-on EC2 instance, which would have cost roughly ten times as much and come with an operating system to patch. Static files don’t have a CVE backlog.