The 404 page on jrignacio.com says, “You found something you weren’t looking for.” Underneath it is a pinball table.
It started with a line from Benji Taylor: “A personal website should feel like you’ve briefly left the rest of the internet.” I removed the links that forced new tabs, added the time in Manila, and made the homepage sound more like me. Then I reached the missing-page screen and gave it something to do.
The first version, built on September 8, had three balls, bumpers, flippers, and a shooter lane. It was roughly 300 lines of canvas drawing and hand-rolled physics, with no game library to load. The table borrowed the site’s own colors, so it belonged to the page in either theme.
This is about that first build, when it looked convincing before it was reliably playable.
The code wasn’t the hard part. Playing it was.
A ball can be inside the game and outside play
Two early problems were obvious once I played it. The top of the dome extended above the canvas, so the ball could fly out of view. The touch launcher fired at roughly two-thirds of the power it needed to clear the lane.
The more interesting one was a small gap near a flipper. An outlane wall ended eight pixels short of its pivot, and the ball could settle into that notch and stay there.
Pressing the flipper looked like the right response. It barely helped. Near the pivot, the flipper’s surface hardly moves, so the part that needed to knock the ball loose was the part that wasn’t doing much of anything.
The fix that stuck was to extend the wall to the pivot and remove the pocket. A stall breaker could nudge a slow ball, but using one to escape a predictable hole in the geometry would leave the hole where it was.
The shooter lane got to the same outcome by another route. After entering the table, a ball could fall back into the lane and rest on its floor. The simulation kept running. The player had lost access to the ball without losing the ball.
So I added a one-way gate: let a rising ball leave the lane, keep a falling ball out.
That sounded complete. It wasn’t.
The gate checked the wrong direction
Later, playing it again, I found that a ball already on the playfield could bounce upward into the lane from the left. The first gate checked vertical direction, and an upward-moving ball satisfied that rule even when it came from the wrong place.
The later correction considered which side of the gate the ball was on and which way it was moving horizontally. The movement I actually wanted to allow was out of the lane, right to left. “Moving upward” had only ever been a rough stand-in for that.
It’s my favorite bug in the sequence, because the first fix was useful and still incomplete. It handled the route I’d seen. Then another trajectory showed what the rule didn’t say.
Before any of that, right after the initial geometry fixes, I ran an 18-second automated play test in Antigravity, partly to see how Gemini 3.8 Flash handled it. It recorded a longest rest of 0.12 seconds and no repeated rest spots. The traps I knew about didn’t come back during that short run. I’d keep the claim that narrow, and the gate bug that turned up afterward is why.
Reading each collision function told me something about how one segment handled an impact. Playing showed what happened when all those segments became a table. A handful of individually reasonable bounces could deliver the ball somewhere the game stopped being a game.
The phone brought its own physics
On mobile, the first instruction told the player to press space.
That was easy to fix. The other problems came from the page around the canvas. A long press could bring up the iOS selection callout. On a short screen, the centered layout clipped the table and gave no way to scroll to the rest of it. Fixing that scroll problem then threw off vertical centering on the homepage and About page, so those needed another look.
The weak tap launch belonged to the same pass. A touch event that works isn’t enough if the launch it triggers can’t reach the table. Multi-touch, on the other hand, already worked: two fingers could hold both flippers up at once.
“Supports touch” turned out to be several claims, not one. The controls had to respond, the launch needed enough force, browser gestures had to stay out of the way, and the table had to fit or stay reachable. The ball physics could be unchanged while the phone experience failed around it.
Still a missing page
The game also had to keep the fact that the requested page was missing.
The site runs on Astro and Cloudflare Workers static assets. In the deployed configuration for that build, an unmatched path served the custom 404 page with a real 404 status code. The verification used an invented path, not just the game’s own page. The page can be playful, but it shouldn’t lie about what happened.
The other constraint was no third-party requests while the page loads or plays. The site used system fonts and inline styles and scripts, with no analytics, no web-font service, and no content-delivery network. Canvas was enough for a table this size. Keeping it self-contained let the game feel like another part of the site, not an app dropped into the middle of it.
The final checks were ordinary: the homepage returned 200, the About route resolved, and an invented path returned 404 with the game in the response. I also played the table, including with touch. Each check covered a different part of what I was about to put online.
None of this was necessary to apologize for a missing page, and that’s some of the appeal. I gave an error screen a small toy, then had to learn enough about its corners to make the toy work.
You found something you weren’t looking for. So did I.