I build for the web. The thing I most recently shipped is Solitaire Online: 27 solitaire variants, eight languages, no third-party ad scripts, exported as static HTML.

That last constraint is the only one that mattered. Every decision below follows from it.

The build: 592 URLs, eight languages

The sitemap has 592 URLs: roughly 30 game pages and 45 written guides, replicated across en (root), en-gb, es, fr, de, nl, it and pt. Each locale has full hreflang plus x-default, and localised titles rather than machine-pasted headings — the German page is titled in German, not "Solitaire" with a lang attribute bolted on.

With a static export you don't get a server to negotiate anything at request time. No Accept-Language rewrite, no middleware, no per-request redirect. That sounds limiting, and at first it is. It also removes a whole class of bugs: there is nothing between the request and the file.

Localised routes: one slug per language, or one shared slug?

Two options. Translate the slugs (/de/solitär/) or keep one slug and vary only the prefix (/de/klondike-solitaire/).

I kept a single slug. Translating slugs gives you marginally nicer URLs and costs you a redirect table that grows with every language, plus a permanent mapping problem when a translation changes. With eight locales that is a matrix you maintain forever. The shared slug gives me one canonical path per game and a prefix per locale — and every URL is guessable.

The trap: sharing a slug across locales makes it very easy to ship the same English page under eight prefixes. That is the failure mode to watch, because it looks finished — the hreflang is valid, the sitemap is consistent, the site technically renders eight languages. It just doesn't localise anything.

The check that caught it was not a crawler. It was opening the German page and reading it out loud.

Structured data is cheap when pages are static

Each game page carries VideoGame, HowTo and FAQPage JSON-LD — three blocks, generated from the same data object that renders the board. Because the pages are static, the JSON-LD is in the HTML at request time; no client-side hydration needed and nothing to test for "does the bot see it".

The mistake I made early: writing the JSON-LD by hand next to the page. It drifts. Every rules change now regenerates the FAQ answers from the same source that the rules section renders, because two sources of truth for a single fact is a queued bug, not a style issue.

What broke at scale

Three things, in order of how much time they cost:

1. Sitemap weight. A single sitemap with 592 URLs is fine. It stops being fine when each locale grows its own set of guides. The split has to happen before it becomes urgent, and the split itself has to be tested — a sitemap index that silently omits one child is invisible for weeks.

2. Icon assets. The site's own logo.png is 68×68. It is in the header, it looks correct at the size it is displayed, and it is unusable everywhere else: social cards, directory listings, profile fields that demand 512×512. A logo that only exists at one size is a logo that exists at no size. Ship the 512 master and scale down.

3. Screenshot routes that quietly return the wrong game. /freecell/ and /klondike-turn-1/ returned HTTP 200, rendered a full page, and served the default Klondike board. No 404, no redirect — a soft duplicate. I generated three "different" screenshots for a listing before noticing they were identical. If a route can 200 while showing the wrong content, assert the rendered heading, not the status code.

What I'd keep

Static output, a data object per game, and no third-party scripts. The last one is a product decision with an engineering payoff: there is no ad SDK to fight, no consent banner, no layout shift from a script that loads whenever it feels like it, and the page weight is a number I control rather than a number I negotiate with a vendor.

Forty Thieves and Eight Off are the variants nobody builds, and they were the easiest part of the whole thing. The hard part was never the game logic.

If you've shipped a multi-locale static site, I'd like to hear what caught you — especially the sitemap split and the slug question. I'm still not certain I chose correctly on the slugs.