Skip to content
/ ox-ghost Public

Emacs org-mode export backend for Ghost Lexical JSON

Notifications You must be signed in to change notification settings

ii/ox-ghost

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ox-ghost: Org-mode to Ghost Lexical JSON Exporter

README.org ==> ox-ghost ==> https://ox-ghost.ii.coop

Overview

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.

Installation

Doom Emacs (packages.el)

(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"))

Straight.el / use-package

(use-package ox-ghost
  :straight (:host github :repo "ii/ox-ghost"
             :files ("ox-ghost.el" "ox-ghost-publish.el"))
  :after org)

Manual Installation

(add-to-list 'load-path "/path/to/ox-ghost")
(require 'ox-ghost)
(require 'ox-ghost-publish) ; Optional: for publishing workflow

Node.js Validator (optional)

For local validation with Ghost’s renderer:

cd /path/to/ox-ghost
npm install

Usage

Interactive

  • M-x org-lexical-export-as-json - Export to buffer
  • M-x org-lexical-export-to-file - Export to .json file

Shell Script

# Export org file to JSON
./org-to-lexical.sh input.org output.json

Batch Mode (direct emacs)

emacs --batch -Q \
  --eval "(require 'org)" \
  --eval "(require 'ox-html)" \
  -l ox-ghost.el \
  --visit input.org \
  --eval "(princ (org-export-as 'lexical))"

Validation

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

Supported Node Types

Standard Org Elements → Lexical Nodes

Org ElementLexical NodeNotes
* HeadingheadingLevel 1 → h2, Level 2 → h3 etc
ParagraphparagraphWith 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]]linkWith nested text children
[[file:img.jpg]]imageDetected by extension
- itemlist/listitembullet or number listType
-----horizontalrule
#+BEGIN_QUOTEquote
#+BEGIN_SRCcodeblockWith language
#+BEGIN_EXAMPLEcodeblocklanguage=”text”
#+BEGIN_EXPORThtml/rawHTML or raw Lexical JSON

Special Blocks → Ghost Cards

Callout

#+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)

Toggle

#+BEGIN_TOGGLE :heading "Click to expand"
Hidden content revealed on click.
#+END_TOGGLE

Properties:

  • :heading - Toggle header text (required)

Button

#+BEGIN_BUTTON :url https://example.com :alignment center
Button Text
#+END_BUTTON

Properties:

  • :url - Button link URL (required)
  • :alignment - left, center, right (default: center)

Aside

#+BEGIN_ASIDE
Secondary content in an aside.
#+END_ASIDE

Gallery

#+BEGIN_GALLERY :images "img1.jpg, img2.jpg, img3.jpg"
#+END_GALLERY

Properties:

  • :images - Comma-separated list of image URLs

Video

#+BEGIN_VIDEO :src https://example.com/video.mp4
#+END_VIDEO

Properties:

  • :src - Video URL (required)

Audio

#+BEGIN_AUDIO :src https://example.com/audio.mp3
Episode Title
#+END_AUDIO

Properties:

  • :src - Audio URL (required)

Embed

#+BEGIN_EMBED :url https://twitter.com/example/status/123
Tweet preview text
#+END_EMBED

Properties:

  • :url - Embed URL (required)

Bookmark

#+BEGIN_BOOKMARK :url https://example.com
Bookmark Title
#+END_BOOKMARK

Properties:

  • :url - Bookmark URL (required)

File Download

#+BEGIN_FILE :src https://example.com/doc.pdf :fileName "Document.pdf"
File description
#+END_FILE

Properties:

  • :src - File URL (required)
  • :fileName - Display filename

Product

#+BEGIN_PRODUCT :url https://shop.example.com :buttonText "Buy Now"
Product Name
#+END_PRODUCT

Properties:

  • :url - Product URL
  • :buttonText - CTA button text

Signup Form

#+BEGIN_SIGNUP :layout regular :buttonText "Subscribe Now"
#+END_SIGNUP

Properties:

  • :layout - regular, wide, split (default: regular)
  • :buttonText - Button text (default: Subscribe)

Call to Action

#+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

Header Card

#+BEGIN_HEADER :size small
Header Text
#+END_HEADER

Properties:

  • :size - small, medium, large (default: small)

Transistor Podcast

#+BEGIN_TRANSISTOR :url https://share.transistor.fm/e/episode-id
#+END_TRANSISTOR

Properties:

  • :url - Transistor episode URL

Email-only Content

#+BEGIN_EMAIL
Content only visible in email newsletters.
#+END_EMAIL

REPL Block (Code + Output)

Wrap source code with its output using configurable styles:

Simple (default)

#+BEGIN_REPL
#+BEGIN_SRC python
print("Hello!")
#+END_SRC

#+RESULTS:
: Hello!
#+END_REPL

Outputs consecutive codeblocks (source + output).

Labeled

#+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.

Callout

#+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.

Toggle

#+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.

Aside

#+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)”)

Image Attributes

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)

Raw Lexical JSON

Insert raw Lexical JSON directly:

#+BEGIN_EXPORT lexical
{"type":"paragraph","version":1,"children":[...]}
#+END_EXPORT

Format Bitmask Reference

Text formatting uses a bitmask:

FormatValueExample
Normal0Plain text
Bold1bold
Italic2italic
Strikethrough4strikethrough
Underline8underline
Code16code
Bold+Italic3/bold italic/

Node Type Summary

Ghost Koenig editor supports these node types (from TryGhost/Koenig):

CategoryNode Types
Textheading, paragraph, quote, aside, ExtendedText
Listslist, listitem
Codecodeblock
Mediaimage, gallery, video, audio, file
Embedsembed, bookmark, transistor
Interactivebutton, toggle, callout, call-to-action, signup
Commerceproduct, paywall
Layouthorizontalrule, header
Specialhtml, markdown, email, email-cta

Files

FilePurpose
ox-ghost.elEmacs org-mode export backend
org-to-lexical.shShell wrapper for batch export
validate-lexical.jsValidation with Ghost’s renderer
package.jsonnpm dependencies for validator
STYLE-GUIDE.orgAuthoring best practices
test-*.orgTest files

Version History

See HISTORY.org for the full changelog.

Current version: 0.8.0 (2026-01-31)

Ghost Publishing Workflow

For a complete publishing workflow with media generation and enrichment, see ox-ghost-publish.el.

Quick Start

(require 'ox-ghost-publish)

Phases

PhaseCommandPurpose
Generateghost-tts, ghost-image, ghost-videoCreate media, upload to Ghost
EnrichM-x ghost-enrich-bufferAdd metadata to media blocks
PreviewM-x ghost-previewView HTML locally
PublishM-x ghost-publishSend to Ghost as draft

Round-Trip Metadata Sync

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:

CommandBehavior
M-x ghost-publishCreate post, sync id/uuid/url back to org
M-x ghost-updateAuto-detects post from #+GHOST_ID:, updates
M-x ghost-pull-metadataRefresh org headers from Ghost
M-x ghost-statusShow diff between local org and Ghost

The org file becomes your source of truth while staying in sync with Ghost.

Resources


The org file becomes your source of truth while staying in sync with Ghost.

About

Emacs org-mode export backend for Ghost Lexical JSON

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •