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

I2 · Part 2 · How it runs

How 1,762 files get made

One source of truth, one pass, and every page, index, test case and preview image is derived from it in about thirty seconds.

6 min read1 diagram7 sections
07On this page
  1. The pipeline
  2. The steps, in order
  3. Where the routes come from
  4. Recording test cases for other languages
  5. Indexes, sitemap and preview images
  6. What next.config.ts does, and does not, do
  7. What the build leaves behind

The pipeline

content/ a file per chapter, a barrel per topic lib/content.ts the only reader 1,222 pages generateStaticParams, 24 files 538 test-case files recordPolyglot inside node:vm 21 search indexes 20 topics plus one global Sitemap, 9 OG images 755 URLs; 1200 by 630 PNG Static generation 21.5 s of the 29.6 s build RUNS FIRST: prebuild copy-wasm-assets.mjs, build-react-sandbox.mjs write public/wasm/, which git ignores
The interface in the middle does most of the work. Add a chapter file to a topic's barrel and its route, search entry, sitemap line and reading time all appear without any other change.

The steps, in order

  1. prebuild. scripts/copy-wasm-assets.mjs copies the 46 TypeScript declaration files listed in lib/tsLibFiles.json into public/wasm/typescript-lib/. The editor's type checker needs them. Then scripts/build-react-sandbox.mjs uses esbuild to bundle lib/reactSandbox/runtime.ts into a minified ES2020 script, public/wasm/react-sandbox.js, which is 205 KB. The React exercises run inside it.
  2. Compile. Turbopack bundles the app. In the last production build's trace this took 2.2 seconds.
  3. Type check. tsc runs inside next build and took 2.8 seconds. When the type check runs on its own on a fresh checkout, npx next typegen has to run first, because the global PageProps and LayoutProps helper types have to exist on disk before a separate tsc --noEmit can check app/.
  4. Static generation. Every generateStaticParams is called, and every page, route handler and metadata file is rendered to disk. This took 21.5 seconds.

The whole next-build span in .next/trace-build was 29.6 seconds. About seventy percent of it is the step that grows with the content.

Where the routes come from

24 files under app/ export generateStaticParams. The 20 topic [chapter] routes call topicChapterParams(topicId) in components/reader/topicPages.tsx, which returns every chapter in the topic, including outlines. An outline still gets a page, a roadmap card with its syllabus section and a "Meanwhile" link to a related written topic, but its metadata sets index: false. Crawlers can follow its links, but it stays out of search results. /problems/[slug] and /problems/[slug]/cases both map over exercises(), and /level/[topic] maps over topics that have levels.

Nothing reads the content directly except lib/content.ts, which is also where derived fields come from. The reading time shown on every chapter is computed there: anything inside a <script>, <pre>, <style> or <svg> element is dropped, the remaining tags are stripped, the words are counted and divided by 180, and the result is rounded with a minimum of two minutes.

Recording test cases for other languages

The most unusual build step is lib/polyglot/record.ts. Every exercise ships a JavaScript reference solution and JavaScript tests. The playground can also grade Python, Ruby, PHP, Lua, C and C++. Rather than hand-write tests in six more languages, the build records what the JavaScript does:

  • It finds the solution's top-level function and its parameter names with a regular expression. Anything else is declined with a reason.
  • It keeps only tests made entirely of direct assert.equal, strictEqual, deepEqual or deepStrictEqual calls on that function, plus const set-up lines.
  • It runs the solution and each test inside vm.runInNewContext with a two-second timeout. The function is wrapped so that every call logs a deep copy of its arguments and its return value.
  • Recorded values have to be plain: numbers, strings, booleans and nested arrays. From those it infers a signature, int, float, bool, string or list of one of them, and it gives up if an argument changes type between calls.

app/problems/[slug]/cases/route.ts is force-static, so this runs 538 times during the build and never in production. Of those, 222 exercises came out gradable in other languages, with 1,439 tests and 2,048 recorded cases, and 395 tests were skipped as not replayable. The other 316 carry a reason the page shows instead:

Why it stays JavaScript-onlyExercises
A React component99
Tests check properties of the answer, not an exact value84
Works on JavaScript objects61
About JavaScript itself (no top-level function)27
Parameters with destructuring or rest syntax17
About the async model11
Arguments change type between calls11
Tests observe something besides the return value6

The cost is size. Most case files are a one-line refusal, and the median is 97 bytes. But recording keeps every call, including the recursive and repeated calls a test makes, so the largest file, ex-maximal-square, is 1,002,059 bytes. Together they come to 4.2 MB. A reader only downloads one after switching that problem to another language.

Indexes, sitemap and preview images

The 21 search indexes are force-static route handlers too. The search chapter covers what goes in them. app/sitemap.ts reads the same interface and lists 1388 URLs. Outlines are left out. A topic's pages carry the meta.updated month from its notes file as lastModified; everything with no such date, including every problem page, carries one shared constant that moves only when that content actually changes, so deploys stop telling crawlers that everything just did. app/robots.ts disallows /api/ and points at the sitemap.

Nine opengraph-image.tsx files draw 1200 by 630 PNG cards with next/og's ImageResponse, through ogCard in lib/og.tsx. The topic cards count their written chapters and exercises at build time, so the chips on a shared link cannot go stale. Each PNG is between 37 and 58 KB, and all of them are prerendered. lib/metadata.ts gives every other page one of those nine images by matching its path to the nearest section, so a shared link always carries a picture, not bare text.

What next.config.ts does, and does not, do

It sets headers and redirects, turns off the automatic trailing-slash redirect so that proxy.ts can do it in one hop, and nothing else: no images block, no experimental flags, no webpack changes. The CSP inside it is assembled from two helpers. One is wasmOrigins() from lib/wasmAssets.ts. The other is sentryOrigin(), which adds nothing unless a DSN is set.

lib/wasmAssets.ts is where every runtime version is pinned: Pyodide 314.0.6, sql.js 1.14.2, wasmoon 1.16.0 for Lua, Ruby WASI 2.10.1, php-wasm 0.1.0, clang 22.0.0-git20542-10 and the WASI shim 0.4.2. By default they are all loaded from cdn.jsdelivr.net. The prebuild script can copy Pyodide and sql.js into public/wasm/ with --all, and three environment variables can point the loaders there, but the default build does neither. tests/wasm-assets.test.ts fails if the pinned Pyodide or sql.js version stops matching the installed package, or if a base URL loses its trailing slash.

What the build leaves behind

OutputCountServed how
Pages (HTML and RSC)1,222Static, never revalidated
Static route handlers571Static JSON
Metadata routes14Static
JavaScript chunks81Hashed, under /_next/static
Functions4 under /api, plus on-demand rendering for unknown slugsPer request

The prerendered HTML alone comes to 63.3 MB. .next/server/app, with the RSC payloads and route bodies, is 198 MB on disk.

Build time is not the constraint

Static generation is the only step that grows with the content, and it renders about 80 files a second. Writing the 358 outlined chapters would not add a single route, because every outline already has its page; it would only make those pages longer. The build would get slower by seconds, not minutes. Deploy storage is a more likely limit: each deployment is large enough that a scheduled workflow prunes old ones.