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

B6 · Part 1 · Foundations

Every route and why the URLs look the way they do

53 pages, 27 route handlers, the metadata files, and the links that live after the # sign.

13 min read2 diagrams12 sections
12On this page
  1. The whole tree on one page
  2. Topic covers and chapters
  3. /problems, and a page per problem
  4. /practice?id=
  5. /whiteboard
  6. /mock, and the question banks
  7. /level, /path, /review, /progress, /soon, /privacy
  8. Search indexes
  9. The three functions
  10. Sitemap, robots, manifest and share images
  11. Links that live after the # sign
  12. The page frames

The whole tree on one page

app/ holds 57 page.tsx files and 26 route.ts files. Only three of those route handlers run on request. The other 23 are JSON files in disguise: they set dynamic = "force-static", run once during the build, and are served from the CDN like any page. Every URL on the site falls into one of five groups.

Framed pages one page header / /<topic> /<topic>/<chapter> /git /level/<topic> /path/<topic>/<level> /review, /progress /mock Full screen a menu drawer, no frame /practice?id=free /problems /problems/<id> /whiteboard Prerendered JSON made once, at build time /search-index.json /<topic>/search-index.json /problems/<id>/cases /mock/bank/<stage> 21 + 538 + 12 files Server functions run on every request /api/tts /api/joke /api/client-error 3 functions Metadata routes /sitemap.xml · /robots.txt · /manifest.webmanifest · /icon · /apple-icon · 9 opengraph-image routes
Four of the five groups are files by the time a reader arrives. Only the right-hand column executes code on the server, and nothing on the reading path depends on it.

Topic covers and chapters

Each of the 20 chaptered topics has its own folder with the same four files: page.tsx for the cover, [chapter]/page.tsx, [chapter]/loading.tsx and search-index.json/route.ts. The pages are a few lines each and call TopicCoverPage and TopicChapterPage in components/reader/topicPages.tsx, where the real work is — including which frame a cover gets. app/notes/page.tsx is no different from the other 19 chaptered folders. The 11 topics that have no chapters yet (Python, Java, C++, Rust, Ruby, Go, MongoDB, DBMS, Networks, Operating Systems and Claude and AI tools) have only the cover: it renders an outline with a “Being planned” note instead of a syllabus, is left out of the sitemap, and has no chapter route or search index.

A single app/[topic]/[chapter] route would have removed the repetition. What the explicit folders give instead: a topic's URL is a folder you can see, a topic can carry its own files (six of them have their own opengraph-image.tsx share card), and there is no dynamic segment at the root of the site that has to be told which names are not topics. The cost is that adding a topic means adding a folder as well as data. The folder names come from the topic's notes field, which is why JavaScript is at /notes, and tests/content.test.ts checks that no two topics resolve to the same one.

generateStaticParams returns every chapter id, written or not, so all 594 chapter pages are prerendered, outlines included. Each [chapter] route also sets export const dynamicParams = false, so an id the build did not know about is a stored 404 rather than a render. Git has the same setting on its single [section] route, one folder, one page, one long HTML string, and no [chapter] of its own. Two sibling dynamic segments do not have this yet: /problems/[slug] and /level/[topic] still default dynamicParams to true, so an unknown problem or topic id is still rendered before it 404s.

/problems, and a page per problem

/problems is built on the server from the exercise list, grouped by the chapter each exercise belongs to. Its filters (search text, topic, level, solved or not, sort order and layout) live in the query string, but not through Next's router. ProblemsView.tsx writes them with history.replaceState, fires a custom groundwork:problems-url event, and reads them back through useSyncExternalStore. Filtering never causes a navigation or a server request, and a filtered view is still a link you can share.

/problems/<id> has 538 prerendered pages, one per exercise. Each page hands the client only its own exercise and the ids and titles of its neighbours, not the list. That was a deliberate change: the commit "Load one exercise per page" records the JavaScript per page falling from 5.0 MB to 2.9 MB for a problem and from 3.5 MB to 1.3 MB for the problem list, uncompressed, because the practice pages had previously imported every exercise.

