Skip to content

Repository files navigation

@charlescms/astro

Static-first visual editing for Astro sites. Free and open source (MIT).

⚠️ Early days (0.x). CharlesCMS is young and under active development — some things may be rough or not work on every site yet. Source edits are byte-verified and conservative by design, but please test on a branch first and report anything that breaks. APIs may change before 1.0.

Quickstart — edit in 60 seconds

Requires Node.js 22.12 or newer and Astro 6.2. No account, no service, no config.

npm install @charlescms/astro
// astro.config.mjs
import { defineConfig } from "astro/config";
import charlesCMS from "@charlescms/astro";

export default defineConfig({ integrations: [charlesCMS()] });
npm run dev

Open /cms, click Start editing locally, then click any text or image on the page to change it. No schema, no content models, no setup.

In npm run dev your changes are written straight to your real source filessrc/pages/index.astro, your Markdown, your data — byte-for-byte and conservative by design. Press Save to local files, then commit with your own Git when you're happy:

git add -A && git commit -m "Edit copy in CharlesCMS"

That's the whole loop: click → edit → Save → git commit. No GitHub App, no Cloudflare, nothing to deploy — this alone is a complete local visual editor.

Want non-technical people to edit the live site themselves (no local checkout, no Git)? That's the optional connector below — about two more minutes.

Publish from your live site (optional)

This deploys the connector, not your website. Your site stays hosted wherever you already put it (Vercel, Netlify, Cloudflare Pages, GitHub Pages…). The connector is a tiny extra Cloudflare Worker whose only job is to commit editors' changes to GitHub — then your existing host rebuilds the site as usual. The loop is: edit in /cms → connector commits to GitHub → your host redeploys → live.

Local editing needs nothing. To let people edit your live site — each save committed to GitHub for you — deploy that connector. You need a GitHub repo and a free Cloudflare account (it fits the Workers free tier). Nothing to install first — npx fetches the tools; just say yes if it offers to install one.

One-time setup (~5 min). Do it in this order so you never have to come back:

  1. Put your site in a GitHub repo and deploy it to your host (push → it builds; any host works — see Deploy your site). You now have your site's URL.
  2. npx wrangler login — opens a browser to sign in to Cloudflare.
  3. npx charlescms setup --deploy --open — answer the prompts; when it asks for your live site URL, paste the one from step 1 (and set an editor password). A browser opens to create the GitHub App (click create); it then deploys the Worker and prints your connector URL.
  4. Click the install link it prints, to add the GitHub App to your repo.
  5. On your live site, open /cmsConnect a publisher → paste the connector URL, repo (owner/name), and password.

Now Edit → Publish → live — each Publish commits to GitHub and your host rebuilds. Verify anytime: npx charlescms doctor <connector-url>.

Not deployed yet? You can still set up the connector — leave the live-site URL blank, finish, then add it later by re-running npx charlescms setup --deploy once your site is live (it remembers your answers). Deploying first just saves that second trip.

Skip the form: bake the connection into astro.config so editors land straight on Start editing (only the password is asked):

charlesCMS({ connector: "https://<worker>.workers.dev", repo: "owner/name", branch: "main" })

Prefer clicking / no CLI? See Manual setup.

CLI reference

npx charlescms setup [--deploy] [--open]   # scaffold the connector (and deploy)
npx charlescms doctor [connector-url]      # check files + a live connector verify
npx charlescms --help                      # all commands and options
npx charlescms --version

setup detects your repository's default branch automatically, so edits target the right branch (main, master, …) without you having to pick. --open opens the GitHub App page for you; --deploy also stores the Worker secrets and runs wrangler deploy.

Deploy your site

Publish commits to GitHub, so any host that rebuilds on push works — Cloudflare Pages, Netlify, Vercel, GitHub Pages, or your own CI.

Cloudflare keeps it simplest. The connector already runs on Cloudflare Workers, so hosting the site on Cloudflare Pages keeps everything on one account and the same wrangler CLI. Connect the repo in the Cloudflare dashboard — build command npm run build, output directory dist — or push straight from the CLI:

npm run build
npx wrangler pages deploy dist

Each Publish commits to GitHub; Pages rebuilds automatically. Edit → Publish → live.

The site is a standard static Astro build (npm run builddist/), so any static host works — only the connector requires Cloudflare Workers. When you host elsewhere, add that site's URL to the connector's ALLOWED_ORIGINS.

Editor password

Protect a public editor with one password — the Worker secret AUTH_SECRET. Editors enter it once on /cms.

  • Set / change: in the assistant, npx wrangler secret put AUTH_SECRET, or the Cloudflare dashboard (Settings → Variables and Secrets). Takes effect immediately.
  • Rotate: replace it with npx wrangler secret put AUTH_SECRET.

