engineering

How this blog is built

One folder per article, Markdown for each language, and a git push that puts the page online in about two minutes. No CMS, no database, no pipeline.

Two minutes, one command

The last fix to this page left a text editor, went through one command, and was online about two minutes later. Nothing in between asked anyone to log in, and no one clicked publish.

That is the point. We write about crawling Instagram, ranking creators and the things that break at three in the morning, and the writing should cost less than the work it describes. So the blog runs on the tools we already use all day: a text file, git, and a build.

37 s

Full build

Every article, every language, rebuilt from scratch.

3

Languages

English, Korean and Japanese from one folder.

0

Databases

Nothing to query, nothing to back up.

An article is a folder

Everything one article needs lives in one directory, and nothing else in the repo knows about it.

Text
apps/blog/content/how-virev-blog-works/
├── en.md
├── ko.md
├── ja.md
├── components/
│   └── DeployFlow.svelte
├── topology.svg
└── style.css

The locale files hold the prose. Beside them sits whatever that story needs and nothing more: a Svelte component written for one diagram, a stylesheet that only this page loads, a CSV that only this chart reads.1 When the story is finished, its code stops mattering to everyone else.

A language is published when its file exists and its frontmatter does not say draft: true. That single rule feeds the index, the sitemap, the RSS feed and the language links, so a folder with only en.md is an English article and there is nothing else to configure.

No CMS, and what that costs

A content system would add a login, a database, a second copy of every article and a migration the first time a field changes.2 Git already records who changed what and when, a text file diffs cleanly, and a pull request is a review. So the page you are reading is a file, and the history of this sentence is a commit.

The price is a repo checkout. Anyone who writes here needs one, and that rules out writers who do not use git.
The trade

We took that trade on purpose. Everyone who writes here already has the repo open.

From Markdown to a static page

Each .md file is compiled by mdsvex into a Svelte component, which is why a paragraph can sit next to a chart and why a file can import a component that lives beside it. SvelteKit then renders every page to HTML at build time. At request time there is no server, only files on a CDN.

Two things happen during that build that a reader notices. Code is highlighted by Shiki, so the colours arrive as plain HTML and no highlighter runs in the browser.3The entire client script on this page positions the margin notes and tracks the table of contents. An article with a chart also loads ECharts, and only then. And Markdown footnotes turn into the notes in the right margin: a small plugin replaces [^1] with a numbered reference and moves the note next to the line that cites it. The HTML is complete without JavaScript; the script only measures where each reference landed.

The rule that decides what is published is short enough to read in full:

TypeScript
export function isPublished(entry: Entry): boolean {
	return !entry.meta.draft;
}

/** Locales of one article whose file exists and is not a draft. */
export function publishedLocales(entries: Entry[], slug: string): Locale[] {
	const set = new Set(
		entries.filter((e) => e.slug === slug && isPublished(e)).map((e) => e.locale)
	);
	return LOCALES.filter((l) => set.has(l));
}
Diagram: the sieun/dev branch on GitHub feeds a Vercel project that builds apps/blog, and virev.ai rewrites /blog, /ko/blog, /ja/blog and /_blog to it.
Where the blog runs. The product site keeps every other path.

How a deploy works

Four steps, and none of them is a form.

  1. 1 commit
  2. 2 push
  3. 3 build
  4. 4 live

commit.mjs stages only the files you name.

Commit. node scripts/repo/agent/commit.mjs -m "post(<slug>): title" apps/blog/content/<slug> stages the files you name and nothing else. A dozen agents share this checkout, so a commit that grabs everything would take someone else's half-finished work with it.

Push. git push origin sieun/dev. The push must fast-forward. If it is rejected, someone else pushed first: fetch and push again. Nobody merges by hand.

Build. Vercel runs an ignored-build step first and skips the build when apps/blog did not change in that push. When it did, the whole blog is rebuilt in about forty seconds.

Live. The new build replaces the old one when it succeeds, and the URL never changes.

When it breaks

A broken article fails the build, and a failed build is never deployed. The previous version stays online and the log names the file. That is the safety net: the worst case is that a new article does not appear, not that the site goes down.

Undoing a published article is git revert and a push, or one click in the Vercel dashboard. Changing the reading page itself, the margin notes or the type scale, rebuilds every article in the same deploy, because there is one set of templates and the articles are data going through it.

Three languages, published one at a time

en.md, ko.md and ja.md share the components, the data and the diagrams, and each one goes live on its own schedule. The language switch above lists only the languages this article actually has, and the hreflang links follow the same list, so a search engine is never pointed at a translation that does not exist yet.

Writing the Korean version is copying the English file and translating the prose. The chart stays the chart.