Asperia Games

Rebuilding asperiagames.com

The old site was a template with invented testimonials and stock controller photos. Here's what replaced it, and the browser problem that shaped it.

The old asperiagames.com was a website-builder template that had never been fully filled in. It advertised a consulting service I don’t offer. It had a newsletter signup that went nowhere. It carried two five-star testimonials from players in Los Angeles and New York who do not exist, and it illustrated my games with stock photographs of game controllers.

Its /blog route rendered the homepage, because there was no blog.

None of that was malicious — it’s just what happens when a template ships with placeholder content and the placeholders never get replaced. But it meant the site was actively misrepresenting a studio that has two real, finished, playable things.

What it is now

A static site where the content is files.

Every game is a folder with a markdown file and its images in it. Every post is the same. Adding a game means creating one folder; nothing else in the codebase changes, because the pages derive everything else — the games index, the devlog list on each game’s page, the tag counts, the RSS feed — from what’s in the collection.

The frontmatter is validated at build time, so a malformed colour, a missing image description, or a post pointing at a game that doesn’t exist fails the build rather than shipping. That’s the closest thing to a CMS here, and it turns out to be enough.

The testimonials, the consulting service and the newsletter are gone.

The problem that shaped the architecture

I wanted both games playable on the site. One of them was straightforward. The other took a detour worth writing down.

Coin Run is LÖVE compiled to WebAssembly. Its build uses threads, which means it needs SharedArrayBuffer, which browsers only hand to a page that is cross-origin isolated — a state a page enters by sending two specific headers.

Here’s the part that matters: isolation is not inheritable by an iframe. An embedded frame cannot opt into it on its own; the entire chain of documents above it has to be isolated too. So the obvious approach — drop the game in an iframe on its own game page — produces a blank canvas and a console error about SharedArrayBuffer being undefined.

The obvious fix is to isolate the game’s page as well. But isolation is contagious in the other direction: a page that sends require-corp will refuse to load any third-party resource that doesn’t explicitly opt in — and LOGFIRE is embedded from Lexaloffle’s BBS widget, which doesn’t. Isolating everything would fix one game by breaking the other.

The resolution is that isolation is scoped per route. Coin Run gets a dedicated play page that sends the headers and loads nothing from anywhere else — which is also why this site self-hosts its fonts. LOGFIRE’s page stays un-isolated and embeds the widget inline as normal. Different routes, different rules, both games playable.

The embed rules I settled on

Working through that produced a few principles I’d keep for any site embedding a game:

Nothing heavy loads on page paint. The PICO-8 iframe lives inside an inert <template> element until you click. Load LOGFIRE’s page and watch the network tab: zero requests to lexaloffle.com until you ask for the game.

The iframe is never the only way in. There’s a permanent “Open on Lexaloffle” link rendered in every state, plus a watchdog — if the frame hasn’t loaded within six seconds, the page assumes an extension or a network policy blocked it and says so, with a working link. Cross-origin frames fire load even for error pages, so you cannot detect this any other way.

Below a certain width, don’t offer it at all. PICO-8’s widget is a fixed 621×513 canvas with no touch controls. Scaled to fit a phone it’s unreadable and unplayable, so on small screens the page shows gameplay footage, the controls, and a link to somewhere that handles mobile better than I can.

Say where focus went. Once a cross-origin iframe takes focus, the parent page stops receiving keystrokes entirely — which is correct, but invisible. The embed shows a “keyboard captured” label and a glow on the frame so it’s never ambiguous whether the arrow keys are going to scroll the page or move your cursor through the log feed.

Still to do

The itch.io links are stubbed out until there’s an itch.io page to point at, and they render as nothing rather than as dead links — which is the same principle that got rid of the fake testimonials. If it isn’t real yet, it isn’t on the site.