MCP + Claude Code
Vibe Coding a Copy Editor: Recipe Edits as Pull Requests
Result:
A password-gated page where saving a recipe edit opens a real pull request against main in under a minute, with the site's existing build check and preview deploy reviewing it before anything merges, no coding agent session needed to fix a typo or reword a paragraph.
Part of a series: this assumes you already have a live Astro site on GitHub and Cloudflare Workers, from Vibe Coding 101.
Shortcut
Paste this into Claude Code, pointed at your own site’s repo, once you have the token below.
Requires manual input
A GitHub fine-grained personal access token scoped to this one repository (Contents and Pull requests, read/write, nothing else), pasted into your host’s build-time environment variables. Step 4 below covers where to click.
Add a password-gated admin page at /admin/edit-copy that lists my content
collection entries and lets me edit one entry's raw source in a textarea:
1. Reuse my site's existing login/session-cookie gate if I have one; otherwise
add a simple password-only login backed by one shared secret.
2. On save, call the GitHub REST API (plain fetch, no new dependency) to:
create a branch off main, commit the edited file to it, and open a pull
request back to main. Never commit straight to main.
3. Re-check the session cookie inside the API route itself, not just the page,
so a direct POST can't skip authentication.
4. Validate the entry slug against a strict allowlist pattern before using it
in a file path, and cap the content length.Problem#
Most of the words on this site live in Markdown files, which means most edits are small: reword a sentence, fix a typo, tighten a paragraph. None of that needs an agent to plan an approach, read five files, or run a build, but opening a coding agent session is the only tool this site had for touching those files, so a five-second fix cost a full session every time. What was missing wasn’t editing power, it was a lighter front door: something a browser tab can reach, gated the same way the rest of the site’s private pages already are, that turns a wording change directly into something reviewable.
Pattern#
1. Gate the page behind whatever login already exists#
Don’t build a second auth system for one more page. If a site already has a password-gated area
(a shared login cookie, a session token, anything that already distinguishes “you” from “everyone
else”), a new admin page just needs to sit behind that same check. Every page under /admin here
redirects to one login page when the session cookie is missing, and every page reuses the identical
check, so adding one more gated route is a one-line import, not a new system.
2. Read and write through the GitHub API, not the filesystem#
A page rendered on demand (rather than built once and served as a static file) has no local
filesystem to read from at request time, and even if it did, reading straight from main is the
better source anyway: the textarea should always show the true current content, not whatever
happened to be bundled the last time the site was built. The GitHub REST API’s Contents endpoint
covers both directions with two calls, no SDK required:
async function getFile(token: string, owner: string, repo: string, path: string) {
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=main`, {
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" },
});
const { content, sha } = await res.json();
// content arrives base64-encoded; sha identifies this exact version for the write below.
return { text: decodeBase64Utf8(content), sha };
}Base64 in, base64 out: encoding and decoding has to round-trip through raw bytes rather than a plain string conversion, or any character outside the Latin-1 range (an em dash, a curly quote) gets mangled on the way through.
3. Never commit straight to main, always open a pull request#
The whole point of moving edits out of a coding agent session is speed, but speed shouldn’t mean
skipping review. Instead of writing the edited file directly to main, create a new branch from
the current main, commit the change there, and open a pull request:
async function openEditPullRequest(token: string, owner: string, repo: string, path: string, content: string, sha: string, branch: string) {
const mainRef = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/ref/heads/main`, {
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" },
}).then((r) => r.json());
await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" },
body: JSON.stringify({ ref: `refs/heads/${branch}`, sha: mainRef.object.sha }),
});
await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`, {
method: "PUT",
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" },
body: JSON.stringify({ message: `Copy edit: ${path}`, content: encodeBase64Utf8(content), sha, branch }),
});
const pr = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" },
body: JSON.stringify({ title: `Copy edit: ${path}`, head: branch, base: "main" }),
}).then((r) => r.json());
return pr.html_url;
}If a repository already runs a build check and a preview deploy on every pull request, that CI now reviews every copy edit too, for free: a broken frontmatter field or a malformed content file fails the same check a code change would, and gets caught before a merge, not after.
4. Wire the token as a build-time secret, and fail closed if it’s missing#
The one new credential this needs is a GitHub personal access token, and it should be a fine-grained one, scoped to one repository with two permissions: Contents (read/write) and Pull requests (read/write). Create it under GitHub’s Settings → Developer settings → Fine-grained tokens, then set it as a secret environment variable in your host’s dashboard, never committed to the repo itself. Read it the same way any other server-only secret already gets read, and check for its presence before doing anything: an unset token should disable the page with a plain message, not silently fall back to something insecure or crash with a raw stack trace.
5. Re-check authentication inside the API route itself#
The page that renders the form isn’t the only way to reach the endpoint that opens a pull request: a direct request straight to that API route bypasses any check that only runs on the page. Read the same session cookie and check it against the same secret a second time, inside the API handler, before touching the GitHub API at all.
Output#
Build this and a copy fix stops requiring a coding agent session: open the page, edit the text, save, and a pull request is waiting with a preview link and a build check already running against it. This exact tool is running on this site right now, gated behind the same login as every other admin page here, and the pull request that shipped it followed the same path this recipe describes: a branch, a commit through the Contents API, a merge only after its own preview deploy was reviewed. The only manual step left is the merge click, which stays manual on purpose: it’s the one place a human confirms the wording reads the way it was meant to before it goes live.
Variations#
- Extend past one content type. This pattern generalizes to any file a content collection loads from, not just one collection. A second admin page (or a dropdown on this one) can point at a different directory the same way, as long as the file being edited is plain text a person can safely retype by hand.
- Add a live diff before saving. Showing the old and new text side by side, or a rendered preview of the Markdown, catches a broken heading or a dropped closing tag before it ever reaches a pull request, not just before it merges.
- Rate-limit or log who saved what. A shared password only identifies “someone with the password,” not a specific person. A team giving this page to more than one editor should log each save (who, when, which file) the same way any other content-management system would.