Skip to content
Blog
LeetLLMContent EngineeringAI EngineeringProduction+1

How We Built LeetLLM

Inside LeetLLM's Git-backed curriculum: reviewable article bundles, generated figures, user-owned state, and the checks between a source edit and a deployed page.

13 min read
On this page

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 describes the implementation checked against the repository on October 10, 2026. Production behavior needs its own live verification.

The local curriculum loader returns 197 visible lessons across 15 phases. Blog posts, glossary entries, and coding practice are separate collections. The engineering problem is making each lesson easy to correct while keeping its examples, navigation, and rendered output in sync.

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 structure as the curriculum:

This post's bundle
how-we-built-leetllm/
├── content.md                 # JSON frontmatter and published text
├── _research.md               # raw editorial background, not rendered
├── social_banner.png          # share image
├── illustrations/
│   ├── content_pipeline.tsx
│   ├── illustration_system.tsx
│   └── _generated/            # theme variants and alt.json
└── diagrams/
    └── _generated/            # PNGs built from Markdown Mermaid fences

loadContentBundle() reads the JSON 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 and regenerate the PNG. If a paper doesn't support a sentence, change the claim or its citation. The bundle gives those edits a common review boundary; review each change for accuracy.

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 requires a build and release because Git owns the canonical content. The shared page holds the lesson; Supabase holds each reader's bookmarks and notes. Which source owns that state?

Public content and account state have different owners

Two readers can see the same canonical lesson and have different bookmarks, notes, and progress. Keep those personal fields in user-scoped storage rather than the shared article artifact.

Two separate data paths. Git bundles pass through the content loader to canonical lessons. Supabase user rows pass through scoped queries to personal study state. User rows do not feed the shared canonical content artifact. Public comments are outside this diagram.
Canonical content and personal study state meet in the application and keep separate sources of truth. This diagram omits publicly readable comments, which have a separate access policy.
DataOwner in the implementationAccess boundary
Canonical lessons, blog posts, citations, visual sourceRepository bundles and shared content filesPublished material is readable without an account
Progress, plans, bookmarks, notes, quiz attemptsSupabase PostgresUser-scoped policies compare the row owner with the authenticated user
Personalization profiles and generated lesson variantsSupabase PostgresPer-user state; Git remains the canonical shared source
Comments and votesSupabase PostgresPublic reads; ordinary writes are tied to the signed-in owner

expresses these database policies. Inspect each policy before describing its access: 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 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.3.6 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. Measure throughput and hosting cost separately from these implementation details.

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. The endpoint searches lessons and excludes the blog. Because the index scores normalized words rather than semantic similarity, conceptual synonyms can be missed. Add a new search backend when observed failures show that this approach falls short.

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. An earlier version of its raw research packet described a strict JSON article-spec system and implied that validation prevents broken pages from reaching readers. Neither sentence became reliable just because it appeared in the briefing. The September review corrected those notes against the implementation: 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. Verify “comments are private” in the SQL policy and “a failed cache purge rolls back the release” in the deployment function and its callers. First-party implementation claims need evidence from the implementation.

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 judges whether the example helps the reader understand them; counts of headings, citations, and illustrations track coverage. A figure can drift in a more mechanical way too. The sentence changes, and a copied PNG still shows the old arrow. Which source generates the light and dark pages?

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.

One TSX illustration source enters a raster builder that produces dark and light PNG files. Both asset URLs are exposed by the content loader. The client Markdown renderer chooses the active variant without rebuilding the illustration.
The builder creates both assets. The loader attaches both URLs, and the client renderer selects the active one. Theme selection doesn't require rerendering the illustration on the server.

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.

That query identifies an image URL; the serving route validates its format and reads the current file at that path. It stores no previous PNG. A cache hit for an old URL may return old bytes, while a miss after a new revision may return new bytes under that URL. The byte-derived query separates fresh page requests. Reproducing a page's historical images requires versioned storage or the corresponding retained PNG.

To rebuild the figures in this post from the repository root, the actual scoped command is:

Rebuild this post's figures
pnpm --dir web illustrations:build:article blog/how-we-built-leetllm --force

A 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.

CheckA defect it can catchReview still required
Metadata and curriculum loadingInvalid frontmatter or a roadmap/content mismatchWhether the title and placement suit the reader
Reference validationA citation key missing from the shared registryWhether the source supports the nearby claim
Marked Python examplesCode that fails or displayed output that no longer matchesWhether the example models the intended behavior
Illustration checks and buildsMissing outputs, bad references, or layout warningsWhether the figure teaches anything accurately
Vitest and Next.js buildTested behavior or content assembly that breaksWhether 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, 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 must include article assets

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. Validate the production container as its own artifact; it runs with a smaller file set than the development checkout.

The Docker build compares the selected runtime content with the source and writes a manifest of file sizes and SHA-256 hashes. Before deploying, the release script resolves the pushed image to an immutable digest and runs runtime-image:validate against that exact image. The check requires the manifest to match the deploy checkout and verifies that the image has no missing, extra, or changed content files. It runs with no network and a read-only container filesystem. This proves a packaging match for that source snapshot. Editorial review assesses whether the lesson's explanation is correct.

Consider changing a figure's caption while keeping an old generated PNG. The runtime manifest will faithfully include that caption and those old pixels. The image check can pass because both match the checkout. Rebuild from the visual source and inspect the page to check whether the caption and drawing explain the same thing. Packaging and editorial checks establish separate contracts.

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:

Diagram showing Candidate checks, Keep old revision, Route 100% to new, and Production checks.
Candidate checks, Keep old revision, Route 100% to new, and Production checks.

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 is best-effort: missing purge credentials skip it, and an API response without a success result produces a warning. A transport failure can still abort the shell script. Check cleanup outcomes separately after the smoke tests pass.

HTML caching is a separate release concern

The source configuration sets public learn, blog, and practice routes to this policy:

Configured public-page cache policy
Cache-Control: public, s-maxage=14400, stale-while-revalidate=3600, max-age=0

The intended shared-cache freshness interval is four hours; max-age=0 asks browsers to revalidate stored responses immediately. Verify edge behavior by inspecting route rendering, final response headers, and Cloudflare configuration. Cloudflare's current Origin Cache Control documentation says s-maxage prevents stale serving without revalidation. An extra stale-delivery hour therefore depends on the actual edge behavior under this configuration.[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, verifies the exact image's content manifest, and executes candidate and production checks. It can therefore package stale diagram PNGs that match the checkout. Treat a fast deploy as packaging and runtime evidence; run the full content review separately.

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.

People still need to read the result. Valid source can mislead. A figure needs to teach accurately, and a release needs supported prose. Our tooling makes problems easier to locate; reviewers still decide and correct them.

Share this article