Developer Experience (DX) Automation Static Architecture

Git Is Your CMS: A Content Workflow That Scales Without a Database

Static Signal
A massive clockwork printing press powered by glowing gears and branching pipelines, dark steampunk machinery converting raw materials into published pages

Every CMS you’ve ever used is trying to rebuild features that Git already has.

Versioning? Git invented it. Branching? Git’s entire model. Collaborative editing with review gates? That’s a pull request. Rollback? git revert. Audit trail? git log. Publishing workflow? That’s CI/CD.

And yet teams keep reaching for admin panels, database-backed content platforms, and SaaS dashboards to manage what amounts to a folder of text files. They’re paying $300/month for a worse version of something they already have installed on every development machine.

If your content is files — markdown, MDX, HTML templates, JSON — then Git isn’t just your version control. It’s your CMS. And when you build the right workflow around it, it’s better than any admin panel you’ve ever used.


Why Traditional CMS Workflows Break

The typical CMS workflow looks clean on paper: log in, write content, hit publish. But once you’re past a single author on a personal blog, it starts to crack.

Staging is a mess. Most CMS platforms handle staging by either giving you a “draft” toggle or requiring a separate staging environment that drifts out of sync with production. Want to preview three different content changes together before any of them go live? Good luck coordinating that through an admin panel.

Rollback is terrifying. Something broke after the last content update? In WordPress you’re restoring a database backup. In a headless CMS you’re hoping the revision history goes back far enough and praying the API-level rollback doesn’t corrupt related entries. Neither option inspires confidence at 11pm on a Friday.

Permissions don’t scale. CMS role systems are coarse. You get “admin,” “editor,” and “author” — maybe a few custom roles if you’re on the enterprise tier. But you can’t say “this person can edit blog posts but not landing pages” without hacking together a custom permission layer. And you definitely can’t say “changes to the pricing page require approval from marketing and legal before publishing.”

There’s no real review process. An editor hits publish and the content is live. Maybe there’s a notification. Maybe someone glances at it after the fact. But there’s no structured review, no diff of what changed, no approval gate that blocks publication until someone signs off.

Git solves every single one of these problems. Not with plugins or enterprise tiers — with the features it ships by default.


Branches as Drafts

A branch is a draft. Not metaphorically — functionally. When a content author creates a branch, they get an isolated workspace where they can make changes without affecting anything in production. They can edit ten pages, add images, restructure a section — and none of it exists outside that branch until they decide it’s ready.

git checkout -b content/spring-product-launch

That branch is now the staging environment for the spring launch content. The author works on it. Pushes commits. Other people can check it out and see the work in progress. And main stays exactly as it is.

This is what CMS platforms try to replicate with draft modes and scheduled publishing. The difference is that Git branches are composable. You can have five branches for five different content initiatives, all in flight at the same time, and merge them in any order. Try doing that with a “Save as Draft” button.


Pull Requests as Editorial Review

This is where Git-as-CMS starts to outperform every admin panel on the market.

A pull request is a structured editorial review process. The author opens a PR, and the diff shows exactly what changed — every word added, every word removed, every image swapped. Reviewers leave inline comments on specific lines. The author responds, makes revisions, pushes new commits. The review history is permanent.

Compare that to a CMS where someone hits publish and you get a Slack message that says “I updated the pricing page.” What did they change? Was it reviewed? Who approved it? Nobody knows unless someone manually checked.

For content teams, the PR workflow gives you:

  • Line-by-line diffs of every content change
  • Inline comments attached to specific content, not floating in a Slack thread
  • Required approvals before anything merges to main
  • A permanent record of who approved what and when

You enforce this with branch protection rules. In GitHub, go to Settings > Branches > Branch protection rules and require:

  • Pull request reviews before merging
  • At least one (or two) approving reviews
  • Specific reviewers for specific paths (more on this below)

No content reaches production without passing through review. That’s not a feature you’re bolting on — it’s how the system works by default.


CODEOWNERS: Per-Path Approval Rules

Here’s where it gets powerful. GitHub’s CODEOWNERS file lets you require specific reviewers based on which files a PR touches. For a content workflow, this means you can route different content to different approvers automatically.

Create a file at .github/CODEOWNERS:

# Blog posts require editorial review
/content/posts/                    @company/editorial-team

# Landing pages require marketing approval
/content/pages/landing/            @company/marketing-team

# Pricing and legal pages require both marketing and legal
/content/pages/pricing.md          @company/marketing-team @company/legal-team
/content/pages/terms.md            @company/legal-team
/content/pages/privacy.md          @company/legal-team