Beside each page is /problems/<id>/cases, a route handler with dynamicParams = false, so an unknown id is a plain 404 without running anything. At build time it runs the exercise's JavaScript solution against its tests and writes the recorded cases as JSON; the playground fetches it only when a reader switches a problem to a language other than JavaScript or SQL. The 538 files come to 4,166,947 bytes in the build, and 222 of them contain translatable cases. The largest, for "maximal square", is 1,002,059 bytes. It is still the right place for them: nobody who stays in JavaScript downloads a byte of it.

/practice?id=

The playground is /practice?id=free. Before every problem had its own page, a problem was /practice?id=<exercise>, and those links are still out there. They still work: PracticeClient.tsx reads id, and if it is anything other than free it shows "Opening the problem…" and calls router.replace() to /problems/<id>. The redirect has to happen in the browser, because a prerendered page cannot see its query string at build time.

/whiteboard

One prerendered page whose only content is a frame and a loading line. The board itself is imported with ssr: false, so none of its 1,851 lines run on the server; there is nothing to render until the browser has read your saved boards from localStorage.

/mock, and the question banks

/mock is prerendered with a catalog only: the 12 stages, what each one decides, and how many questions it has. The questions live at /mock/bank/<stage>, 12 static JSON files totalling 474,513 bytes. app/mock/useStageBanks.ts fetches only the stages a loop needs and caches each promise in a Map. The lobby starts those fetches when the reader points at Start, by hover or focus, and loads the room's code in idle time while they look at their plan. The commit that set this up, "Cut the mock lobby from 628 KB of JavaScript to 237 KB", records that the lobby had been fetching 462 KB of questions before the reader chose anything, and 12 KB after.

/level, /path, /review, /progress, /soon, /privacy

RouteWhat the server buildsWhat the browser adds
/level/<topic>30 pages, one per topic with levels. 5 render the three levels with chapter, minute and exercise counts; the other 25, which have nothing written, redirect to the topic's own cover instead. DSA's is also the topic's home, drawn by the topic shell: a Check my level invitation for a first-time reader, a level card and one Continue button for everyone else, a journey map of three stage cards with each chapter's state, and a row of practice, review, interview and notes tiles above a closed Plan & level panelYour progress through each level, for the 5 that render; for DSA, whether you are a first-time reader, a placed one or one with ticks, which decides what comes first on the page
/dsaNothing: the page is gone. A permanent redirect in next.config.ts sends it to /level/dsa, and the sitemap lists only the level pageNothing
/level?topic=An empty pageA redirect to /level/<topic>
/path/<topic>/<level>12 static pages: the 4 written topics (JavaScript, React, DSA, System Design) times their 3 levels. Each page's chapter metadata, exercise links and syllabus rows come from that one topic only, never the othersWhich steps are read, the progress meter, and which step the primary button opens next
/path/<topic>4 static pages, one per written topic, each an empty shellA client redirect to /path/<topic>/<level> at the reader's saved level, or Beginner if none is saved; any other topic is a 404. A request with a capital letter or a trailing slash, such as /dsa/DSA-HASHING or /dsa/, never reaches a page: proxy.ts redirects it once to the lower-case address, and a bare quiz topic straight to its level page
/path?topic=&level=An empty pageWith both a topic and a level, a redirect straight to /path/<topic>/<level> from next.config.ts, before any client code runs — unless the topic is a pinned outline one, in which case the outline redirect above still wins and sends it to the topic's own cover instead, level param or not; with only a topic, a client redirect once the saved or default level is known
/dsa/placementOne static page, noindex, whose HTML holds the site header and a skeleton, because the intro, the plan and the questions depend on the reader's storage and on ?retake, which it reads in the browser; the DSA chapter list and the level names travel in the page data and no questions are in itThe placement itself: it loads the placement questions, asks them one at a time with no timer, saves the plan in groundwork:dsa:placement and shows it, with Retake and Reset (?retake=1 starts a new run for a reader who already has a plan)
/reviewEvery written chapterWhich ones are due, from your read dates
/progressA frameStreaks, XP, badges and the calendar, all from localStorage
/soon?topic=Nothing: the page is gone. Three permanent redirects in next.config.ts cover the old links, an outline topic to its own cover, a written topic to /level/<topic>, and any other or missing topic to the home pageNothing
/privacyThe whole page: what stays in the browser, what leaves it, and which service receives itNothing

