Folders and files
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Repository files navigation
#+TITLE: ox-ghost: Org-mode to Ghost Lexical JSON Exporter #+AUTHOR: ii.coop #+DATE: 2026-01-28 #+GHOST_SLUG: ox-ghost #+GHOST_STATUS: published #+GHOST_TAGS: emacs, org-mode, ghost, tools, open-source #+GHOST_EXCERPT: Export org-mode documents directly to Ghost CMS with full support for all Koenig editor card types. #+GHOST_ID: 697dcd073c8ddf0001728fa8 #+GHOST_UUID: 90fe6afd-1fb6-426f-8dbc-6cacaa3c8d33 #+GHOST_URL: https://www.ii.coop/ox-ghost/ #+GHOST_VISIBILITY: public #+GHOST_CREATED_AT: 2026-01-31T09:36:07.000Z #+GHOST_UPDATED_AT: 2026-01-31T11:57:17.000Z #+GHOST_IMAGE: https://www.ii.coop/content/images/2026/01/ox-ghost.jpeg #+GHOST_PUBLISHED_AT: 2026-01-31T11:57:17.000Z [[https://www.ii.coop/content/files/2026/01/README.org][README.org]] ==> [[https://github.com/ii/ox-ghost][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) #+BEGIN_SRC elisp (package! ox-ghost :recipe (:host github :repo "ii/ox-ghost" :files ("ox-ghost.el" "ox-ghost-publish.el"))) #+END_SRC Then in =config.el=: #+BEGIN_SRC elisp (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")) #+END_SRC ** Straight.el / use-package #+BEGIN_SRC elisp (use-package ox-ghost :straight (:host github :repo "ii/ox-ghost" :files ("ox-ghost.el" "ox-ghost-publish.el")) :after org) #+END_SRC ** Manual Installation #+BEGIN_SRC elisp (add-to-list 'load-path "/path/to/ox-ghost") (require 'ox-ghost) (require 'ox-ghost-publish) ; Optional: for publishing workflow #+END_SRC ** Node.js Validator (optional) For local validation with Ghost's renderer: #+BEGIN_SRC bash cd /path/to/ox-ghost npm install #+END_SRC * 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 #+BEGIN_SRC bash # Export org file to JSON ./org-to-lexical.sh input.org output.json #+END_SRC ** Batch Mode (direct emacs) #+BEGIN_SRC bash emacs --batch -Q \ --eval "(require 'org)" \ --eval "(require 'ox-html)" \ -l ox-ghost.el \ --visit input.org \ --eval "(princ (org-export-as 'lexical))" #+END_SRC ** Validation Validate exported JSON using Ghost's actual renderer: #+BEGIN_SRC bash # 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 #+END_SRC * Supported Node Types ** Standard Org Elements → Lexical Nodes | 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 | ** Special Blocks → Ghost Cards *** Callout #+BEGIN_SRC org ,#+BEGIN_CALLOUT :emoji 🎉 :color green Your callout text here. ,#+END_CALLOUT #+END_SRC Properties: - =:emoji= - Emoji icon (default: 💡) - =:color= - Background color: blue, green, yellow, red, pink, purple, grey (default: blue) *** Toggle #+BEGIN_SRC org ,#+BEGIN_TOGGLE :heading "Click to expand" Hidden content revealed on click. ,#+END_TOGGLE #+END_SRC Properties: - =:heading= - Toggle header text (required) *** Button #+BEGIN_SRC org ,#+BEGIN_BUTTON :url https://example.com :alignment center Button Text ,#+END_BUTTON #+END_SRC Properties: - =:url= - Button link URL (required) - =:alignment= - left, center, right (default: center) *** Aside #+BEGIN_SRC org ,#+BEGIN_ASIDE Secondary content in an aside. ,#+END_ASIDE #+END_SRC *** Gallery #+BEGIN_SRC org ,#+BEGIN_GALLERY :images "img1.jpg, img2.jpg, img3.jpg" ,#+END_GALLERY #+END_SRC Properties: - =:images= - Comma-separated list of image URLs *** Video #+BEGIN_SRC org ,#+BEGIN_VIDEO :src https://example.com/video.mp4 ,#+END_VIDEO #+END_SRC Properties: - =:src= - Video URL (required) *** Audio #+BEGIN_SRC org ,#+BEGIN_AUDIO :src https://example.com/audio.mp3 Episode Title ,#+END_AUDIO #+END_SRC Properties: - =:src= - Audio URL (required) *** Embed #+BEGIN_SRC org ,#+BEGIN_EMBED :url https://twitter.com/example/status/123 Tweet preview text ,#+END_EMBED #+END_SRC Properties: - =:url= - Embed URL (required) *** Bookmark #+BEGIN_SRC org ,#+BEGIN_BOOKMARK :url https://example.com Bookmark Title ,#+END_BOOKMARK #+END_SRC Properties: - =:url= - Bookmark URL (required) *** File Download #+BEGIN_SRC org ,#+BEGIN_FILE :src https://example.com/doc.pdf :fileName "Document.pdf" File description ,#+END_FILE #+END_SRC Properties: - =:src= - File URL (required) - =:fileName= - Display filename *** Product #+BEGIN_SRC org ,#+BEGIN_PRODUCT :url https://shop.example.com :buttonText "Buy Now" Product Name ,#+END_PRODUCT #+END_SRC Properties: - =:url= - Product URL - =:buttonText= - CTA button text *** Signup Form #+BEGIN_SRC org ,#+BEGIN_SIGNUP :layout regular :buttonText "Subscribe Now" ,#+END_SIGNUP #+END_SRC Properties: - =:layout= - regular, wide, split (default: regular) - =:buttonText= - Button text (default: Subscribe) *** Call to Action #+BEGIN_SRC org ,#+BEGIN_CTA :layout minimal :buttonText "Learn More" :url https://example.com Your CTA text here. ,#+END_CTA #+END_SRC Properties: - =:layout= - minimal, immersive, split (default: minimal) - =:buttonText= - Button text (default: Learn more) - =:url= - Button link URL *** Header Card #+BEGIN_SRC org ,#+BEGIN_HEADER :size small Header Text ,#+END_HEADER #+END_SRC Properties: - =:size= - small, medium, large (default: small) *** Transistor Podcast #+BEGIN_SRC org ,#+BEGIN_TRANSISTOR :url https://share.transistor.fm/e/episode-id ,#+END_TRANSISTOR #+END_SRC Properties: - =:url= - Transistor episode URL *** Email-only Content #+BEGIN_SRC org ,#+BEGIN_EMAIL Content only visible in email newsletters. ,#+END_EMAIL #+END_SRC *** REPL Block (Code + Output) Wrap source code with its output using configurable styles: **** Simple (default) #+BEGIN_SRC org ,#+BEGIN_REPL ,#+BEGIN_SRC python print("Hello!") ,#+END_SRC ,#+RESULTS: : Hello! ,#+END_REPL #+END_SRC Outputs consecutive codeblocks (source + output). **** Labeled #+BEGIN_SRC org ,#+BEGIN_REPL :style labeled :label "Result" ,#+BEGIN_SRC python x = 2 + 2 print(x) ,#+END_SRC ,#+RESULTS: : 4 ,#+END_REPL #+END_SRC Adds a label paragraph before the output. **** Callout #+BEGIN_SRC org ,#+BEGIN_REPL :style callout :emoji 💻 :color green ,#+BEGIN_SRC shell uname -a ,#+END_SRC ,#+RESULTS: : Linux host 6.1.0 x86_64 ,#+END_REPL #+END_SRC Wraps output in a colored callout box. **** Toggle #+BEGIN_SRC org ,#+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 #+END_SRC Puts code in a collapsible toggle, output follows after. **** Aside #+BEGIN_SRC org ,#+BEGIN_REPL :style aside ,#+BEGIN_SRC elisp (message "Hello!") ,#+END_SRC ,#+RESULTS: : Hello! ,#+END_REPL #+END_SRC 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: #+BEGIN_SRC org ,#+ATTR_LEXICAL: :cardWidth wide [[file:photo.jpg][Alt text for the image]] #+END_SRC Properties: - =:cardWidth= - regular, wide, full (default: regular) * Raw Lexical JSON Insert raw Lexical JSON directly: #+BEGIN_SRC org ,#+BEGIN_EXPORT lexical {"type":"paragraph","version":1,"children":[...]} ,#+END_EXPORT #+END_SRC * Format Bitmask Reference Text formatting uses a bitmask: | Format | Value | Example | |---------------+-------+----------------------------| | Normal | 0 | Plain text | | Bold | 1 | *bold* | | Italic | 2 | /italic/ | | Strikethrough | 4 | +strikethrough+ | | Underline | 8 | _underline_ | | Code | 16 | =code= | | Bold+Italic | 3 | */bold italic/* | * Node Type Summary Ghost Koenig editor supports these node types (from [[https://github.com/TryGhost/Koenig][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 | * Files | 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 | * Version History See [[file:HISTORY.org][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 #+BEGIN_SRC elisp (require 'ox-ghost-publish) #+END_SRC ** Phases | 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 | ** Round-Trip Metadata Sync After publishing, =ox-ghost= automatically syncs Ghost metadata back to your org file: #+BEGIN_SRC org ,#+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 #+END_SRC 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. ** Pages vs Posts By default, =ghost-publish= and =ghost-update= create/update Ghost *posts*. To publish a Ghost *page* instead, add: #+BEGIN_SRC org ,#+GHOST_TYPE: page #+END_SRC All commands (=ghost-publish=, =ghost-update=, =ghost-pull-metadata=, =ghost-status=) automatically route to the correct Ghost API based on this header. | Header Value | API Used | |--------------+--------------------------------| | (not set) | =ghost.js post create/update= | | =post= | =ghost.js post create/update= | | =page= | =ghost.js page create/update= | * Resources - [[https://ghost.org/docs/editor/][Ghost Editor Documentation]] - [[https://github.com/TryGhost/Koenig][Ghost Koenig Editor Source]] - [[https://lexical.dev/][Lexical Framework]] ----- *The org file becomes your source of truth while staying in sync with Ghost.*