Overview
The platform hosts a small arcade of self-contained, static, client-side games โ no
database, no backend, pure HTML/CSS/JS served directly through the existing Hugo +
HAProxy pipeline. Each game is a single index.html file with everything inlined
(no external JS/CSS dependencies), which keeps deployment trivial: drop the file in
the right static folder, rebuild Hugo, done.
As of this writing the arcade has 6 games: Test Platformer, Breakout, Snake, Space Shooter, Invaders, and Frogger.
Repo structure
apps/hugo/service/static/games/<game-name>/index.html โ the actual game, static asset
apps/hugo/service/content/culture/games/_index.md โ archive landing page (card grid)
apps/hugo/service/content/culture/games/<game-name>.md โ per-game content page (iframe embed)
apps/hugo/service/static/css/games-grid.css โ shared card grid styling
URL mapping:
/games/<game-name>/โ serves the raw static game (the iframe target)/culture/games/<game-name>/โ the Hugo content page that embeds it/culture/games/โ the archive index with all game cards
Important: the static file must be named index.html (not <game-name>.html)
inside its own folder, so the clean URL /games/<game-name>/ resolves. Naming it
anything else means visitors hit an ugly /games/<game-name>/<game-name>.html URL
instead โ this has bitten us before (Breakout was initially saved as
breakout.html and had to be renamed).
Shared conventions across every game
Every game file follows the same baseline, established across the platformer, Breakout, Snake, the shooter, Invaders, and Frogger builds:
- Viewport meta tag:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">โ required for correct mobile scaling. Do not adduser-scalable=noโ that traps users with an oversized canvas and no way to zoom out if sizing is ever off. - Fullscreen button (โถ): uses
requestFullscreenwith vendor-prefix fallbacks (webkitRequestFullscreen,msRequestFullscreen) since plainrequestFullscreenalone under-detects support on many mobile browsers, especially inside an iframe. The button hides itself if no variant is supported. Even with prefixes handled, iOS Safari in particular may still block fullscreen from inside an iframe โ this is a known ceiling, not a bug to keep chasing. - Pause button (โธ) + Esc key: freezes the update loop, shows a Resume/Restart menu. Wired so pausing never counts as a loss and resuming doesn’t cause a physics/timer catch-up jump.
- Mobile touch controls: on-screen buttons gated behind
@media (pointer: coarse), hidden on desktop. Landscape mobile layout triggers on@media (pointer: coarse) and (max-height: 500px)rather thanorientation: landscapeโ the latter proved unreliable inside an embedded iframe on real devices. #game-areaflex container: canvas and touch-controls live together in one real flex layout (notposition: fixedoverlays) so the browser genuinely reserves space for both instead of controls floating on top of the canvas. This was a real bug once โ fixed-position controls looked fine in isolation but sat on top of the canvas rather than beside/below it once actually tested on a phone.- Best-score persistence:
localStorage(notsessionStorage) so high scores survive across visits, not just the current tab session. Keyed per-game, e.g.snake-best,invaders-best,frogger-best. - Iframe embed attributes: every
<iframe>embedding a game in its content page must includeallow="fullscreen" allowfullscreen, or the in-game fullscreen button silently does nothing โ browsers block fullscreen requests from unpermitted iframes by default.
Checklist: adding a new game
- Build the game file as a single self-contained
index.html, following the shared conventions above (viewport, pause, fullscreen, mobile controls, localStorage best score). - Create the static folder and place the file:
cd apps/hugo/service/static/games mkdir <game-name> mv "$DL/<game-name>.html" <game-name>/index.html - Test locally โ
explorer.exe index.htmlfrom WSL2 opens it directly viafile://, no deploy needed for basic testing. Confirm keyboard controls, mobile touch controls (browser dev tools device emulation is a reasonable proxy but real-device testing has caught bugs emulation missed), pause/resume, and win/lose states. - Create the content page:
Frontmatter:apps/hugo/service/content/culture/games/<game-name>.mdtitle,date. Body: one-line description, the iframe embed (src="/games/<game-name>/", rememberallow="fullscreen" allowfullscreen), and a Controls: line. - Add a card to the archive index at
apps/hugo/service/content/culture/games/_index.md, inside the<div class="games-grid">block:<a href="/culture/games/<game-name>/" class="game-card"> <div class="game-icon">๐ฎ</div> <p class="game-title">Game Title</p> <p class="game-blurb">One-line hook.</p> <p class="game-play">Play โ</p> </a> - Deploy:
./scripts/deploy-hugo.sh - Verify live:
/culture/games/shows the new card/culture/games/<game-name>/loads the content page with the game embedded/games/<game-name>/loads the raw game directly- Card link, iframe, and controls all work
- Test on an actual mobile device if possible, not just emulation โ several real bugs (landscape sizing, fullscreen support, touch-vs-click event mismatches) only surfaced on real hardware
Known open issues / rough edges
- Fullscreen from inside the iframe embed is not guaranteed to work on all mobile browsers, particularly iOS Safari. The vendor-prefix fix widens support but does not eliminate every case. No further fix identified yet beyond the fallback already in place; a “open in new tab” link is a possible future workaround if fullscreen reliability keeps being a problem.
- Games with more complex physics (Frogger’s log-riding, wrap-around lane
entities) use
dt-scaled movement to stay roughly frame-rate independent โ worth checking this doesn’t drift on longer play sessions if a game runs for a long time in one sitting.
Games built so far
| Game | Path | Notes |
|---|---|---|
| Test Platformer | /games/platformer/ | 8 levels, double jump, coyote time, character skins |
| Breakout | /games/breakout/ | Paddle physics, angled bounce based on hit position |
| Snake | /games/snake/ | Classic grid movement, can’t reverse into self |
| Space Shooter | /games/shooter/ | Free 8-directional movement, escalating waves |
| Invaders | /games/invaders/ | Classic marching grid, enemies fire back |
| Frogger | /games/frogger/ | Road + river crossing, log-riding, countdown timer |