Galle, LK--:--5/6 sites live

React Content Studio: Editing a Codebase's Copy Without an LLM

Babel AST extraction with byte offsets, context-correct escaping, re-parse-or-refuse writes, an 18-tool MCP server and 145 end-to-end tests on real bytes.

27 Jul 20264 min read#Babel#AST#Developer Tools#MCP
React Content Studio

Every agency has this conversation. The client wants to change a headline. The headline is a string in app/page.tsx. The options are: give a non-developer access to the repo, wire the whole site to a headless CMS, or have a developer make a two-word change and redeploy.

React Content Studio is a fourth option. Point it at a React, Next.js or Vite codebase and it extracts every non-tag thing — copy, image references, links, colours — into a live editor. Change a value and it's written back into the exact character span it came from.

Nothing in it uses a model. That's the point.

Bundled sample app


How extraction works

The scanner parses each file with @babel/parser into a real AST, plus dedicated readers for CSS, JSON and HTML. It collects:

  • JSX text, alt / title / aria-* attributes
  • href and src values, static image imports
  • Strings in data arrays and template literals
  • CSS colours and url() references
  • Every JSON string
  • For Vite: the index.html title, meta description, theme-color and favicon

Every value knows its file, line, column and byte offsets. That's why edits are exact and reversible rather than a global find-and-replace.

Why AST, not a headless CMS

A headless CMS asks you to refactor components into fetching hooks and pay a subscription forever. It also moves the content away from the code reviewers see. Content Studio leaves the code as it is and produces a clean, minimal git diff for every edit — a reviewer sees "Ship faster" → "Ship safer" on one line of one file.

Why no AI

An LLM asked to "change the hero text" might also reformat the file, rename a prop or drop an import. Here, a write is a byte-range replacement with context-correct escaping:

  • JSX text wraps to {"…"} when the new value needs it
  • JSX attributes use HTML entities
  • JS strings match the existing quote style

After every write the file is re-parsed. Anything that wouldn't parse is refused and the file is left untouched. You cannot break the build from the text box.


Features that came out of exact spans

Swap a logo without touching code. Replace an image's bytes while keeping its path and filename; every component that used it keeps working.

Rebrand in one click. Colours are deduplicated across TSX, CSS, JSON and HTML. One swap rewrites every occurrence, one write per file.

Deterministic bulk changes. A command bar with a small grammar — sub "Acme" => "Globex" in src/ — matches against the extracted index, not raw text, so it can never rename a variable or break an import.

Next mode. Writes a copy of the project where every value comes from content.json:

ExampleTSX
<h1>{content.page.ship_faster_break_less}</h1>
const accent = content.theme.colors.color_3b82f6;
ExampleCSS
.btn { background: var(--color_3b82f6, #3b82f6); }

Colours become shared tokens; every distinct image path becomes one key under assets. The original project is never modified.


Built for agents, too

The same live index is exposed three ways: a browser UI, an HTTP API, and an MCP server with 18 tools over stdio. Attach an agent to a running studio and its edits appear live in your open browser tab. For agents that read files instead:

  • CONTENT-MAP.md lists every value with file, line, column, character span and owning content.json key — so nobody greps or guesses an offset.
  • AGENTS.md is the entry point that says which file answers which question.
  • change-log.txt, changes.jsonl and per-file agent-report.md record every change with before/after, backup path and a proposed variable name — with a verdict on whether it's a legal identifier.

It also renders a typeset PDF report — charts, palette, every change, the variable plan — from a PDF writer written against the spec, with no dependency.

Tested on bytes, not mocks

ExampleShell
npm run test:all

145 end-to-end checks — 89 through the HTTP API, 56 through a real stdio JSON-RPC MCP session. Each boots a server against a throwaway copy of a sample app and asserts on the bytes that end up on disk. Nothing is mocked. The whole thing has four runtime dependencies: express and three @babel packages.

Limits, stated up front

  • Next mode doesn't rewrite HTML — a document can't import a JS module.
  • A colour inside a longer string ('1px solid #fff') is editable but not variablised.
  • Template literals with ${…} are editable piece by piece, not extractable as one variable.
  • Import specifiers, TypeScript types and directive prologues are never touched.

Each is reported explicitly rather than failing quietly.

ExampleShell
APP_DIR=/absolute/path/to/your/app docker compose up --build   # http://localhost:4310

See the case study →