Publishing by Pull Request
I never wanted a CMS for this site. A CMS is a second source of truth — a
database, an admin login, a preview environment — bolted onto a codebase that
already versions everything it needs. So the writing you're reading lives where
the rest of the site lives: as Markdown files in the same Git repository, built
by @analogjs/content at compile time.
Where a post actually lives
A post is a file. The one you're reading is a Markdown file committed to this
site's Git repo, published at /blog/publishing-by-pull-request. It opens with a
small block of YAML front-matter — the title, the date, the tags — and everything
below the second --- is the body. There's no row in a database and no draft
sitting in someone else's cloud; the file is the record.
At build time the content plugin reads every file under src/content, parses
the front-matter into a typed object, and renders the body to HTML with
marked. The listing page asks for the collection; the
detail page asks for one entry by its slug. Both are just typed data.
The typed part
The front-matter isn't loose — it's an interface the pages compile against, so a
post missing its date or tags fails the build instead of shipping broken:
export interface BlogPost {
title: string;
description: string;
date: string;
tags: string[];
draft?: boolean;
slug?: string;
}
That single type is the contract shared by the Markdown, the listing, the detail page, and the static prerender. Change the shape in one place and the compiler tells me every post and page that no longer agrees.
Why the pull request is the publish step
Because a post is a file, publishing is a commit — and a commit belongs on a branch, in a pull request. That turns writing into the workflow I already trust for code:
- Draft in the open. A post with
draft: trueis filtered out of the listing and skipped by the prerender, so work-in-progress can sit on a branch without leaking to the live site. - Review the prose like code. A diff of a paragraph reads exactly like a diff of a function — line by line, with history.
- Ship atomically. Merging the PR is the publish button. The static build
regenerates, a new
index.htmllands for the post, and there is nothing else to click. - Roll back the same way. A bad edit is one
git revertaway, not a frantic click through an admin panel.
No preview server to babysit, no separate deploy for content, no drift between
what's in the repo and what's on the page. The tools are the ones I'd reach for
anyway — an editor, git, and a pull request — and the framework stays pure:
standalone components, signals, and a build step, with nothing heavier wedged in
between the writing and the page.
That's the whole system. Author a file, open a PR, merge it. The publish button
was git the entire time.