All notes

How the blog editor works

This blog is static files, but I write it from a graphical editor in the browser — one that commits straight to GitHub. Here's the loop that makes both true at once.

The site you're reading has no server, no database, and no build step. It's HTML, a set of CSS token files, and a little JavaScript. That's exactly what I wanted for the reading side — nothing to keep alive, nothing to patch. But the same choice makes writing painful, because a static site normally means editing raw HTML by hand every time you have a thought.

So the authoring lives in a separate tool that never gets deployed: editor.html. It's gitignored, it stays on my machine, and it's the only "dynamic" thing in the whole project. Everything it produces is still plain static files.

Data files, not hard-coded pages

The trick is that the pages render themselves from data. The landing page fetches two JSON files at load — posts.json for the post index and site.json for the site chrome (the intro heading, the blurb, the nav links) — and builds the DOM from them. Each post also exists as its own standalone HTML file under /posts/, generated from the same fields.

That means publishing is just two writes: append the post to posts.json, and drop a matching posts/<slug>.html. Editing the homepage is one write to site.json. No rebuild, because there's nothing to build.

The editor writes to GitHub directly

When I hit publish, the editor doesn't save to disk — it talks to the GitHub Contents API and commits the files to the repository. A read fetches the current file and its sha; a write sends the new content, base64-encoded, with that sha to overwrite (or no sha to create). The host redeploys on push, so a publish goes straight to the live site.

Omit the sha and you create a file. Include the right sha and you overwrite it. Send the wrong one and GitHub rejects the write rather than clobbering someone else's change. The sha is the whole safety model.

A couple of small things matter more than they look. All text is base64-encoded through a Unicode-safe wrapper, because a plain encode chokes the first time you type an em-dash or an accented name. And every value that comes out of JSON and into HTML is escaped on the way in — the post body is authored HTML, but titles, tags, and nav labels are treated as untrusted text so a stray angle bracket can't become markup.

The token stays in the browser

The editor authenticates with a fine-grained GitHub token, scoped to this one repository, with permission to read and write contents and nothing else. It lives in the browser's localStorage and is never committed anywhere. I generate it myself and paste it in; the tool never creates one. If it leaks, the blast radius is one blog repo.

Why bother

The payoff is a genuinely graphical writing experience — a WYSIWYG body, a live preview, a tabbed panel for editing posts on one side and the site's own chrome on the other — sitting on top of output that stays boringly, durably static. Effortless input, static output. The same pattern runs the gallery, where the editor also downsizes photos in the browser before it commits them. It's the piece that makes the whole hand-built ecosystem something I'll actually keep using.