← Posts

Day 11: a CMS for a writer who isn't me

Today I built a narrator-script editor and gave the URL to a creative freelancer. She types into a text box on the web. She hits Save. About three minutes later the live game says her words. Nobody pushes anything. I am not in the loop.

Why a CMS, and why someone else

The narrator is the moat. I have said this since day one. It is also the part of the game I am least good at. I can engineer the system that delivers the lines. I cannot reliably write 115 lines of paternal-disappointed AI voice without sounding like I am trying.

A friend who actually writes for a living offered to take a pass. I said yes the same day. Then I had to figure out how to give her access without handing her a Godot project and a copy of git.

That meant a CMS. An editor on the web, behind a password, that writes commits to the repo on her behalf. She never opens the engine. I never copy-paste her text into .tscn files. The pipeline does both.

What she sees

The Atlas was already there. I built it weeks ago as a read-only browser of every narrator line in the game, organised by mission and zone, cross-referenced with the tone notes and the easter-egg policy. It runs as a single HTML file, edits live in localStorage, and used to export a JSON file I would manually apply.

Today I made three changes. The atlas is now password-gated with its own login (guest / a password I will give her). Every line card has an Edit button that opens a textarea. The Save button at the top no longer downloads a file. It posts the edits to the Cloudflare Worker that fronts the game and the Worker commits them to the repo.

I also rewrote the visual layer. The mission tabs read as "🖥️ M1 · Cradle / the data center" instead of "M1", with little icons per zone, so somebody who has never seen the design doc can still tell what they are looking at. The tape collectibles get their own card style now because the player-facing cassette name and the narrator's later reaction to the tape were two different things and the old card collapsed them into one quoted block. New writer would have thought the quoted line was the cassette name and "fixed" it. Caught it before sending her the URL.

How saves reach the game

Here is the pipeline I ended up with.

  1. She edits in the browser, hits Save.
  2. The Worker accepts the JSON, commits it into the repo at tools/narrator-edits/incoming-<timestamp>.json via the GitHub API.
  3. A new GitHub Action wakes up on that commit. It applies the edits to the actual source files. Godot is installed in the runner, re-exports the game headlessly, and the rebuilt build gets committed back to the repo.
  4. Cloudflare auto-deploys the rebuilt site.
  5. She refreshes the live URL. Her words are in the game.

Round trip is about three minutes. Five if the CI runner has not cached the Godot binary from a previous run. I tested it once by saving a fake edit and watching the Actions tab. It worked on the second try. First try crashed because the runner did not have sharp (image library) and Godot's headless export skipped every asset because .godot/imported/ was empty. Two lines of YAML fixed both. The whole loop now runs without me.

The Cloudflare cache fight

This is the texture of the day. Most of the actual work was not the pipeline. It was a two-hour argument with Cloudflare's edge cache.

The Worker is supposed to check the password before serving the editor. The first version did. I tested. Locked out. Good. Then I tested with the correct password. Got in. Good. Then I tested with the WRONG password again. Still got in.

What happened is that the first successful login response got cached at Cloudflare's edge, and after that every request to that URL was served the cached HTML directly without ever asking the Worker whether the request was authorised. The cache was sitting between the user and my code, doing its job too well.

I tried four different fixes. Renaming the URL. Telling Cloudflare not to cache the response. Telling Cloudflare not to even look at static files with that name. Each fix worked for one path and exposed another. The thing that finally worked was embedding the editor HTML directly into the Worker's source code as a string. No file in the static folder. No cache layer to bypass. The Worker is the only way the HTML exists, and the Worker checks the password every time.

I learned more about Cloudflare's caching model in those two hours than I have all year. None of it was material I was looking for.

The other things that happened

While I was waiting for the CI to deploy, I cleaned up three smaller items.

The drone music in the game is gone. I had been ambivalent about it for weeks. Today I said it out loud, "I hate that drone," and ripped it. The piano layer that sits on top is what stays. The drone code is still there as a no-op for any old save file that still references it, but no audio plays and the toggle is hidden from the settings menu. Twenty-three megabytes of OGG files no longer ship to mobile.

The default character is now the red-beanie vector one. I have six character slots, three pixel and three vector. The red beanie was always the one that looked most like a person trying to break in, which is what the game is about. I made it the cold-start default for new players. Existing saves keep whatever they had.

I built a light theme for the Atlas because the dark one was making my eyes hurt after eight hours of staring at it. It is GitHub-light palette, the toggle remembers, and the writer will see the light theme first.

One thing I want to remember

The freelancer has not used the editor yet. She gets the URL tomorrow. I keep wanting to call this end-to-end, automatic, done. It will not be done until she actually saves a line, the line reaches the live game, she sees it, and she tells me whether the experience felt like "I edited a Google doc" or "I filed a ticket with engineering". That is the only test that matters.

If it works the way I hope, the game gets a real narrator voice without me typing any of it. If it doesn't, I will know within a week.

Comments

Anyone can post. Captcha keeps the bots out. No links allowed.

0 / 800