Migrating our site from a .com to a .co.uk domain meant that right from the beginning, we knew some of our incoming traffic would hit bad URLs, no matter how good we were. So what do you show then? So many companies treat the 404 like a tombstone - we decided the page guaranteed to see traffic you weren't expecting during a migration is the perfect place for a small browser game. So here's why we did it, how we did it, and the cost of doing it.1. The Migrating CliffThe overlooked bit about domain migration has always been that the 301 redirect map is a best guess at best, not a HOLY GRAIL. We did try to craft an exhaustive map of what we thought would hit it, and even some known URLs failed to make it onto the map and instead had this; nearly every valid domain landed exactly as expected, with the exception of...Legacy print citations - we had a parent's magazine reference class names back in print, dated to 2019 (the URLs had long since been restructured, and print magazines can't do PATCH requests, or update to my site!).Mis-typed and abbreviated paths - The 2019 magazine wrote 'programme' when the route was indeed 'programmes' - that was also missed. We have customers who typoed their address too, or deliberately shorten links for business cards and flyers.Links from other sites with stale parameters - We had affiliate links that sent traffic with old filtering parameters in the URL; e.g. ?stage=infant&loc=old-centreThe long tail - we had old preview URLs, cached Google links (from 2016), or a particularly old Pinterest link. Each one bounced.So we all know what the traditional 404 page looks like. Nothing to it but to "exit". It’s barely any SEO points and just leaves users frustrated. You've already gained and spent SEO equity driving them there; it seems a waste to throw it away like that.So our new mantra was simple: 'Hope for bad redirects and reward any visit to them'.2. The Restriction- The Error Page had to have zero-bloatMy first idea was that we build a tiny game, but immediately following it was the thought that on an error page, this has to have as low a load time as possible. If a user visits from an unreliable 3G connection, they could spend 500ms watching a game page that would normally appear immediately. If we tried to build this in with Phaser or another one of these popular but large games, users who already expected something bad from us would rage-quit and never return. So we had to come up with a game with strict constraints for load time:Must load in less than 100 ms.No 3rd-party game dependencies. This eliminated every single engine on the market and pushed us to do the bare minimum:Using only raw 2D canvas (800x400), and not WebGL, to save on initial setup and keep draw calls clean.requestAnimationFrame is perfect on the 2D canvas, as well as keeping it all nice, battery-efficient, and performant.A simple, pure, closures-scoped state machine (the game logic). NO react or React extensions in the rendering for anything. All rendering data (score and object states, etc.) were passed around simply as variables, and all the data was immutable so we could save on unnecessary computations and calls in the effect lifecycle; just keep drawing what state we are currently in.It was styled purely with CSS3, including the surrounding shell of the page, mobile-friendly touch controls, and a small fake-terminal style border-this just leaves you with a zero-weight component.3. Engineering a Cognitive Task Game on a 404 (or as we call it: '404Brain')A simple arcade game wouldn't really align with our site, so we decided to play on the site's child education themes. We needed to make people think, with words flying around and having meanings beyond the literal; we displayed PORT, NORTH, SOUTH, NEXT, PEAK with word-based prompts requiring people to select the correct arrow to go to either the port direction or the 'meaning' required. So you got little words like "PORT" appearing when left-right arrows were displayed. This effectively is a reversed micro-Stroop test; it requires you to interpret, rather than match.The State machineThere's a closure (we do use no libraries for anything):type GameState = 'START' | 'PLAYING' | 'GAMEOVER' | 'HELP';let state: GameState = 'START';let score = 0;let speed = 2.5;let obstacles: Obstacle[] = [];Input, with hit-zone filteringHit confirmation, if signal is in central hit zone. Hit early or late and signals are missed, as is hitting the wrong arrow and you miss the next shot: The hit-confirmation logic is actually essential to the feel of the game:function handleGameInput(code) { if (state !== 'PLAYING') return; if (!['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(code)) return; const activeCue = obstacles.find((o) => { const cx = o.x + o.width / 2; return cx > HIT_ZONE_START && cx < HIT_ZONE_END; // in the brain }); if (activeCue && activeCue.targetKeys.includes(code)) { score += 10; // 10 "BOLTS" per intercept speed += 0.2; //