README.org ==> ox-ghost ==> https://ox-ghost.ii.coop
The org file becomes your source of truth while staying in sync with Ghost.
ox-ghost is an Emacs org-mode export backend that converts org files to
Ghost’s Lexical JSON format, enabling direct publishing to Ghost CMS.
(package! ox-ghost
:recipe (:host github :repo "ii/ox-ghost"
:files ("ox-ghost.el" "ox-ghost-publish.el")))Then in config.el:
(use-package! ox-ghost
:after org
:config
;; Optional: Set path to ghost.js for publishing
(setq ghost-publish-script "/path/to/ox-ghost/ghost.js"))(use-package ox-ghost
:straight (:host github :repo "ii/ox-ghost"
:files ("ox-ghost.el" "ox-ghost-publish.el"))
:after org)(add-to-list 'load-path "/path/to/ox-ghost")
(require 'ox-ghost)
(require 'ox-ghost-publish) ; Optional: for publishing workflowFor local validation with Ghost’s renderer:
cd /path/to/ox-ghost
npm installM-x org-lexical-export-as-json- Export to bufferM-x org-lexical-export-to-file- Export to .json file
# Export org file to JSON
./org-to-lexical.sh input.org output.jsonemacs --batch -Q \
--eval "(require 'org)" \
--eval "(require 'ox-html)" \
-l ox-ghost.el \
--visit input.org \
--eval "(princ (org-export-as 'lexical))"Validate exported JSON using Ghost’s actual renderer:
# Validate JSON
node validate-lexical.js output.json
# Validate and generate HTML preview
node validate-lexical.js output.json --html preview.html
# Validate directly from org file (exports first)
node validate-lexical.js input.org --html preview.html
# Quiet mode for scripting (outputs JSON stats)
node validate-lexical.js output.json --quiet| Org Element | Lexical Node | Notes |
|---|---|---|
* Heading | heading | Level 1 → h2, Level 2 → h3 etc |
| Paragraph | paragraph | With inline formatting |
*bold* | text (format=1) | Bitmask format |
/italic/ | text (format=2) | |
_underline_ | text (format=8) | |
+strikethrough+ | text (format=4) | |
=code= | text (format=16) | Inline code |
[[url][desc]] | link | With nested text children |
[[file:img.jpg]] | image | Detected by extension |
- item | list/listitem | bullet or number listType |
----- | horizontalrule | |
#+BEGIN_QUOTE | quote | |
#+BEGIN_SRC | codeblock | With language |
#+BEGIN_EXAMPLE | codeblock | language=”text” |
#+BEGIN_EXPORT | html/raw | HTML or raw Lexical JSON |
#+BEGIN_CALLOUT :emoji 🎉 :color green
Your callout text here.
#+END_CALLOUT
Properties:
:emoji- Emoji icon (default: 💡):color- Background color: blue, green, yellow, red, pink, purple, grey (default: blue)
#+BEGIN_TOGGLE :heading "Click to expand"
Hidden content revealed on click.
#+END_TOGGLE
Properties:
:heading- Toggle header text (required)
#+BEGIN_BUTTON :url https://example.com :alignment center
Button Text
#+END_BUTTON
Properties:
:url- Button link URL (required):alignment- left, center, right (default: center)
#+BEGIN_ASIDE
Secondary content in an aside.
#+END_ASIDE
#+BEGIN_GALLERY :images "img1.jpg, img2.jpg, img3.jpg"
#+END_GALLERY
Properties:
:images- Comma-separated list of image URLs
#+BEGIN_VIDEO :src https://example.com/video.mp4
#+END_VIDEO
Properties:
:src- Video URL (required)
#+BEGIN_AUDIO :src https://example.com/audio.mp3
Episode Title
#+END_AUDIO
Properties:
:src- Audio URL (required)
#+BEGIN_EMBED :url https://twitter.com/example/status/123
Tweet preview text
#+END_EMBED
Properties:
:url- Embed URL (required)
#+BEGIN_BOOKMARK :url https://example.com
Bookmark Title
#+END_BOOKMARK
Properties:
:url- Bookmark URL (required)
#+BEGIN_FILE :src https://example.com/doc.pdf :fileName "Document.pdf"
File description
#+END_FILE
Properties:
:src- File URL (required):fileName- Display filename
#+BEGIN_PRODUCT :url https://shop.example.com :buttonText "Buy Now"
Product Name
#+END_PRODUCT
Properties:
:url- Product URL:buttonText- CTA button text
#+BEGIN_SIGNUP :layout regular :buttonText "Subscribe Now"
#+END_SIGNUP
Properties:
:layout- regular, wide, split (default: regular):buttonText- Button text (default: Subscribe)
#+BEGIN_CTA :layout minimal :buttonText "Learn More" :url https://example.com
Your CTA text here.
#+END_CTA
Properties:
:layout- minimal, immersive, split (default: minimal):buttonText- Button text (default: Learn more):url- Button link URL
#+BEGIN_HEADER :size small
Header Text
#+END_HEADER
Properties:
:size- small, medium, large (default: small)
#+BEGIN_TRANSISTOR :url https://share.transistor.fm/e/episode-id
#+END_TRANSISTOR
Properties:
:url- Transistor episode URL
#+BEGIN_EMAIL
Content only visible in email newsletters.
#+END_EMAIL
Wrap source code with its output using configurable styles:
#+BEGIN_REPL
#+BEGIN_SRC python
print("Hello!")
#+END_SRC
#+RESULTS:
: Hello!
#+END_REPL
Outputs consecutive codeblocks (source + output).
#+BEGIN_REPL :style labeled :label "Result"
#+BEGIN_SRC python
x = 2 + 2
print(x)
#+END_SRC
#+RESULTS:
: 4
#+END_REPL
Adds a label paragraph before the output.
#+BEGIN_REPL :style callout :emoji 💻 :color green
#+BEGIN_SRC shell
uname -a
#+END_SRC
#+RESULTS:
: Linux host 6.1.0 x86_64
#+END_REPL
Wraps output in a colored callout box.
#+BEGIN_REPL :style toggle :heading "Python Example"
#+BEGIN_SRC python
for i in range(3):
print(i)
#+END_SRC
#+RESULTS:
: 0
: 1
: 2
#+END_REPL
Puts code in a collapsible toggle, output follows after.
#+BEGIN_REPL :style aside
#+BEGIN_SRC elisp
(message "Hello!")
#+END_SRC
#+RESULTS:
: Hello!
#+END_REPL
Wraps everything in an aside block.
Properties:
:style- simple, labeled, callout, toggle, aside (default: simple):label- Label text for labeled style (default: “Output”):emoji- Emoji for callout style (default: 📤):color- Color for callout style (default: grey):heading- Heading for toggle style (default: “Code (language)”)
Use #+ATTR_LEXICAL to set image properties:
#+ATTR_LEXICAL: :cardWidth wide
[[file:photo.jpg][Alt text for the image]]
Properties:
:cardWidth- regular, wide, full (default: regular)
Insert raw Lexical JSON directly:
#+BEGIN_EXPORT lexical
{"type":"paragraph","version":1,"children":[...]}
#+END_EXPORT
Text formatting uses a bitmask:
| Format | Value | Example |
|---|---|---|
| Normal | 0 | Plain text |
| Bold | 1 | bold |
| Italic | 2 | italic |
| Strikethrough | 4 | |
| Underline | 8 | underline |
| Code | 16 | code |
| Bold+Italic | 3 | /bold italic/ |
Ghost Koenig editor supports these node types (from TryGhost/Koenig):
| Category | Node Types |
|---|---|
| Text | heading, paragraph, quote, aside, ExtendedText |
| Lists | list, listitem |
| Code | codeblock |
| Media | image, gallery, video, audio, file |
| Embeds | embed, bookmark, transistor |
| Interactive | button, toggle, callout, call-to-action, signup |
| Commerce | product, paywall |
| Layout | horizontalrule, header |
| Special | html, markdown, email, email-cta |
| File | Purpose |
|---|---|
ox-ghost.el | Emacs org-mode export backend |
org-to-lexical.sh | Shell wrapper for batch export |
validate-lexical.js | Validation with Ghost’s renderer |
package.json | npm dependencies for validator |
STYLE-GUIDE.org | Authoring best practices |
test-*.org | Test files |
See HISTORY.org for the full changelog.
Current version: 0.8.0 (2026-01-31)
For a complete publishing workflow with media generation and enrichment, see ox-ghost-publish.el.
(require 'ox-ghost-publish)| Phase | Command | Purpose |
|---|---|---|
| Generate | ghost-tts, ghost-image, ghost-video | Create media, upload to Ghost |
| Enrich | M-x ghost-enrich-buffer | Add metadata to media blocks |
| Preview | M-x ghost-preview | View HTML locally |
| Publish | M-x ghost-publish | Send to Ghost as draft |
After publishing, ox-ghost automatically syncs Ghost metadata back to your org file:
#+GHOST_ID: 697dc6b53c8ddf0001728f9f
#+GHOST_UUID: e34c9f93-6f87-4cad-ae5c-c4e338d1df14
#+GHOST_SLUG: my-post-slug
#+GHOST_URL: https://www.ii.coop/my-post-slug/
#+GHOST_STATUS: draft
#+GHOST_CREATED_AT: 2026-01-31T09:09:09.000Z
#+GHOST_UPDATED_AT: 2026-01-31T09:09:09.000Z
This enables a true round-trip workflow:
| Command | Behavior |
|---|---|
M-x ghost-publish | Create post, sync id/uuid/url back to org |
M-x ghost-update | Auto-detects post from #+GHOST_ID:, updates |
M-x ghost-pull-metadata | Refresh org headers from Ghost |
M-x ghost-status | Show diff between local org and Ghost |
The org file becomes your source of truth while staying in sync with Ghost.
The org file becomes your source of truth while staying in sync with Ghost.