How this is builtGroundworkHow this is built
7 / 26 written chapters

I1 · Part 2 · How it runs

What happens when you open a page

Every page and every JSON file is already a file when you ask for it. Three handlers under /api are the only code that runs per request, plus one quiet exception.

6 min read1 diagram7 sections
07On this page
  1. Four kinds of request
  2. Before anything is served
  3. The common case: a file
  4. What a page weighs
  5. Moving between pages
  6. The quiet exception, now half-closed
  7. What the browser fetches after load

Four kinds of request

A request to this site ends up in one of four places. Most go to a file that the build already wrote. A few go to a function. One awkward case goes to a function when it should not. And the playground's language runtimes come from somewhere else entirely: the browser fetches them from a public CDN, not from this site.

Browser one request Vercel edge security headers, legacy-host redirect Prerendered file 1,762 outputs, never revalidated On-demand render an unknown chapter or problem slug Function three route handlers under /api Upstream Edge voices, JokeAPI cdn.jsdelivr.net Pyodide, sql.js, Lua, Ruby, PHP, clang only in the playground
The green path is what nearly every request takes. The yellow one is a gap in the configuration, covered below. Nothing in the bottom box ever passes through this site's servers.

Before anything is served

There is no middleware.ts and no proxy.ts in the repository, so no code runs in front of the routes. What happens at the edge is set in next.config.ts, which the build turns into routing rules:

  • Headers on every path. One rule, /:path*, attaches a Content-Security-Policy, X-Frame-Options: DENY, nosniff, a referrer policy and a permissions policy that turns off camera, microphone and geolocation. The CSP's script-src and connect-src lists are built from lib/wasmAssets.ts, so they allow the jsDelivr origin and nothing else.
  • One redirect. Requests for the old groundwork-ivory-beta.vercel.app host get a permanent redirect to groundwork.austincoders.com, with the path kept.

Rate limiting in front of /api/ is a Vercel Firewall rule. Its threshold is set in the Vercel dashboard and deliberately kept out of this public repository, so this chapter cannot quote it.

The common case: a file

The last production build wrote 1,762 entries into .next/prerender-manifest.json, and every one has initialRevalidateSeconds: false. Nothing is on a timer and nothing is regenerated in the background. A file changes only when a deploy replaces it.

WhatCountMade by
Pages, as HTML plus an RSC payload1,222page.tsx and generateStaticParams
Test cases for other languages538app/problems/[slug]/cases/route.ts
Search indexes2120 per-topic routes plus one global route
Mock interview question banks12app/mock/bank/[stage]/route.ts, one per stage
Metadata files149 Open Graph images, sitemap, robots, manifest, two icons

On top of those sit the 81 hashed JavaScript chunks under /_next/static, and public/wasm/, which holds 46 TypeScript lib.*.d.ts files and a 205 KB React sandbox bundle. Both are written by the prebuild script.

What a page weighs

Sizes from that same build. HTML is uncompressed, as the manifest records it. JavaScript is the first-load set from .next/diagnostics/route-bundle-stats.json, with gzip measured on the actual chunk files.

RouteHTMLFirst-load JS
Home186 KB732 KB, 224 KB gzip, 18 files
A chapter page50 to 222 KB; the JavaScript topic averages 106 KB681 KB, 210 KB gzip, 14 files
A problem page39 to 50 KB1,182 KB, 373 KB gzip, 18 files
/practice27 KB1,182 KB, 373 KB gzip, 18 files
/whiteboard27 KB636 KB, 196 KB gzip, 13 files
/problems276 KB681 KB, 210 KB gzip, 13 files

Two rows need explaining. /practice has so little HTML because PracticeClient reads useSearchParams() inside a <Suspense fallback={null}>. During prerendering, everything below that boundary is left for the browser to render, so the page ships an empty frame. The whiteboard is small for a different reason: its canvas is loaded with ssr: false and is not in the first load at all.

Moving between pages

After the first page, a click on a link does not fetch HTML. The router fetches the target's .rsc payload, which is also a prerendered file. Nineteen chapter routes have a loading.tsx that shows ChapterSkeleton while it arrives. The root layout wraps every page in RouteFade, which fades in the new page on client navigations but not on the first load.

The quiet exception, now half-closed

The chapter, problem and level routes all export generateStaticParams. Of the 25 dynamic routes in the build's manifest, 22 did not also set dynamicParams, so the default of true applied: a slug the build never saw, such as /notes/not-a-chapter, was not answered with a stored 404. It was handed to a function, which rendered the page, and TopicChapterPage called notFound() after the fact — a page built on demand that happens to say "not found."

Nineteen of those 22 now set export const dynamicParams = false: the 18 topic [chapter] routes and /architecture/[chapter]. The manifest lists all 19 with fallback: false, the same as /git/[section], which already had it. An unknown slug under any of those routes is now a stored 404, nothing rendered. Three routes are still open, still listed with fallback: null: /problems/[slug], /level/[topic], and /interview/[chapter], which renders through RoundView rather than TopicChapterPage and was never part of this fix.

Three routes still take the slow path

/problems/[slug] and /level/[topic] would take the same one-line fix as the chapter routes, but that is a separate change, not made here. /interview/[chapter] is a different page component entirely. Until all three set dynamicParams = false, a mistyped problem id, level or interview round still costs a render before the 404.

What the browser fetches after load

RequestWhenServed as
{topic}/search-index.json and /search-index.jsonFirst character typed in the chapter search boxStatic file
/problems/{id}/casesSwitching a problem to a language other than JavaScript or SQL, or starting the debuggerStatic file
/mock/bank/{stage}Pointing at or focusing the start button in the mock lobby, then starting; cached per stage in memoryStatic file
/api/ttsPressing Listen, once per chunk of up to 700 charactersFunction, then CDN-cached
/api/jokeOpening /progressFunction
/api/client-errorAn uncaught error or rejection, at most five per page load, and only when no Sentry DSN is configuredFunction
Runtimes on cdn.jsdelivr.netFirst run of Python, SQL, Lua, Ruby, PHP, C or C++Someone else's CDN

Each of the three functions is an extra on top of a page that already works without it. If one fails, the narrator does not start, a hardcoded joke appears, or an error is not logged. No chapter depends on any of them. The APIs chapter goes through each one in detail.