Engineering pillar

Publishing Should Be a Commit

An anvil in near-darkness, a bar of glowing cyan steel on its face breaking apart into a row of luminous blocks that trail off like commits on a branch

There is a particular kind of tired that comes from logging into a content management system to publish four hundred words.

You wait for the dashboard. You dismiss the update notice. You fight an editor that has opinions about your paragraph breaks. You hunt for the field that sets the meta description, and you find it three accordions deep in a plugin that also wants to sell you something. Then you publish, and a caching layer you did not configure decides the world will see your post in about twenty minutes.

None of that is writing. All of it is administration.

The actual requirement#

Strip the tooling away and the requirement underneath is small. I want to write a file. I want that file to become a fast, correct, well-marked-up page at a durable URL. I want to be certain that it is correct before anyone can see it.

That is three things: a format, a contract, and a gate.

Everything else a CMS offers — the editor, the media library, the user roles, the revision history — is either something I do not need or something git already does better. Revision history in particular is worth pausing on. A CMS stores revisions in a database table you will never query. Git stores them in a history you already know how to read, blame, branch, and revert.

A folder is the format#

The LaForge blog is a directory:

getlaforge.com/
  site.json
  content/
    taxonomy.json
    authors/pete-corcoran.json
    posts/2026-09-05-publishing-should-be-a-commit.md

That is the entire authoring surface. Everything else under getlaforge.com/ is generated output — HTML, feeds, sitemap, social card — and is regenerated from scratch on every build. There is no state anywhere that is not a file in this repository.

The practical effect is that publishing a post is git commit. Rolling one back is git revert. Drafting one in private is a branch. Reviewing one before it goes out is a pull request. None of that had to be built, because none of it is really a publishing problem.

A definition file is the contract#

Here is the part that matters more than the format, and the part most static site generators get wrong.

Every post opens with front matter. The obvious fields are there — title, date, description, tags. But the interesting fields are these:

purpose: pillar
intent: informational
audience: engineering-leaders
cluster: software-craft
funnel: top

Those five lines are a declaration of what this post is for. Not what it is about — what it is for. Is it a cornerstone essay other posts will point back to, or a supporting piece that points inward? Is the reader looking to learn something or to buy something? Which topic cluster does this belong to? Where does it sit relative to someone deciding whether to work with us?

Most blogs never write this down. The information exists — it lives in the author's head on the day they wrote the post — and then it evaporates. Two years and eighty posts later nobody can tell you which pieces are load-bearing, which ones are orphans, or which cluster is thin enough to be worth another essay. The archive becomes a pile instead of a structure.

Writing it into the file changes what the software can do for you. The generator uses cluster and tags to pick genuinely related posts instead of guessing by recency. It uses purpose to decide which posts get a table of contents. It uses intent and funnel to choose the call to action. And because these are required fields, the question gets asked at the moment you are best equipped to answer it: while you are writing.

This is the thing I want from a definition file. Not configuration. A forcing function.

The gate is the whole point#

Automation that can publish bad work quickly is not an improvement.

So the build refuses. Before a single byte reaches S3, the generator validates the entire site and exits non-zero if anything is wrong:

  • a missing description, or one outside 70–160 characters
  • a title too long to survive the search result
  • a tag or category that is not declared in taxonomy.json
  • an image with no alt text
  • a heading level that skips from h2 to h4
  • two posts sharing a description
  • any internal link that points at a page the build did not produce

That last check is my favourite, because it is the one no human does reliably. The generator finishes writing every page, then reads all of them back and resolves every internal href against the files it actually created. A typo in a link is a build failure, not a 404 someone finds in six months.

The failure mode of a CMS is that it publishes whatever you give it. The failure mode of this is that it tells you no. I would rather argue with a build than apologise to a reader.

Deploying is not just aws s3 sync#

The last mile is where fast publishing usually dies, and it dies in a cache.

The deploy runs in two passes. Assets go up first — CSS, fonts, images — with a one-year immutable cache header. HTML, feeds, and the sitemap go up second, with max-age=0, must-revalidate. The order matters: HTML must never reach the bucket referencing an asset that is not there yet. The headers matter more: assets should be cached forever and documents should never be, and a single flat sync gets one of those two wrong no matter which header you pick.

