feat(game): put the region in the URL path

/game?region=TPHCM becomes /game/tphcm, backed by an app/game/[region]
dynamic segment. The region is a property of the page, so it belongs in
the path: the URL is now shareable and honest, an unknown region is a
real 404 instead of silently rendering as "Vietnam", and the page no
longer needs useSearchParams or its Suspense boundary.

It also makes per-region traffic visible. Vercel's Pages dimension strips
query parameters on every plan, so ?region= was invisible; one row per
region falls out of correct routing.

Slugs are lowercase, and regionSlug/regionFromSlug in lib/regions.js own
the conversion. Three call sites build these URLs -- the picker,
generateStaticParams, and the legacy redirect -- and a casing mismatch
between any two would split one region across two rows, which is the
whole point of the move.

An unusual casing renders rather than redirecting to the canonical form.
A redirect on a prerendered route gets cached as that route's response:
on a case-insensitive filesystem the ISR cache folds /game/DNA onto
/game/dna, and a cached redirect that has lost its Location header then
answers the canonical URL with a 307 to nowhere for the life of the
process. Reproduced on Windows against next start; no redirect on the
route means no such mechanism anywhere.

Legacy ?region= and ?location= redirect from app/game/page.js rather
than next.config.mjs, because a config redirect forwards the source
query to the destination and would park ?region= on /game/tphcm
permanently. The legacy value is encodeURIComponent'd: it reaches a
Location header unvalidated, where a raw CRLF makes Node throw a 500
instead of the honest 404, and a raw ../ would be normalised onto
another path.

A region-less /game now plays the country rather than defaulting to
TPHCM, which was never a stated default -- just where the old query
chain happened to end.

API routes keep their query params. They are fetch calls, not
navigations, so a path segment buys nothing there.

Also repairs four specs that were already failing on main: three
asserted that the landing page shows no username prompt, which the
landing-prompt change contradicted, and one expected the build sha and
its copy button to be a single element, which splitting them
contradicted.
This commit is contained in:
tiennm99 committed 2026-09-06 16:37:43 +07:00
1 parent 90ebed728a
commit 711ef863a6
14 files changed
+409 -71

No files matched your search

+10 -3
View File
@@ -4,7 +4,9 @@
- `CLAUDE.md` - Project instructions and guidelines for Claude Code
- `components.json` - shadcn/ui configuration
- `package.json` - Dependencies and scripts
- `next.config.mjs` - Next.js configuration
- `next.config.mjs` - Next.js configuration. Deliberately has no `redirects()`:
a config redirect forwards the source query string to the destination, so the
legacy `/game?region=` links are redirected from `src/app/game/page.js` instead
- `eslint.config.mjs` - ESLint configuration
- `postcss.config.mjs` - PostCSS configuration
- `jsconfig.json` - JavaScript project configuration
@@ -26,7 +28,12 @@ Next.js 16 App Router structure:
- `favicon.ico` - Site favicon
#### Game Pages
- `game/page.js` - Main game interface
- `game/[region]/page.js` - The game screen for one region (`/game/tphcm`).
Server Component: validates the slug, prerenders one page per region,
404s an unknown one
- `game/[region]/not-found.js` - The 404 for an unknown region code
- `game/page.js` - Redirects the legacy `?region=` / `?location=` links to
`/game/{slug}`; a region-less `/game` goes to the country round
- `credits/page.js` - Data sources, licenses, and open-source credits
- `debug/page.js` - Debug hub: lists every debug tool as a peer
- `debug/layout.js` - Shared shell for all debug pages: app bar, DebugNav, theme
@@ -84,7 +91,7 @@ the shadcn CLI when a screen needs them, rather than keeping unused ones around.
Both directories are build output. Do not hand-edit; see *Rebuilding the
generated region data* in [development.md](development.md).
- `regions/index.js` - The 67-node tree: code, name, parent, level, children,
- `regions/index.js` - The 85-node tree: code, name, parent, level, children,
center, bbox, and coverage flags
- `regions/counts.js` - Per-region panorama and cell tallies. **The one
panorama-derived file a client component may import** - it carries counts