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.
12On this page
- The whole tree on one page
- Topic covers and chapters
- /problems, and a page per problem
- /practice?id=
- /whiteboard
- /mock, and the question banks
- /level, /path, /review, /progress, /soon, /privacy
- Search indexes
- The three functions
- Sitemap, robots, manifest and share images
- Links that live after the # sign
- 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.
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
| Route | What the server builds | What 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 panel | Your 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 |
/dsa | Nothing: the page is gone. A permanent redirect in next.config.ts sends it to /level/dsa, and the sitemap lists only the level page | Nothing |
/level?topic= | An empty page | A 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 others | Which 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 shell | A 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 page | With 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/placement | One 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 it | The 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) |
/review | Every written chapter | Which ones are due, from your read dates |
/progress | A frame | Streaks, 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 page | Nothing |
/privacy | The whole page: what stays in the browser, what leaves it, and which service receives it | Nothing |
/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.
Links that live after the # sign
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.tspacks the open files as JSON, compresses them with the browser'sCompressionStream("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 bylib/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-authinto/react/react-auth, so an anchor-style link to a chapter still lands on it. It ignores#topand 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
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.
Finished reading?
0 of 26 chapters read in this series