/path, /dsa/placement, /review, /progress and the bare /level are marked noindex: without your browser's data they are either empty or a duplicate of a page that is indexed. /level/<topic> is indexed, and listed in the sitemap, only for topics with something written. /privacy is static and indexed. A topic with nothing written has its own indexable URL too — /<topic>, the same cover route every topic uses — but stays noindex itself until a first chapter ships, the same rule topicCoverMetadata always applied.

Search indexes

Each topic has /<topic>/search-index.json: every chapter's title, short name, subtitle and body text, lowercased, stripped of tags, and deduplicated word by word by compactWords(). The root /search-index.json covers every written chapter on the site but only by title, short name and subtitle. The reader's search box fetches both on the first keystroke, never before. Together the 21 files are 953,236 bytes in the build; the largest is JavaScript's at 193,062 and the cross-topic one is 49,794.

The three functions

/api/tts, /api/joke and /api/client-error run on the Node runtime, each behind an in-memory per-IP limit from lib/rateLimit.ts: 40 requests a minute for speech and 20 for the rest. /api/tts answers a GET with an MP3 the CDN may cache for a year, word timings in a header, and a POST with uncacheable JSON. robots.txt disallows /api/. The endpoints chapter has the details.

Sitemap, robots, manifest and share images

These are metadata routes: TypeScript files whose default export Next turns into the file. app/sitemap.ts lists the home page, /problems, /practice, /whiteboard, every topic with something written, their level pages, every written chapter, and every problem: 755 URLs in the last production build. Outlines, /mock and the personal pages are not in it, and tests/seo.test.ts fails if an unwritten chapter creeps in. app/manifest.ts makes the site installable. app/icon.tsx and app/apple-icon.tsx draw the icons, and nine opengraph-image.tsx files, the root's and eight topic or section ones, draw the share cards through lib/og.tsx. Pages without their own card inherit the nearest one above them.

Two features share work without a server by putting the whole thing in the URL's fragment. The fragment is never sent in a request, so a static page can receive it, and nothing is stored anywhere.

  • The playground: /practice?id=free#share=…. lib/shareLink.ts packs the open files as JSON, compresses them with the browser's CompressionStream("deflate-raw"), and encodes the bytes as URL-safe base64. Reading one back keeps at most 20 files, 60 characters of each name and 200,000 characters of each file, and drops any language the editor does not know.
  • The whiteboard: /whiteboard#board=…, packed the same way by lib/whiteboard/exporter.ts. Images are dropped before packing, because a pasted photo would make the link unusable, and the share dialog says how many were left out. Reading one back keeps at most 2,000 elements.
  • Chapter anchors: a cover page runs HashRedirect, which turns /react#react-auth into /react/react-auth, so an anchor-style link to a chapter still lands on it. It ignores #top and anything that is not a plain id.

The limit of this design is the length of a URL. A large board makes a long link, and a link that contains the data cannot be edited after it is sent. For a site with no accounts, that is the right trade.

The page frames

PAGE FRAME: EVERY OTHER PAGE back · menu · brand · title the page max reading width BARE: FULL-SCREEN TOOLS menu · title · actions SiteDrawer, opened on demand: 7 links, theme, font the tool, edge to edge
The drawer on the right is drawn dashed because it is not there until the menu button is pressed. It is a portal with role="dialog", and Escape closes it. The frame on the left opens the same drawer from its own menu button.

components/frame/PageFrame.tsx is the frame nearly every page uses: a sticky header with a labelled back pill, a labelled Menu pill for the site drawer, the brand and the page title. Home, review, progress, privacy, the not-found and error pages render it directly. components/topic/TopicFrame.tsx is a thin wrapper over it for a topic: the topic's mark and name linking back to its cover. Every topic cover and chapter, written or outlined, the level and path pages, Git and the architecture guide render in it.

The playground, /problems, every problem page and /whiteboard do not. The commit "Playground and problems: no sidebar, a site menu drawer" took the old sidebar off them, and the whiteboard was built full-screen from the start. An editor, a list of 538 problems and an infinite canvas all want the width more than they want navigation that is always visible, so these pages use components/SiteDrawer.tsx instead: a Menu pill that opens a four-section drawer with a Continue card, six Go to rows, the topics, and a closed Appearance fold with the theme and handwriting choices. components/AppHeader.tsx provides the thin header and a BareShell frame, which the whiteboard uses with the header turned off. The bare frame and PageFrame never meet on one page.