Then only the paths that actually changed get invalidated at the CDN, parsed out of the sync output, with the listing pages that link to the new post added in. Invalidating /* on every deploy works, technically, in the way that rebooting works.

The whole thing is one command:

./laforge publish getlaforge.com -m "post: publishing should be a commit"

Check, build, commit, push, sync, invalidate. If the check fails, nothing else runs.

What this costs#

It is worth being honest about the tradeoff, because there is one.

You give up handing the keys to someone who does not use git. You give up the plugin ecosystem, which means the day you want comments or a paywall or a newsletter archive, you are building it or wiring it up yourself.

For a studio where the person writing the posts is the person who wrote the generator, that is not a cost. It is a filter. For a marketing team of nine, it would be a bad decision, and I would tell them so.

The cost people expect me to list first is writing from your phone. I left it out on purpose.

Where this goes#

The reason a CMS wins on mobile is not that it has a better editor. It is that it has a server — something already running that will accept your intent from wherever you are and act on it. A static site has no such thing, which is why "just commit it" quietly means "get to a laptop."

But a build that refuses bad input is exactly the kind of thing you can safely put a server in front of. Every rule in the previous section is a guardrail an automated caller has to satisfy too. The gate does not care whether a human or a process is on the other side of it.

A sibling project of ours already runs the other half. A background service sits on a machine and takes instructions over a chat client. You talk to one leader process in one conversation; it coordinates workers behind that conversation and decides what is worth surfacing back to you. Its own state is files on disk — records, an inbox, retros — for the same reason this blog's state is files: it is inspectable, diffable, and does not need a database to be trustworthy.

The interesting part is not the chat interface. It is what had to be built around it before that interface was safe: an allowlist for who may issue instructions, a rate limiter, a kill switch, redaction on the way out, and an append-only audit log of every exchange. That log currently sits at a little over eleven hundred entries. Giving an agent the ability to act on your behalf is easy. Giving it the ability to act on your behalf and be reviewed afterwards is the actual work.

Point that at this generator and the loop closes. Draft on your phone. Send it to the agent. The agent writes the file, runs the check, and either publishes or comes back and tells you the description is nine characters too long. You never opened a dashboard, and nothing shipped that would not have passed review.

That is the direction LaForge is going: not tools that replace the judgement in a workflow, but workflows whose judgement is written down precisely enough that something else can execute them. The definition file earns its keep twice. Today it makes the archive maintainable by a person. Later it is the thing that makes the archive maintainable by an agent, because "what is this post for" is a question a machine can act on only if somebody wrote the answer down.

The mobile limitation is real. It is also temporary, and it is temporary precisely because of the constraint that caused it.

The part that generalises#

Even if you never build this, the middle section transfers.

Write down what your content is for, in a structured field, next to the content itself. Make it required. Let the software use it. The blogs that age badly are not the ones with bad writing — they are the ones where nobody recorded the intent, and so nobody could maintain the structure.

A post you cannot classify is a post you cannot maintain. And a publishing system that cannot tell you no is not automation. It is just a faster way to ship a mistake.

Questions

Does a static blog hurt SEO compared to WordPress?

No. Search engines index HTML, not the software that produced it. A static site typically scores better because it ships less JavaScript, has no plugin bloat, and returns HTML in a single round trip.

How do you schedule posts without a server?

A nightly scheduled build runs the generator and publishes any post whose status is 'scheduled' and whose date has passed. The build is the scheduler.

What happens when a post is missing a description?

The build fails before anything reaches the bucket. Validation is a gate, not a linter you can ignore.

Can you publish from a phone without a CMS?

Not directly, but that is the point of the validation gate. Because the build refuses bad input, it is safe to put an agent in front of it: draft on your phone, send it to a background service, and let it write the file, run the check, and either publish or report what failed.

LaForge builds tooling like this for teams who would rather ship than administrate.

Read the studio notes