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.

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.

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 hrefandsrcvalues, static image imports- Strings in data arrays and template literals
- CSS colours and
url()references - Every JSON string
- For Vite: the
index.htmltitle, meta description,theme-colorand 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:
<h1>{content.page.ship_faster_break_less}</h1>
const accent = content.theme.colors.color_3b82f6;.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.mdlists every value with file, line, column, character span and owningcontent.jsonkey — so nobody greps or guesses an offset.AGENTS.mdis the entry point that says which file answers which question.change-log.txt,changes.jsonland per-fileagent-report.mdrecord 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
npm run test:all145 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.
APP_DIR=/absolute/path/to/your/app docker compose up --build # http://localhost:4310Finished this one?
Progress is saved in this browser.