# Site configuration requires a developer
/config/                           @company/dev-team
/content/navigation.json           @company/dev-team

Now when a marketing intern opens a PR that edits the pricing page, GitHub automatically requests reviews from the marketing and legal teams. The PR can’t be merged until both approve. No custom permission layer. No enterprise CMS tier. Just a text file in your repo.

This is more granular than any CMS role system I’ve seen. You’re not assigning permissions to user roles — you’re assigning review requirements to content paths. The pricing page has different rules than blog posts, which have different rules than the navigation config. And it’s all version-controlled, auditable, and changeable via PR.


CI/CD as the Publish Button

In a Git-based content workflow, merging to main is the publish action. That merge triggers a CI/CD pipeline that builds your site and deploys it. The content is live within minutes — often seconds.

Here’s a GitHub Actions workflow that handles the build and deploy:

name: Build and Deploy

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - run: npm ci
      - run: npm run build

      - name: Deploy to production
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          command: pages deploy out --project-name=my-site

Swap the deploy step for whatever hosting you use — Render, Vercel, Netlify, an S3 bucket. The pattern is the same: push to main, build runs, site deploys. The CI pipeline is your publish button, and it’s more reliable than any “Publish” button in a CMS because it runs the same build process every single time.


Content Validation in CI

Here’s something no traditional CMS gives you: automated content validation that runs before anything gets published. In your CI pipeline, you can check frontmatter, validate links, enforce image sizes, and lint prose — all before the PR can be merged.

A frontmatter validation script:

import { glob } from "node:fs/promises";
import { readFile } from "node:fs/promises";
import matter from "gray-matter";

const required = ["title", "slug", "excerpt", "publishedAt", "heroImage"];
let failed = false;

for await (const file of glob("content/posts/**/*.md")) {
  const raw = await readFile(file, "utf-8");
  const { data } = matter(raw);

  for (const field of required) {
    if (!data[field]) {
      console.error(`Missing "${field}" in ${file}`);
      failed = true;
    }
  }

  if (data.slug && !/^[a-z0-9-]+$/.test(data.slug)) {
    console.error(`Invalid slug format in ${file}: "${data.slug}"`);
    failed = true;
  }

  if (data.title && data.title.length > 100) {
    console.error(`Title too long in ${file} (${data.title.length} chars)`);
    failed = true;
  }
}

if (failed) process.exit(1);
console.log("All frontmatter valid.");

Add it to your CI pipeline alongside other checks:

name: Content Validation

on:
  pull_request:
    paths:
      - 'content/**'

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - run: npm ci

      - name: Validate frontmatter
        run: node scripts/validate-frontmatter.js

      - name: Check for broken links
        run: npx broken-link-checker-local ./content --recursive

      - name: Enforce image sizes
        run: |
          find content -name "*.webp" -o -name "*.png" -o -name "*.jpg" | while read img; do
            size=$(stat -f%z "$img" 2>/dev/null || stat -c%s "$img")
            if [ "$size" -gt 500000 ]; then
              echo "Image too large: $img ($(($size / 1024))KB, max 500KB)"
              exit 1
            fi
          done

Now a PR that’s missing frontmatter fields, has broken links, or includes a 2MB hero image gets blocked automatically. The author sees the failure in the PR checks, fixes it, pushes again. No editor needs to manually check formatting. No broken content reaches production.

This is the kind of guardrail that CMS platforms charge enterprise pricing for — if they offer it at all. In a Git workflow, it’s a CI job.


Preview Deployments for Content Review

Most modern hosting platforms can deploy preview builds for every PR. This means your content reviewer doesn’t need to clone the repo and run a local build — they get a live URL with the proposed changes rendered exactly as they’ll appear in production.

With Vercel or Netlify, this is automatic. Every PR gets a unique preview URL. With Cloudflare Pages, it’s built in. With Render, you configure preview environments. The specifics depend on your host, but the workflow is the same:

  1. Author creates a branch and opens a PR
  2. CI builds the site from that branch
  3. A preview URL is posted as a comment on the PR
  4. Reviewers click the link and see the actual rendered content
  5. They approve or request changes directly in the PR
  6. On merge, the production build deploys

Your marketing lead doesn’t need a GitHub account to review content. Send them the preview link. They see the page exactly as it will look in production. They reply in whatever channel you use — the developer translates feedback into PR comments or pushes fixes directly.

This workflow is faster and more accurate than any CMS preview mode. CMS previews are approximations rendered by the CMS platform. Preview deployments are the real site, built with the real build process, running on real infrastructure.


