1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
#+date: [2026-08-11 Tue 20:51:00]
#+title: I Built My Own Org-Mode Static Site Generator
#+description: I was tired of bolting solutions onto my SSGs, so I built my own.
#+slug: orgo
#+filetags: :development:org-mode:
* Introducing: Orgo
For a long time, I've been using [[https://github.com/emacs-love/weblorg][weblorg]], a static site generator (SSG) for
org-mode. It uses Emacs Lisp, which pairs well with Emacs. However, it has a few
downsides:
- since it requires Emacs, the set-up and maintenance is heavy
- it has upstream dependencies that must also be installed and loaded into Emacs
- it re-runs the full process every time you need to regenerate the site, even
if you only change a single character in one file
- since it runs through Emacs in the background, it takes quite a while to build
my site (~180 pages)
So, I decided to spin up my own org-mode-based SSG: [[https://github.com/krazywarez/orgo][orgo]]. This SSG uses org-mode
(rather than markdown) for its content.
#+caption: The orgo CLI
#+attr_html: :alt Terminal output of orgo --help, listing the build, watch, serve, clean, audit, and init commands.
[[https://img.cleberg.net/blog/20260811-orgo/orgo.webp]]
* Results Up Front
I want to highlight the speed first, before I go into the major differences. As
you can see in the table below, orgo is incredibly fast.
| Method | Time | Diff |
|----------------------------------------+-------+-------------|
| weblorg (=emacs --script publish.el=) | 49.0s | |
| weblorg + [[https://github.com/ccleberg/cleberg.net/blob/8ec9cdfeae71068a8924dd9f61b9cc28c947ec31/build.py][build.py]] | 50.3s | |
| orgo, cold build | *0.22s* | 223× faster |
| orgo, nothing changed since last build | *0.13s* | 377× faster |
Additionally, I added a few new features that weblorg didn't provide me:
- Native features I used to build in Python post-weblorg:
- Tag listings
- Year separators on the blog page
- RSS feed
- Sitemap
- Recent posts lists
- Syntax highlighting for 75 languages is supported, and you can drop in your
own syntax definitions for anything missing.
- Builds are incremental. Only the pages whose content, config, or templates
changed get re-rendered — adding a single post re-renders that post and the
index that lists it, and nothing else.
- There is no Emacs, no package manager, and no runtime to install since orgo is
a single binary.
This results in a fast, clean, and simple SSG that stays out of my way instead
of requiring extensive set-up and maintenance.
* What It Is Built On
/Note: This section is for the nerds. Feel free to skip if you don't care about
the technology behind orgo./
orgo is Rust, and it leans on a handful of well-worn crates rather than
reinventing them:
- The org parser is hand-written, about 1,500 lines of it. There is no org-mode
crate to lean on, and writing that parser is most of what writing an org SSG
turns out to be.
- Templates run on [[https://crates.io/crates/minijinja][minijinja]], which speaks Jinja2. weblorg's templates are
written for templatel, which is close enough that porting them was mostly a
rename job.
- Syntax highlighting is [[https://crates.io/crates/syntect][syntect]], so the definitions and the themes are Sublime
Text's. This site renders code with =InspiredGitHub=.
- Incremental builds hash the content, the config, and the templates with
[[https://crates.io/crates/blake3][blake3]]. A page is re-rendered when its hash changes, and the manifest that
remembers them is JSON sitting in the output directory.
- Pages render in parallel with [[https://crates.io/crates/rayon][rayon]], which is straightforward here because
each page writes only its own file.
- The config is TOML, the CLI is [[https://crates.io/crates/clap][clap]], and =orgo serve= is [[https://crates.io/crates/notify][notify]] watching for
edits with a [[https://crates.io/crates/tiny_http][tiny_http]] server in front of the output.
* Getting Started
If you use org-mode (or have been looking for a proper org-mode SSG), check it
out and let me know what you think.
It's as simple as installing, initializing a new site, and serving it!
#+begin_src shell
cargo install orgo
orgo init my-site
orgo serve my-site -o _site
#+end_src
It can create a new project for you, adapt to your current org site, or simply
serve your folder of org-mode notes and files.
Three more commands cover the rest of the day-to-day:
- =orgo watch= rebuilds incrementally as files change, driven by filesystem
events, without serving anything. This is handy when something else is already
serving the output directory.
- =orgo clean= removes the output directory, cache manifest and all, for when you
want to start from scratch.
- =orgo audit= reports which org constructs a folder actually uses, and marks the
ones it doesn't handle.
#+caption: An orgo Audit Report
#+attr_html: :alt Terminal output of orgo audit, tallying every org construct in a corpus by frequency and marking each one in or out of scope, followed by counts of keywords, block types, drawers, and link schemes.
[[https://img.cleberg.net/blog/20260811-orgo/audit.webp]]
* The Future
I'll be looking into making some built-in themes soon and exploring other
features that make it a bit more seamless, as well as adopting to various
methods different users may expect it to work.
If you have any suggestions, reach out and let me know or open an issue/PR on
GitHub!
|