Skip to content
Andrew Higgins Writing

Writing Beehive Digital Solutions

By Full-Stack Engineer & Team Lead at Moat

The Page Is the Editor

Decap CMS hands a client a form. A florist does not want a form. She wants to click the words on her website and change them.

Every content management system I have handed to a client has the same shape. There is a sidebar of fields, a Save button, and a preview pane that is almost but not exactly the website. The developer sees a clean separation of content from presentation. The client sees a form with a field called body_2 and has no idea which paragraph that is.

Decap CMS is good software and it works exactly this way. For the four small business sites I build and host, the form was the whole problem. These are owners who update their hours, swap a photo, add a review. They are not going to learn a content model to do it.

So I rebuilt the admin so the page itself is the editor.

What the client actually gets

Opening an entry fills the window with the real page, the live one, styled exactly as a visitor sees it. Every editable region carries a faint dashed outline. You click a heading and type. Repeated things, cards, FAQs, service listings, get hover controls to add or delete one. Clicking an image opens the picker. Internal links are chosen from a menu of the site's own pages rather than typed as a path, because a client typing a path will eventually type a broken one.

The form is still there, one button away, for the handful of settings a page cannot express. It is the exception rather than the interface.

Getting content out of the markup

None of that works while copy lives in HTML. The first move is extraction: every page becomes a JSON file, shared things like contact details and nav live in one settings file, and the markup ends up with zero words in it. A build step compiles the JSON into a single content object the pages render from.

Rewriting an entire live site's markup is exactly the kind of change that quietly breaks something. So the gate for that phase is byte-identical output: dump every rendered string, every link href, every image source from the old site and the new one, and diff them. That diff caught a curly apostrophe that had been silently flattened to a straight one. Nobody eyeballing the page would have found it, and the client would have noticed eventually and assumed I was careless.

Fighting the CMS for its own interface

Making someone else's admin look and behave like yours is a knife fight. Decap injects its styles at runtime, so every rule in my stylesheet carries an important flag, which I would normally treat as a smell and here is simply the price of entry. The inline editing bridge has to find Decap's own controls in the DOM, which means matching on class fragments and in places walking React's fiber tree to find the component behind a node.

That last part is honest technical debt. Those selectors survive patch rebuilds of the same Decap version and do not survive an upgrade. So a version bump is not a version bump, it is a version bump plus running the entire test harness again. I decided that was an acceptable trade for an interface a florist will actually use, but it is a real cost and I would rather write it down than pretend it is not there.

Photos, and why they do not belong in git

Decap's git backend commits uploads into the repository. On a client site that means a three megabyte phone photo becomes a three megabyte blob in git forever, every upload triggers a full rebuild, and the picture does not appear until that rebuild finishes. The client uploads a photo, sees nothing, and uploads it again.

Decap has a seam for this: register your own media library and it hands every image click to you instead of its built in one. Uploads now go to an R2 bucket on a media subdomain, the commit carries a URL instead of a binary, and the image is live the instant it finishes uploading. The image widget still renders the same button, so the inline editing bridge that clicks that button never knew anything changed.

Three things that went wrong

Deploying the repository root once published the .git directory to the public internet. The ignore file people recommend for this does not work; the only real fix is assembling a clean output directory from an allowlist and deploying that.

A submission from inside the editor preview once sent a real lead to the contact form endpoint. Forms have to submit on the live site and be inert in the editor, and nothing tells you that until a test message arrives from a page nobody visited.

And a deploy command that uploads a version without promoting it: builds go green, the pipeline reports success, and the site never changes. That one is my least favourite category of bug, the kind where every signal says it worked.

Why it is written down as skills

Each site is its own repository, which fell out of the hosting shape rather than any grand plan: a Cloudflare Pages project per site, a Worker per media bucket. The consequence is that every fix above would otherwise have to be remembered and reapplied by hand on the next site.

So the whole thing is encoded as skills: how the admin is installed, how a blog behaves, how reviews behave, how media gets to R2. The next client site does not start from Decap's defaults and rediscover the apostrophe and the .git directory. It starts from the version where those are already fixed.

← All writing