Correct a sentence in a lesson and the work can look finished. But the diagram may still describe the old explanation. Its caption may contradict both versions. A reader may receive an older page from the cache, pointing to an older image.
LeetLLM's architecture has to keep those pieces connected. The canonical curriculum lives in Git, figures are built from source, and deployment packages the pages with their assets. User progress and other account data take a separate path through Supabase. This post explains that implementation, checked against the repository on September 2, 2026, rather than claiming that every configured behavior has been verified in production.
The local curriculum loader returns 196 visible lessons across 15 phases. Blog posts, glossary entries, and coding practice are separate collections. The interesting engineering problem isn't the count. It's making one lesson easy to correct without leaving its examples, navigation, or rendered output behind.
A directory per lesson
A lesson needs more than a body field. It has citations, diagrams, runnable examples, and metadata that affect how readers find it. We keep those dependencies close enough to review together.
This post uses the same article bundle structure as the curriculum:
1how-we-built-leetllm/
2├── content.md # JSON frontmatter and published text
3├── _research.md # raw editorial background, not rendered
4├── social_banner.png # share image
5├── illustrations/
6│ ├── content_pipeline.tsx
7│ ├── illustration_system.tsx
8│ └── _generated/ # theme variants and alt.json
9└── diagrams/
10 └── _generated/ # PNGs built from Markdown Mermaid fencesloadContentBundle() reads the JSON frontmatter and body from content.md. It still supports older bundles with a separate metadata.json. The lesson and blog loaders then derive reading time, resolve citation keys through the shared references.json registry, and turn local image references into serving URLs.
That makes a correction a source change. If an arrow points the wrong way, edit the TSX or Mermaid definition, not the generated PNG. If a paper doesn't support a sentence, change the claim or its citation. The bundle gives those edits a common review boundary; it doesn't make them correct automatically.
Curriculum order has a different owner. ROADMAP_SECTION_SEQUENCE and SECTION_ROADMAP_SLUG_ORDER in questions.ts define the path through the lessons. Folder names don't decide what comes next. The loader checks that the visible content and the roadmap agree, so moving a lesson is also a navigation change.
Files in Git let the explanation, its visual source, and its place in the curriculum appear in the same diff. Publishing a correction also requires a build and release, not just updating a database row.
Public content and account state have different owners
The canonical lesson can be the same for two readers while their bookmarks, notes, and progress differ. Those aren't fields we want to bake into a shared article artifact.

| Data | Owner in the implementation | Access boundary |
|---|---|---|
| Canonical lessons, blog posts, citations, visual source | Repository bundles and shared content files | Published material is readable without an account |
| Progress, plans, bookmarks, notes, quiz attempts | Supabase Postgres | User-scoped policies compare the row owner with the authenticated user |
| Personalization profiles and generated lesson variants | Supabase Postgres | Stored per user rather than replacing the canonical Git source |
| Comments and votes | Supabase Postgres | Public reads; ordinary writes are tied to the signed-in owner |
Row-level security expresses these database policies, but “uses RLS” isn't a synonym for “private.” Our comments schema explicitly allows public reads. A service-role client can also have broader authority than an ordinary signed-in client, so server-side code still needs to use the appropriate identity and scope.[1]
The app layer is Next.js and React with TypeScript, Tailwind CSS, and shared UI components. The repository currently pins Next.js 16.2.11 and React 19.2.6. Supabase handles authentication and persisted user data. A standalone Next.js container runs on Cloud Run, with Cloudflare in front of the public site. These are the implementation choices, not claims about measured throughput or hosting cost.
Search deliberately stays close to the canonical source. learn-search.ts builds an in-process index from getQuestions(), scoring normalized titles, descriptions, metadata, and body text. It removes fenced code and display math before scoring the body. There is no embedding model or separate search service in that path, and /api/search/learn returns Cache-Control: no-store.
That is enough machinery for lexical matching, with limitations we can explain: the endpoint searches lessons, not the blog, and a conceptual synonym won't match merely because the meanings are similar. A new search backend would be justified by demonstrated search failures, not by the presence of AI in the curriculum.
Research notes are allowed to be wrong
Each bundle's _research.md is background for the editor. It can contain an earlier draft, suggested coverage, and model-generated source suggestions. It isn't rendered, and it isn't treated as verified evidence. Current repository guidelines require reviewing one complete bundle at a time and preserving raw research unless a refresh is explicitly requested.
This post supplies a useful example. Its raw research packet describes a strict JSON article-spec system and implies that validation prevents broken pages from reaching readers. Neither sentence is a reliable description just because it appears in the briefing. The current implementation has JSON frontmatter, specific validators, and human review. Those are narrower, checkable claims.
Reviewing first-party architecture also needs different evidence from reviewing an external paper. The source for “comments are private” is the SQL policy, not a general Supabase tutorial. The source for “a failed cache purge rolls back the release” is the deployment function and its callers, not Cloud Run's feature list. In both cases, inspecting the code changes the sentence we should publish.
For teaching material, the same discipline goes beyond fact checking. “Retrieval improves accuracy” needs a workload, a source set, and an evaluation that can detect stale or irrelevant evidence. RAG Evaluation develops those distinctions. A reviewer still has to decide whether the example helps the reader understand them. No count of headings, citations, or illustrations establishes that.
One figure source, two theme variants
A theme-aware illustration shouldn't require maintaining two separate drawings. Our TSX figures use Vizmatic components, rendered through Satori and Resvg into light and dark PNGs. Mermaid fences take a separate Puppeteer-based rendering path and also produce both themes.

