When I started Interview Forge (it was called CS/LAB back then), every guide was a TypeScript object. It worked for ten topics. At a hundred, every typo fix was a code change, every new phase meant editing three files, and reviewing content meant reading diffs full of braces and quotes.

The fix was not a CMS. It was a boring, strict pipeline: YAML in, validated JSON out, React only renders.

The shape of the pipeline

Content lives in a content/ folder that anyone can edit:

Folder What it holds
site.yaml Navigation, home page copy, footer, SEO text
tracks/*.yaml Learning tracks, their phases, and topic order
guides/<track>/*.yaml One file per written guide
articles/**/*.yaml Long-form articles like this one

A single Node script reads everything, validates it, and writes JSON into src/generated/. It runs automatically before dev, build, and test, so the site can never render content that failed validation.

Validation is the real feature

YAML is friendly until it is not. An unquoted colon turns a sentence into a map, and a stray comma splits a table cell in two. Instead of discovering that in production, the build fails with a message that names the file and the fix:

content/guides/system-design/http-status-codes.yaml:
  "overview" item 1 is not text (a line containing ": " must be wrapped in quotes)

Every rule I learned the hard way became a check: required fields, minimum numbers of examples, links to topics that actually exist, and unique article ids.

Scaling to thousands of articles

Articles needed two extra properties: shareable URLs that never break, and pages that stay fast as the archive grows.

Each article gets a random 12-character id when it is created. The URL combines a readable slug with that id:

/articles/how-i-made-a-learning-platform-where-every-page-comes-from-yaml-4048f9616dc1

The page looks up the article by id only. If I later rename the article, old links still resolve and permanently redirect to the new canonical URL. The same trick makes a bare id work as a short link.

Load only what you render

The build writes a small index (title, date, tags, reading time) plus one JSON file per article body. The list page filters and paginates the index on the server. An article page reads exactly one body file. A thousand articles cost a thousand small files, not one giant bundle.

What I would tell my past self

  • Put content in files the moment you have more than a handful of pages.
  • Make the build fail loudly; friendly error messages are documentation.
  • Give every shareable thing a permanent id that is independent of its title.
  • Keep React components dumb. They should render data, not own it.

The best content system is the one where adding a page is a text edit and a pull request.

Adding an article here is now one command:

npm run new-article -- --title "My next article" --tags "System Design, AI"