@synapsmedia/ghost-theme-editor
v0.4.3
Published
A browser-based Ghost theme editor injected into Ghost Admin via clientExtensions.
Readme
Ghost Theme Editor
A browser-based Ghost theme editor that injects itself into Ghost Admin and lets you edit, create, rename, and delete theme files directly in your browser — no ZIP downloads, no local tooling, no redeploy cycle. Useful for live fixes, quick tweaks, and authoring themes on any device.
- Monaco editor — the same editor engine as VS Code, with syntax highlighting for Handlebars, CSS, JavaScript, JSON, YAML, and more.
- Diff + review workflow — inspect per-file diffs in the editor and review all changed files from a dedicated changes panel before uploading.
- Full file management — right-click any file or folder in the tree to create, rename, or delete. New files open immediately for editing.
- Default theme support — Ghost's built-in themes (
casper,source) can't be overwritten directly; the editor automatically uploads edits as a new<theme>-editedtheme. - Single JS file — no runtime CDN dependencies beyond Monaco (loaded from jsDelivr on first open). Drop one URL into Ghost config and you're done.
- Same-origin auth — reuses Ghost Admin's existing session cookie; no re-authentication.
- Non-destructive — Ghost automatically validates the theme before replacing it. The editor confirms every upload and keeps unsaved changes visible until you explicitly discard them.
Usage
[!TIP] If you are hosting your Ghost site on Synaps Media, Ghost Theme Editor is already installed. Enjoy!
The package is published to npm as @synapsmedia/ghost-theme-editor and served via jsDelivr — no build step needed.
1. Add to Ghost config
Open config.production.json and add:
{
"clientExtensions": {
"script": {
"src": "https://cdn.jsdelivr.net/npm/@synapsmedia/ghost-theme-editor@0/dist/ghost-theme-editor.js",
"container": "<div id=\"ghost-theme-editor-root\"></div>"
}
}
}To pin to a specific version (recommended for production stability):
"src": "https://cdn.jsdelivr.net/npm/@synapsmedia/ghost-theme-editor@{version}/dist/ghost-theme-editor.js"[!WARNING] Change
{version}with your desired version, like0.4.3.
2. Restart Ghost
ghost restartThen hard-reload Ghost Admin (⌘⇧R / Ctrl⇧R) to pick up the new config.
3. Verify
Open Ghost Admin → Settings → Design & branding → Change theme → Installed. Expand any installed theme's "…" menu. You should see Edit in Browser above the existing Download item.
Open DevTools → Console to confirm:
[ghost-theme-editor] 1.0.0 ready — open a theme's "…" menu to see the editor.How it works
┌──────────────────────────────────────────────────────────────────┐
│ Ghost Admin (authenticated session) │
│ │
│ ┌────────────────────────┐ ┌──────────────────────────┐ │
│ │ Installed Themes list │ │ ghost-theme-editor.js │ │
│ │ (admin-x React) │ │ │ │
│ │ │ │ ├─ MutationObserver │ │
│ │ …menu → Download │◄──────►│ ├─ Injects "Edit in │ │
│ │ Edit in │ │ │ Browser" button │ │
│ │ Browser ✱ │ │ ├─ Downloads theme ZIP │ │
│ └────────────────────────┘ │ │ via Admin API │ │
│ │ ├─ Extracts in memory │ │
│ ┌────────────────────────┐ │ │ (JSZip, bundled) │ │
│ │ Full-screen modal │◄──────►│ ├─ Edit modal: │ │
│ │ tree │ Monaco editor │ │ │ tree + Monaco │ │
│ │ │ │ │ ├─ Repack + upload │ │
│ │ Save & Upload │ │ │ to Admin API │ │
│ └────────────────────────┘ │ └─ Ghost replaces theme │ │
│ │ (old version kept │ │
│ │ as backup) │ │
└──────────────────────────────────────────────────────────────────┘Ghost's clientExtensions.script config lets an operator specify a JavaScript file that Ghost Admin loads for authenticated users. See ANALYSIS.md for the full trace of Ghost's codebase and the contracts this extension depends on.
Requirements
- Ghost 6.x
- An authenticated Ghost Admin session. The script loads only for authenticated users — Ghost's
showScriptExtensiongetter checkssession.isAuthenticated && session.user. - Ability to edit Ghost's
config.production.json(or equivalent env vars).
Using the editor
- Click Edit in Browser from any theme's "…" menu.
- The editor downloads the theme ZIP via
/ghost/api/admin/themes/<name>/download(session cookie, same origin — no re-auth required). - The file tree on the left shows every file. Editable text files (
.hbs,.css,.js,.json,.md,.txt,.svg,.yaml, …) are clickable. Binary files (images, fonts, WOFF) appear grayed out and are preserved byte-for-byte through the round trip. - Click any editable file to open it in the Monaco editor. A dot next to the filename marks it as modified. Use the breadcrumb's Edit / Diff toggle to inspect a per-file diff.
- Right-click any file or folder in the tree for a context menu:
- On a folder: New File, New Folder, Rename, Delete
- On a file: Rename, Delete
- On the tree background: New File, New Folder (created at the root)
- New files open immediately in the editor if the extension is editable.
- Creating a folder adds a
.gitkeepplaceholder so it appears in the tree.
- Click the toolbar's modified-files badge to open the all-changes review panel.
- Review changed files in a list with status badges (
added,modified,deleted). - Open any file in a split diff viewer.
- Revert individual files or use Revert all.
- Review changed files in a list with status badges (
- Click Save & Upload when ready.
- You'll be asked to confirm the number of changed/added/deleted files.
- The modified tree is repacked as a ZIP (JSZip, DEFLATE).
- The ZIP is uploaded as
multipart/form-datato/ghost/api/admin/themes/upload. - Ghost renames the existing theme folder as a backup, extracts the new ZIP, and re-activates the theme if it was active.
- If you're editing a default Ghost theme (
casper,source), the upload is redirected to<theme>-edited— a new theme is created with your changes while the original is left untouched.
- Cancel / press Esc / click the backdrop to close. Unsaved changes prompt for confirmation before discarding.
Architecture
src/
├── index.js — bootstrap; starts the button injector
├── api/
│ ├── paths.js — subdir + apiRoot detection (mirrors Ghost's getGhostPaths)
│ ├── download.js — fetch theme ZIP → ArrayBuffer
│ └── upload.js — POST FormData with zipBlob + correct filename
├── zip/
│ ├── extract.js — JSZip → { files: { path: {editable, content|binary, …} }, rootPrefix }
│ └── repack.js — file tree → Blob (preserves root prefix, binaries, permissions)
├── editor/
│ ├── editable.js — extension whitelist + language detector
│ ├── loadMonaco.js — loads Monaco Editor from jsDelivr CDN (AMD, blob-worker pattern)
│ ├── FileTree.js — collapsible tree, right-click context menu, dirty markers
│ ├── FileEditor.js — Monaco editor pane, per-file models, breadcrumb, language badge
│ └── EditorModal.js — top-level controller: download → extract → edit → repack → upload
└── ui/
├── injectButton.js — MutationObserver, Radix popover detection, button injection
├── icons.js — inline SVG icons
├── styles.js — inlines modal.css via ?raw
├── modal.css — scoped .gte-* classes, dark theme, responsive
└── toast.js — post-close success/error notificationsDesign principles:
- No framework. The injected script never assumes Ember or React is running. It reads only stable, public-looking DOM attributes:
id="theme-<name>",data-testid="popover-content", the Radixaria-controlslinkage. - Idempotent injection.
startButtonInjection()guards onwindow.__gteInjectorStarted, and every popover injection checks for a pre-existing[data-gte-edit-button]before inserting. - Binaries are untouched.
extract.jssplits files intoeditable(UTF-8 text) andbinary(Uint8Array).repack.jswrites binaries directly — images, fonts, and other assets survive unchanged. - Monaco loaded lazily. Monaco is loaded from jsDelivr on the first time the editor is opened, not on page load. The CDN fetch is kicked off as a prefetch when the editor module initializes, overlapping with the theme ZIP download.
- Filename matters. Ghost's upload handler derives the theme name from the uploaded filename.
upload.jsalways sets it to<themeName>.zip.
Self-hosting
If you prefer to host the bundle yourself (e.g. to avoid CDN dependencies or pin offline):
npm install
npm run buildThis produces a self-contained IIFE at dist/ghost-theme-editor.js. JSZip is bundled in; the modal CSS is inlined. Monaco itself is always loaded from jsDelivr at runtime — it is intentionally not bundled due to its size (~3 MB).
Host the file at any HTTPS URL and point clientExtensions.script.src at it. No CORS configuration is required since it loads as a plain <script src>.
Development
npm install
npm run dev # vite build --watch (writes dist/ on every change)
npm run build # production minified bundle
npm test # zip round-trip tests (vitest)Point clientExtensions.script.src at http://localhost:<port>/ghost-theme-editor.js during development. You'll need to serve dist/ separately (e.g. npx serve dist), since Vite's dev server targets HTML entry points rather than raw IIFEs.
Troubleshooting
Edit in Browser button doesn't appear
- Check DevTools → Console for a
[ghost-theme-editor]log line. If absent, the script didn't load. - Confirm
clientExtensions.scriptis present inGET /ghost/api/admin/config. - Check DevTools → Network for a 200 on the
ghost-theme-editor.jsrequest.
403 / CSP / mixed-content error
Ghost itself sets no Content-Security-Policy. The culprit is usually a reverse proxy. Add the jsDelivr host to your script-src directive, and style-src 'unsafe-inline' because the editor injects a <style> tag.
Monaco editor doesn't load
Monaco is loaded from https://cdn.jsdelivr.net/npm/[email protected]/. If the CDN is blocked, check network access from the browser. The editor will show an error in the banner.
"Upload failed" after editing a default theme
Default Ghost themes (casper, source) cannot be overwritten. The editor will prompt to upload as <theme>-edited instead. Accept the prompt to create a new theme with your changes.
Limitations
- Diff is text-only. Editable text files support diff/review. Binary files are preserved byte-for-byte, but visual/binary diffs are not shown.
- No multi-user coordination. If two people edit the same theme simultaneously, last writer wins. Ghost's backup-on-replace means the previous version is recoverable but not automatically merged.
- Theme validation is server-side. Ghost's theme validator runs on upload. If your edits produce an invalid theme, the error messages are surfaced in the editor's banner so you can fix and retry.
- In-memory only. Everything runs in browser memory, so the tab must stay open during editing. For very large themes with many binary assets, the initial download and extract may be slow.
License
MIT — see LICENSE.