The distinction between building and selecting is small but important. resolveIllustrations() emits one image element with dark and light source attributes. The Markdown renderer uses the resolved client theme to choose the displayed source. The loader doesn't know the reader's active theme; its initial fallback is dark.
The illustration cache tracks source hashes and shared rendering inputs. A changed figure rebuilds; changed framework inputs can invalidate the whole set; missing expected outputs also trigger rebuilding. Unchanged source alone isn't sufficient to reuse a missing PNG.
There's another cache boundary after rendering. contentImageUrl() adds a version query derived from the image bytes. If a figure's pixels change, its serving URL changes even though the local filename stays the same. That helps a fresh page request the new asset. It doesn't update old cached HTML that still contains the previous reference.
To rebuild the figures in this post from the repository root, the actual scoped command is:
1pnpm --dir web illustrations:build:article blog/how-we-built-leetllm --forceA successful render proves the source could become an image. We still inspect both outputs for clipping, misleading arrows, unreadable labels, and diagrams that merely repeat the paragraph beside them. Alt text and captions remain necessary because a raster image doesn't expose its relationships as semantic HTML.
A green check needs a precise meaning
Our validators are useful when their names describe the evidence they produce. Reference validation can establish that a citation key exists. It can't establish that the cited paper supports the conclusion. Python-example validation can execute marked code and compare displayed output. It can't turn a simulated model response into evidence about a live model.
| Check | A defect it can catch | Review still required |
|---|---|---|
| Metadata and curriculum loading | Invalid frontmatter or a roadmap/content mismatch | Whether the title and placement suit the reader |
| Reference validation | A citation key missing from the shared registry | Whether the source supports the nearby claim |
| Marked Python examples | Code that fails or displayed output that no longer matches | Whether the example models the intended behavior |
| Illustration checks and builds | Missing outputs, bad references, or layout warnings | Whether the figure teaches anything accurately |
| Vitest and Next.js build | Tested behavior or content assembly that breaks | Whether the rendered page is coherent and usable |
We run narrower checks while editing and broader checks before release. A full deploy invokes metadata, reading-time, citation, structure, style, curriculum-handoff, mastery-quiz, and marked-Python checks; practice-solution validation; and Vitest. It builds illustrations, runs visual and cover checks, and renders diagrams before building the application container.
Those checks don't all apply equally to every page. This architecture post doesn't need a mastery quiz or an artificial Python example to explain its design. The lesson contracts and the blog's editorial job are different.
The container needs the content, not just the app
Next.js standalone output is only part of the runtime artifact. Our Dockerfile also copies a selected set of content files: Markdown, metadata, references, illustration source, generated images, and local assets. Content-serving routes and personalized visual rendering still need those files after the build. Raw research packets aren't part of that selected runtime-content set.
That packaging detail is easy to miss in a file-backed site. A build can succeed locally because the whole checkout is present, then fail in a smaller production image because a route expects a file that wasn't copied. The production container is therefore a distinct thing to validate, not just another way to start the development server.
The release script deploys a Cloud Run revision with --no-traffic and a temporary tag. The tag provides a candidate URL for checks without moving the service's ordinary traffic. Cloud Run explicitly supports this separation between deploying a revision and assigning it traffic.[2]
At that point, the traffic decision has two stages:

Candidate checks cover public content routes and authenticated Leety integration. If they fail, the script leaves ordinary traffic on the previous revision. If they pass, it routes 100% to the new revision, attempts a Cloudflare purge, and repeats checks through the public URL. Failed production checks trigger an attempt to restore the previous revision, followed by another purge attempt if rollback succeeds.
Rollback can fail, and the script reports that failure. Cache purge isn't a transactional release gate: missing purge credentials cause it to skip, and an API response without a success result produces a warning rather than an automatic rollback. A transport failure can still abort the shell script. Passing the smoke tests doesn't guarantee that every cleanup operation succeeded.
HTML caching is a separate release concern
The source configuration sets public learn, blog, and practice routes to this policy:
1Cache-Control: public, s-maxage=14400, stale-while-revalidate=3600, max-age=0The intended shared-cache freshness interval is four hours; max-age=0 asks browsers to treat their stored response as immediately stale. But a header in next.config.mjs isn't proof of observed edge behavior. Route rendering, final response headers, and Cloudflare configuration all matter. Cloudflare's current Origin Cache Control documentation also says s-maxage prevents stale serving without revalidation, so the stale-while-revalidate=3600 text shouldn't be read as a guaranteed extra hour of stale delivery.[3]
The normal Supabase auth-cookie path in proxy.ts sets private, no-store, must-revalidate after session handling. That's the application-side intent to keep authenticated responses out of shared caches. Checking the actual response at the public URL is still necessary when changing authentication or caching behavior.
Moving Cloud Run traffic and refreshing cached HTML are separate operations. A new origin revision doesn't by itself remove an old edge response. Image versioning helps the new HTML address new pixels, while purge and response checks address which HTML readers receive.
There is also a deliberately weaker release mode. ./deploy.sh --fast skips pre-deploy content checks, practice validation, Vitest, illustration validation, cover checks, and Mermaid rendering. It still runs the illustration builder, builds the container, and executes candidate and production checks. It can therefore package stale diagram PNGs. A fast deploy isn't evidence that the full content review passed.
What we'd keep even in a smaller site
The stack could change without changing the useful design decisions: keep a lesson's dependencies reviewable, distinguish canonical content from user-owned state, derive assets from source, and give each validator a limited claim it can actually establish.
The part that shouldn't be automated away is reading the result. A source can be valid and misleading. A figure can fit its frame and explain nothing. A release can serve successfully and still contain an unsupported sentence. Our tooling makes those problems easier to locate; the review still has to fix them.