Data
Every read goes through the Go API — how server-api.ts is shaped, what SWR does on the client, and why almost nothing in the dashboard is prerendered.
There is no database client in this app and no connection string in its environment. Every read is an HTTP call to the API, and there are exactly two ways to make one.
On the server: server-api.ts
src/lib/server-api.ts is the server-side adapter — it binds the shared
statshub-api-client transport to API_ORIGIN and
Next's data cache. import "server-only" keeps the adapter out of client
bundles.
const team = await getPublicApi<Team>(`/team/${id}`, { revalidate: 3600 });Three behaviours worth knowing before you call it:
undefinedandnullquery values are dropped, so an optional filter does not become?league=undefined.- 404 returns
null, not a throw. A missing entity is an ordinary answer; the caller decides whether that isnotFound()or an empty state. - Any other non-2xx throws, with the status and path in the message. That
reaches the nearest
error.tsxrather than rendering half a page.
revalidate defaults to 300 seconds and is what makes a route cacheable.
Passing it is a decision about staleness — a fixture list wants seconds, a
team's badge wants an hour.
On the client: SWR
Anything that has to react to a filter the user just changed is fetched with
SWR, configured once in src/components/layout/providers.tsx:
{
dedupingInterval: 24 * 60 * 60 * 1000,
revalidateIfStale: true,
revalidateOnFocus: true,
revalidateOnReconnect: true,
}That deduping interval is a day, which is unusual and deliberate: the boards
re-render constantly as filters change, and without it every re-render of a
shared hook is another request. Staleness is handled by the three
revalidate* flags instead — on focus, on reconnect, and when a key is known
stale — rather than by a timer.
There is no React Query here
Where state lives is the rule across apps: a cache owns server state. On web that cache is SWR; on expo it is React Query. Mixing them in one app means two caches with two copies of the same row.
SWR's fetcher and postFetcher use the same shared transport as Server
Components and Expo. SWR still owns the web cache; sharing a transport does not
create a second cache.
What is prerendered
Almost nothing, and the reason is one line in a layout.
src/lib/static-params.ts generates params for the entity routes by reading
the sitemaps the API already serves — /api/sitemap-teams.xml
and friends. That was chosen over a "list every id" endpoint per route because
the sitemap is already the answer to "which of these pages do we want
crawled": it filters to visible tournaments, and the fixtures one is scoped to
finished matches from the last 60 days.
It deliberately does three things by halves:
- It does not prerender everything.
sitemap-players.xmlis six figures of URLs. Each route takes the firstSTATIC_PARAMS_LIMIT(default 250); the rest render on first request, which is the App Router's own default for a param outside the generated set. - It does not fail the build.
docker buildhas no API on its network. An unreachable API means zero prerendered paths and a warning, not a broken image. - It does not, on its own, make these routes static. Everything under
(dashboard)inherits a layout that reads thestatshub_localecookie, so the whole segment renders per request and nogenerateStaticParamsin it currently produces a prerendered page.
Moving the locale out of that layout, or turning on cacheComponents, is what
switches them on. Until then generateStaticParams is a warm cache waiting for
a change elsewhere.
Images
Served through Next's own optimizer with sharp, in AVIF and WebP, cached 30
days at the optimizer and edge-cached by Cloudflare in front. The origin is R2.
Every remote host has to be listed in images.remotePatterns in
next.config.js — a new CDN that is not there renders a broken image with a
400 from /_next/image, which reads like a bad URL and is not.
Architecture
How a page in the product site is put together — the route groups, the shell it mounts into, the portal targets a board fills, and the hand-over from legacy.
Conventions
The rules for writing code in the product site — where a filter goes, where state lives, what a new board must reuse, and how strings get translated.