Rollback Without Panic

Something went wrong with the last content publish. In a CMS, you’re now digging through revision histories, hoping the restore function actually works, worrying about relational content that might not roll back cleanly.

In Git:

git revert HEAD
git push origin main

That’s it. The revert creates a new commit that undoes the last change. CI picks it up, builds, deploys. Your site is back to the previous state in under two minutes. The bad change is still in the history — you can inspect it, understand what went wrong, and fix it properly later. Nothing is lost.

Need to revert something from three days ago without undoing the changes that came after it? Git handles that too:

git revert a1b2c3d

Reverts just that commit. Everything else stays. Try doing that in a CMS where content changes are stored as opaque database mutations.

The confidence this gives you is hard to overstate. Deploying content changes goes from an anxiety-inducing event to a routine operation because you know — with certainty — that any change can be undone in seconds.


The Content Lifecycle

Here’s the full lifecycle of a piece of content in a Git-based workflow, from idea to published:

1. Idea — An issue is created in GitHub Issues or your project tracker. The content topic, target audience, and key points are documented.

2. Draft — A branch is created. The author writes the content as a markdown file with proper frontmatter. They commit as they go — each commit is a save point.

git checkout -b content/git-as-cms-article
# Write the post
git add content/posts/git-is-your-cms.md
git commit -m "draft: git as CMS article"
git push origin content/git-as-cms-article

3. Review — The author opens a PR. CI runs validation checks — frontmatter is correct, links work, images are optimized. A preview deployment goes live. CODEOWNERS triggers automatic review requests. Reviewers leave feedback inline.

4. Revision — The author addresses feedback, pushes new commits. The preview deployment updates automatically. Reviewers re-review.

5. Approval — Required reviewers approve the PR. All CI checks pass.

6. Publish — The PR is merged to main. CI builds the production site and deploys. The content is live.

7. Monitor — If something’s wrong, git revert and push. The site rolls back in under two minutes.

Every step is tracked. Every change is attributed. Every decision is documented in PR comments. Six months from now, you can look at any piece of content and trace its entire history — who wrote it, who reviewed it, what feedback was given, when it was published, and whether it was ever rolled back.

No CMS gives you that level of traceability without an enterprise contract.


Multi-Author at Scale

The objection I hear most is: “This doesn’t work for non-technical authors.” Fair. If your content team can’t use Git, this workflow isn’t for them — at least not directly.

But for teams where the content authors are developers, technical writers, or anyone comfortable with a text editor and a basic Git workflow, this scales remarkably well. Here’s why:

No bottleneck. Ten authors can have ten branches open simultaneously. There’s no database lock, no CMS rate limit, no “someone else is editing this page” error.

No merge conflicts on content. Different authors working on different markdown files never conflict. The only time you’d get a merge conflict is if two people edited the same paragraph of the same post — and Git shows you exactly what happened so you can resolve it in seconds.

Permissions are granular and auditable. CODEOWNERS handles review routing. Branch protection handles merge gates. Repository access controls handle who can push at all. Every permission is a line in a config file, not a checkbox buried in a CMS admin panel.

The workflow self-documents. New team members read the CODEOWNERS file and immediately understand who reviews what. They look at the CI pipeline and understand what validation runs. They open a closed PR and see exactly how the review process works. The workflow teaches itself.


The Tools You Already Have

Take stock of what Git gives you without installing anything:

  • Versioning — every commit is a snapshot
  • Branching — isolated workspaces for drafts
  • Diffing — see exactly what changed
  • Blame — know who wrote every line
  • Revert — undo any change instantly
  • Log — complete history of every content change
  • Tags — mark releases or content milestones
  • Hooks — run validation on commit or push

Add GitHub (or GitLab, or Bitbucket) and you get:

  • Pull requests — structured review with inline comments
  • CODEOWNERS — automatic reviewer assignment by path
  • Branch protection — required reviews and status checks
  • Actions/CI — automated build, validation, and deploy
  • Issues — content planning and tracking
  • Preview deployments — live previews for every PR

Add your hosting platform’s CI integration and you get:

  • Automatic production deploys on merge
  • Preview deployments on PR
  • Instant rollback via revert

That’s a complete content management system. No database. No admin panel. No monthly fee. No vendor lock-in. Just files, version control, and automation you configure once and never think about again.

The next time someone asks what CMS you use, the honest answer might be: Git. And that’s not a compromise. It’s an upgrade.


Static Signal is published by Neuron Web Development.