This guide captures best practices for writing org-mode documents that export beautifully to Ghost via the ox-ghost exporter.
Lexical documents are a flat list of nodes - headings don’t contain content, they’re just styled markers. Understanding this helps you structure content that renders well.
| Org Level | Lexical | Usage |
|---|---|---|
* | H2 | Major sections (H1 is post title) |
** | H3 | Subsections |
*** | H4 | Sub-subsections (use sparingly) |
**** | H5 | Rarely needed |
***** | H6 | Avoid - too deep |
Ghost posts typically use H2 for sections, H3 for subsections. Deeper nesting often signals content should be restructured.
#+TITLE: Your Post Title #+AUTHOR: Your Name #+DATE: 2024-01-29 #+PROPERTY: header-args :eval never-export
The :eval never-export prevents babel re-execution during Stage 2 export.
Keep code lines under 80 characters to avoid horizontal scrollbars. Ghost’s content area fits ~80-90 monospace characters.
| Width | Usage |
|---|---|
| 72 | Strict - fits all displays |
| 80 | Standard - recommended maximum |
| 100 | Extended - may scroll on narrow views |
Code blocks automatically include a copy-to-clipboard button (via Prism.js toolbar plugin). No special markup needed.
Code followed by explanation reads naturally:
#+BEGIN_SRC python result = compute_something() print(result) #+END_SRC The function returns the computed value...
Results flow directly after code - no heading needed:
#+BEGIN_SRC shell :results output echo "Hello from $(hostname)" #+END_SRC #+RESULTS: : Hello from myserver
Use #+NAME: to create reusable, named code blocks that can be called
from anywhere in the document. This decouples generation from display.
#+NAME: greeting-generator #+BEGIN_SRC elisp :var name="World" (format "Hello, %s!" name) #+END_SRC
Then reference it elsewhere with #+CALL::
#+CALL: greeting-generator(name="Ghost") #+RESULTS: : Hello, Ghost!
Hidden Generation Sections
For complex documents, create a :noexport: section for all generators:
* Generators :noexport:
#+NAME: make-list
#+BEGIN_SRC elisp :var items='()
(mapconcat (lambda (i) (format "- %s" i)) items "\n")
#+END_SRC
#+NAME: make-table
#+BEGIN_SRC elisp :var headers='() :var rows='()
;; Table generation logic...
#+END_SRC
* Visible Content
Features:
#+CALL: make-list(items='("Fast" "Reliable" "Beautiful"))
This pattern keeps generation logic separate from content structure.
Full syntax for calling named blocks:
#+CALL: block-name(arg1=value1, arg2=value2) ;; With header args: #+CALL: block-name() :results raw ;; Inline in text: This value is call_block-name()[:results raw].
Wrap code and results in REPL blocks for styled presentation.
Pass-through, no styling. Good for quick examples.
#+BEGIN_REPL #+BEGIN_SRC python 2 + 2 #+END_SRC #+RESULTS: : 4 #+END_REPL
Adds a label before output. Good for teaching.
#+BEGIN_REPL :style labeled :label "Output"
#+BEGIN_SRC python
print("Hello")
#+END_SRC
#+RESULTS:
: Hello
#+END_REPL
Wraps output in a colored callout. Use when output is the point.
#+BEGIN_REPL :style callout :emoji "checkmark" :color green #+BEGIN_SRC shell ./deploy.sh && echo "Success!" #+END_SRC #+RESULTS: : Success! #+END_REPL
Code in collapsible toggle, output visible. Good for long code.
#+BEGIN_REPL :style toggle :heading "Implementation Details"
#+BEGIN_SRC python
# Long implementation here...
def complex_function():
pass
#+END_SRC
#+RESULTS:
: Function defined
#+END_REPL
Wraps everything in aside styling. For tangential examples.
#+BEGIN_REPL :style aside #+BEGIN_SRC python # Alternative approach... #+END_SRC #+RESULTS: : ... #+END_REPL
| Situation | Recommended Style |
|---|---|
| Quick example, inline | simple |
| Teaching concept, show input/output | labeled |
| Important result (success, error, key) | callout |
| Long code readers can skip | toggle |
| “By the way” alternative approach | aside |
| Regular code, no special treatment | no wrapper |
Use for tips, warnings, and important notes - not for regular content.
#+BEGIN_CALLOUT :emoji warning :color yellow Be careful with this approach in production! #+END_CALLOUT
| Color | Emoji | Usage |
|---|---|---|
| blue | bulb | Info, tips |
| yellow | warning | Caution |
| green | check | Success, do this |
| red | x | Error, don’t |
| grey | Neutral note |
For content readers can skip:
#+BEGIN_TOGGLE :heading "Technical Details" Extended explanation that most readers don't need... #+END_TOGGLE
For tangential information:
#+BEGIN_ASIDE *Note:* This also works with the older API, though it's deprecated. #+END_ASIDE
#+ATTR_LEXICAL: :cardWidth wide [[./images/screenshot.png][Alt text description]]
Card widths: regular, wide, full
External: [[https://ghost.org][Ghost CMS]] Internal: [[file:other-post.org][Related Post]] Plain URL: https://example.com (auto-linked)
Org lists map directly to Lexical lists:
- Unordered item - Another item - Nested item 1. Ordered item 2. Another item 1. Nested numbered
Tables export as HTML blocks (Lexical doesn’t have native tables):
| Name | Value | |------+-------| | Foo | 42 | | Bar | 17 |
- One idea per section - Don’t overload headings
- Code then explain - Show the code, then discuss it
- Callouts are seasoning - Use sparingly for impact
- Toggles hide complexity - Let readers choose depth
- Asides are whispers - Brief tangents, not main content
- Flat is fine - Don’t over-nest; Lexical is flat anyway
Define once, use anywhere. Good for repeated elements.
* Setup :noexport: #+NAME: version-badge #+BEGIN_SRC elisp :var v="1.0.0" :var status="stable" (format "[[https://img.shields.io/badge/v%s-%s-green]]" v status) #+END_SRC * Introduction Current version: call_version-badge(v="2.1.0", status="beta")[:results raw] * Changelog Version call_version-badge(v="2.0.0")[:results raw] added...
Name your results block to place output away from source:
#+NAME: stats-output
#+RESULTS: generate-stats
* Appendix :noexport:
#+NAME: generate-stats
#+BEGIN_SRC python :results output
print("Generated statistics...")
#+END_SRC
Build code from named fragments:
#+NAME: imports
#+BEGIN_SRC python :exports none
import json
import requests
#+END_SRC
#+NAME: main-logic
#+BEGIN_SRC python :exports none :noweb yes
def fetch_data(url):
return requests.get(url).json()
#+END_SRC
* The Complete Script
#+BEGIN_SRC python :noweb yes :tangle script.py
<<imports>>
<<main-logic>>
if __name__ == "__main__":
print(fetch_data("https://api.example.com"))
#+END_SRC
Generate content based on document variables:
#+NAME: audience
: developers
#+NAME: intro-for-audience
#+BEGIN_SRC elisp :var audience="general"
(pcase audience
("developers" "This guide assumes familiarity with git and CLI.")
("designers" "No coding required - we'll use the visual editor.")
(_ "Welcome to our platform!"))
#+END_SRC
* Introduction
#+CALL: intro-for-audience(audience=(org-entry-get nil "audience" t))
Bad:
* Setup ** Installing *** Step 1 **** Download
Better:
* Setup ** Installing Download the package...
Bad: Every other paragraph is a callout
Good: One or two callouts per major section
Bad:
#+BEGIN_SRC python x = foo(bar(baz(y))) #+END_SRC #+BEGIN_SRC python z = transform(x) #+END_SRC
Better: Add context between code blocks
| Version | Date | Changes |
|---|---|---|
| 0.1.0 | 2024-01-29 | Initial style guide |
| 0.2.0 | 2026-01-29 | Added decoupled generation patterns |
| Added line width guidelines (80 char max) | ||
| Added #+NAME: and #+CALL: documentation | ||
| Added copy button note (Prism.js toolbar) |