The connector fails closed: with no AUTH_SECRET set it refuses every edit (503), so a misconfigured deploy can never publish openly — always set one. The secret is sent as a header over HTTPS and compared in constant time; GitHub credentials stay in the Worker (never in the site or localStorage).

It's a single shared editor identity (no per-user audit; revoke by rotating). For teams / per-user auth on higher-value sites, put the Worker behind Cloudflare Access, or add GitHub OAuth in the authorizeRequest() hook (connector/worker.js), plus a Cloudflare rate-limit rule on /api/*.

Add sections (optional)

By default the editor changes existing content in place. Optionally, let editors insert whole new sections between developer-approved children. Mark the wrapper:

<main
  data-charlescms-sections
  data-charlescms-section-class="prose section-shell"
  data-charlescms-section-tools="header,paragraph,list,image,quote,delimiter"
>
  <Hero />
  <Features />
  <Testimonials />
</main>

That one attribute is the whole decision: sections can be inserted exactly where the developer placed the wrapper, and nowhere else — pages without it never show an insert control. The wrapper's direct children must be static markup (dynamic .map() children are skipped).

  • data-charlescms-section-class — class(es) every inserted section receives, so new sections inherit the site's own spacing and look.
  • data-charlescms-section-tools — which blocks editors may use (header,paragraph,list,image,quote,delimiter).

Offer branded templates as static snippets in src/charlescms/templates/ (recommended — you own the markup and classes). Every .html or .astro file there becomes a choice in the picker; the filename is the label (team-quote.html → “Team Quote”):

<!-- src/charlescms/templates/newsletter.html -->
<h2 class="display-lg">Stay in the loop</h2>
<p>Occasional notes from the studio.</p>
<a class="button" href="/newsletter">Subscribe</a>

Templates must be pure static markup: files containing scripts, frontmatter, {expressions} or components are skipped with a dev-server warning (a section is inserted verbatim as page content, so nothing dynamic can ride along).

What the editor experiences:

  1. Add section here appears before, between, and after the wrapper's sections.
  2. Clicking it offers your templates; choosing one stages the section as a live draft on the page. Clicking the draft offers Remove section — nothing touches the repository until Publish.
  3. Publish commits the section as sanitized, static Astro source (wrapped in a durable data-charlescms-block). From then on it is ordinary page content: its text and images are edited right on the page, like everything else, and the whole section can be removed again at any time.

There is deliberately no free-form block builder: editors compose pages from sections you designed, and edit content in one consistent way — on the page.

What it can edit

Everything visible is edited by clicking it on the page. Page info in the toolbar holds the fields that render nowhere visible — the Google/browser-tab title, the meta description, and other frontmatter.

  • Static text & headings, and safe inline rich text (bold, italic, inline code, links, line breaks)
  • Markdown frontmatter and body — incl. Astro Content Collections (.md, .mdx, and JSON type: 'data' collections)
  • Data rendered with .map() — arrays of objects and positional tuples ([["Name","Desc","8"], …]): names, descriptions, prices, list items
  • Config values (site.phone, address, hours, …) and component props (hero titles, section eyebrows) — clickable on the page, edited safely at source
  • Navigation menus — labels edit; destinations stay locked
  • Links & downloads — link text edits; PDFs/files replace in place
  • Images, video, audio, embeds, iframes — upload, swap, bounded remove
  • New sections (see above) and versions / undo via Git commits

