Jerry Orta
← Blog

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: true is 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.html lands for the post, and there is nothing else to click.
  • Roll back the same way. A bad edit is one git revert away, 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.