Rich vs plain is decided by how your site renders a value: where it interprets formatting (a Markdown body, set:html), you get full rich text; where it prints raw text ({value}), it stays plain so formatting can never show as literal characters. Pure logic — loops, conditions, computed expressions — is never editable (that's code, not content); clicking such generated content shows a calm "generated by the page, can't be edited here" hint instead of doing nothing. Every write is verified against the exact source bytes and refused if the source changed, so an edit can't silently corrupt surrounding code. Edited values are escaped as content — including the {/} Astro reads as expressions — so typed text stays text and can't be reinterpreted as code; a downstream astro build remains the final check on any committed file.

CharlesCMS scans your source to know what's editable — automatically on save in dev, at build time in production (rebuild to pick up new content). It works on any Astro site, with or without a layout, alongside React/Svelte/Vue islands (those files are never touched).

Options

charlesCMS({
  // Connection (optional — bake in for one-click "Start editing"):
  connector: "https://<your-worker>.workers.dev",
  repo: "owner/name",
  branch: "main",
  sourceRoot: "website",             // monorepos only

  editablePaths: ["/demo", "/blog"], // limit where the editor activates
  adminPath: "/cms",                 // change the editor route
})

Monorepos: sourceRoot is the app folder holding the Astro project, so src/pages/index.astro commits as website/src/pages/index.astro.

Manual setup

Skip the assistant and set up the Worker by hand:

  1. Install + configure as in Quickstart.

  2. Create the Worker:

    mkdir -p connector
    cp node_modules/@charlescms/astro/connector/worker.js connector/worker.js
    # connector/wrangler.toml
    name = "<your-worker>"
    main = "./worker.js"
    compatibility_date = "2026-06-04"
    
    [vars]
    ALLOWED_ORIGINS = "https://www.yoursite.com"   # comma-separated; add http://localhost:4321 for dev
    ALLOWED_REPOS = "owner/name"
    DEFAULT_BRANCH = "main"
    cd connector && npx wrangler login
  3. Store secrets (GitHub App with contents: write + metadata: read, installed on the repo) and the editor password:

    npx wrangler secret put GITHUB_APP_ID
    npx wrangler secret put GITHUB_PRIVATE_KEY
    npx wrangler secret put AUTH_SECRET   # required — the connector refuses to act without it
  4. Deploy and verify:

    npx wrangler deploy
    npx charlescms doctor https://<your-worker>.workers.dev

Troubleshooting

npx charlescms doctor <url> maps each failure to a fix:

Symptom Likely cause Fix
401 on publish or in doctor Missing/wrong editor password Use the value matching the Worker's AUTH_SECRET (or set it with wrangler secret put AUTH_SECRET)
403 "not allowed" Origin/repo not allow-listed Fix ALLOWED_ORIGINS / ALLOWED_REPOS in wrangler.toml, redeploy
403 from GitHub App lacks access, or stale Worker Install/grant the GitHub App, then wrangler deploy
404 GitHub App not installed on the repo Install it (doctor prints the exact github.com/apps/<app>/installations/new link), or check the repo path

How it works & safety

The editor ships as static HTML/JS, reads a build-time source map, and stages edits as a live preview. In npm run dev with no connector, Save applies the edit to your local source file on disk through a dev-only endpoint — the same byte-verified transform, no network. With a connector, Publish sends the exact source span to the Worker, which commits to GitHub — credentials stay in the Worker, never in the site or localStorage. Either way the file is re-read first and the edit refused if the source changed, and rich text and section HTML are re-sanitized before the complete updated file is written.

esbuild dev-server pin (Windows only)

A path-traversal advisory (GHSA-g7r4-m6w7-qqqr) affects esbuild's development server on Windows in versions before 0.28.1. It is dev-only and Windows-only — it does not affect your built or published site. Vite 7 still permits esbuild@0.27.x, so if you run astro dev on Windows, pin the patched release in your site's root package.json and reinstall:

{ "overrides": { "esbuild": "^0.28.1" } }

You can drop this once your installed Vite requires esbuild >= 0.28.1.

Checks

npm test
npm run test:coverage
npm run test:e2e
npm run build

GitHub Actions runs the coverage and browser suites on Linux plus the packed package smoke test on ubuntu-latest, macos-latest, and windows-latest. Each job installs the tarball in a clean Astro project, runs the CLI setup, builds, starts the dev server, and checks both / and /cms.

Runtime structure

The browser editor is split by responsibility:

  • src/client.js is the composition root: shared state, module wiring, session helpers, and the direct Tiptap imports required for reliable Vite file: installs.
  • src/runtime-controller.js, src/source-map-runtime.js, and src/edit-affordance.js own startup, source-map binding, and click/hover behavior.
  • src/element-editor.js, src/rich-text-editor.js, and src/section-editor.js own the three editing experiences.
  • src/publishing.js owns staging, publishing, uploads, replacements, and bounded media removal.
  • src/content-panel.js owns the Page info (frontmatter/SEO) and data forms; src/content-bridge.js maps rendered data to those deterministic forms.
  • src/panel-manager.js, src/toolbar.js, src/versions-panel.js, and src/editor-styles.js own the surrounding editor UI.
  • src/analyzer.js handles Astro markup while src/data-analyzer.js handles conservative JavaScript/TypeScript/JSON data collections. Both read source through src/js-ast.js, a small layer over the Babel parser that locates string literals at exact offsets (any nesting depth) without evaluating code.

All source writes remain byte-verified in the shared source-editing layer; the DOM bridge only chooses which deterministic editor to open.

License

CharlesCMS is free and open source under the MIT License — use it for anything, including commercial work, no fees and no keys. Third-party dependency licences are summarized in THIRD_PARTY_NOTICES.md.

Everything self-hosts on your own Cloudflare and GitHub.

About

Source-native live CMS editor for Astro sites.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages