aboutsummaryrefslogtreecommitdiff
path: root/content
diff options
context:
space:
mode:
Diffstat (limited to 'content')
-rw-r--r--content/blog/2026-08-11-orgo.org119
-rw-r--r--content/orgo.toml16
-rw-r--r--content/templates/base.html12
-rw-r--r--content/templates/blog.html4
-rw-r--r--content/templates/feed.xml9
-rw-r--r--content/templates/garden.html2
-rw-r--r--content/templates/home.html8
-rw-r--r--content/templates/tags.html9
-rw-r--r--content/uses/index.org2
9 files changed, 144 insertions, 37 deletions
diff --git a/content/blog/2026-08-11-orgo.org b/content/blog/2026-08-11-orgo.org
new file mode 100644
index 0000000..9204fc5
--- /dev/null
+++ b/content/blog/2026-08-11-orgo.org
@@ -0,0 +1,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!
diff --git a/content/orgo.toml b/content/orgo.toml
index 307df57..c78b365 100644
--- a/content/orgo.toml
+++ b/content/orgo.toml
@@ -5,9 +5,6 @@
#
# orgo serve content -o /tmp/preview # write and preview
# orgo build content -o _site --strict # build for real, failing on broken links
-#
-# This sits alongside the existing weblorg setup (publish.el, theme/) and changes nothing
-# about it. Delete this file and content/templates/ to remove orgo entirely.
[site]
title = "cleberg.net"
@@ -21,8 +18,7 @@ base_url = "https://cleberg.net"
[nav]
# Explicit, because the pages that belong in the nav are not all top-level: /blog/ and
# /garden/ are generated below, and salary/ is a section index one level down. index.org
-# is deliberately absent — the layout writes the Home link itself, exactly as weblorg's
-# base.html does.
+# is deliberately absent — the layout writes the Home link itself.
#
# Generated pages are named by their output path, authored ones by their source. Listing
# them together is what puts Salary last: a collection with `nav = true` that is not
@@ -32,15 +28,13 @@ mode = "explicit"
pages = ["blog/index.html", "garden/index.html", "salary/index.org"]
[build]
-# theme/static/ is weblorg's static root, published at /. orgo reads it from here
+# theme/static/ is the site's static root, published at /. orgo reads it from here
# rather than needing its files copied next to the writing — one copy, not two.
assets = ["../theme/static"]
[templates]
dir = "templates"
-# Blog posts render through post.html — the shared layout plus a reply footer — the way
-# weblorg routes them through theme/templates/post.html. Everything else uses base.html.
# Blog posts render through post.html, which adds the reply snippet. Nothing else does:
# the live site also invites replies on garden notes and /now/, and that is a choice
# rather than something a directory should decide. Any page opts in for itself with
@@ -63,13 +57,11 @@ toc = true
section_numbers = true
# --- Generated pages -------------------------------------------------------------
-# These have no source .org file. weblorg produces the equivalents today through its
-# blog-index, garden-index and tags routes.
+# These have no source .org file.
# The home page. Generated rather than authored, because it is a hand-written
# introduction *plus* the most recent posts — and the post list is not something to
-# maintain by hand. The prose lives in templates/home.html, which is where weblorg keeps
-# it today too.
+# maintain by hand. The prose lives in templates/home.html.
#
# content/index.org was deleted to make room: it held only #+title and #+description,
# both of which are now [site] settings above. Restore it any time with
diff --git a/content/templates/base.html b/content/templates/base.html
index e35098c..237c398 100644
--- a/content/templates/base.html
+++ b/content/templates/base.html
@@ -1,9 +1,9 @@
-{# Page layout for orgo, mirroring theme/templates/base.html.
+{# Page layout for the site.
Styling comes from theme/static/styles.css, published by `[build] assets` in
- orgo.toml — the same directory weblorg serves at /, read in place rather than
- copied. weblorg links the minified styles.min.css instead; that file is gitignored
- build output and the rules are the same, so this links the tracked one.
+ orgo.toml and read in place rather than copied. It is served as authored: at ~1KB,
+ minifying it saved 52 bytes over the wire once compressed, which did not pay for a
+ build step and a toolchain dependency.
Available here: page (.title .url .source .date .date_iso .tags .excerpt .toc
.keywords), site, nav, root, stylesheet. See the orgo guide on Templates. #}
@@ -45,8 +45,8 @@
<link rel="alternate" type="application/rss+xml" title="{{ site.title }}" href="{{ root }}feed.xml">
<link rel="stylesheet" href="{{ root }}styles.css" type="text/css">
<link rel="icon" href="data:,">
-{#- rel="me" identity links. weblorg's base.html writes the last one as <a>, which a
- browser hoists out of <head>; <link> is the spelling that stays where it is put. #}
+{#- rel="me" identity links, spelled as <link> rather than <a>: a browser hoists a
+ stray <a> out of <head>, while <link> stays where it is put. #}
<link href="https://github.com/ccleberg" rel="me">
<link href="https://gitlab.com/ccleberg" rel="me">
<link href="mailto:[email protected]" rel="me">
diff --git a/content/templates/blog.html b/content/templates/blog.html
index 17f8870..ecb5c5d 100644
--- a/content/templates/blog.html
+++ b/content/templates/blog.html
@@ -1,5 +1,5 @@
-{# The blog index at /blog/, mirroring theme/templates/blog.html.
- Separate from list.html because the live page carries its own prose. #}
+{# The blog index at /blog/.
+ Separate from list.html because the page carries its own prose. #}
{% extends "base.html" %}
{#- Group the list under year headings, as the live site does. Set to false for one flat
diff --git a/content/templates/feed.xml b/content/templates/feed.xml
index fa761b8..7596b02 100644
--- a/content/templates/feed.xml
+++ b/content/templates/feed.xml
@@ -1,10 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?>
-{#- The RSS feed at /feed.xml, mirroring theme/templates/feed.xml. A feed is read away
- from the site that served it, so every URL here is absolute — which is what
- `| absolute` needs [site] base_url for.
+{#- The RSS feed at /feed.xml. A feed is read away from the site that served it, so
+ every URL here is absolute — which is what `| absolute` needs [site] base_url for.
- <description> carries each post's full rendered HTML, as weblorg's feed does — the
- collection sets `include_content` to ask for it. #}
+ <description> carries each post's full rendered HTML — the collection sets
+ `include_content` to ask for it. #}
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
diff --git a/content/templates/garden.html b/content/templates/garden.html
index dc669a2..113128a 100644
--- a/content/templates/garden.html
+++ b/content/templates/garden.html
@@ -1,4 +1,4 @@
-{# The garden index at /garden/, mirroring theme/templates/garden.html.
+{# The garden index at /garden/.
Notes are listed by title and description rather than by date. #}
{% extends "base.html" %}
{% block main %}
diff --git a/content/templates/home.html b/content/templates/home.html
index 6d280ee..e63fa6a 100644
--- a/content/templates/home.html
+++ b/content/templates/home.html
@@ -1,9 +1,8 @@
{# The home page: authored prose plus the most recent posts.
A generated page, so it has no source .org file — `pages` holds the blog collection's
- entries, newest first, and the prose lives here. This mirrors what weblorg's
- theme/templates/index.html does today, including its bare <title>: the home page is
- "cleberg.net" and not "cleberg.net - cleberg.net". #}
+ entries, newest first, and the prose lives here. The <title> is bare on purpose: the
+ home page is "cleberg.net" and not "cleberg.net - cleberg.net". #}
{% extends "base.html" %}
{% block subtitle %}{% endblock %}
{% block main %}
@@ -36,8 +35,7 @@ user control, and long-term utility.</p>
<h2>Elsewhere</h2>
<p>Places to find me online.</p>
<ul>
-<li>cgit (primary): <a href="https://git.krz.sh">~cmc</a></li>
-<li>GitHub (mirror): <a href="https://github.com/ccleberg">@ccleberg</a></li>
+<li>GitHub: <a href="https://github.com/ccleberg">@ccleberg</a></li>
<li>Email: <a href="mailto:[email protected]">[email protected]</a> (<a href="{{ root }}gpg.txt">GPG</a>)</li>
<li>Lemmy: <a href="https://r.nf/u/cmc">@cmc</a></li>
<li>Mastodon: <a href="https://c.im/@cmc">@cmc</a></li>
diff --git a/content/templates/tags.html b/content/templates/tags.html
index 5240b27..9b3a1e2 100644
--- a/content/templates/tags.html
+++ b/content/templates/tags.html
@@ -1,11 +1,10 @@
{# The tag index at /tags/. Receives `groups` — every tag with its count — rather than
`pages`, because it lists tags and not posts.
- This deliberately differs in shape from the live weblorg page, which is one page with
- every post inlined under an `#anchor` per tag. Here each tag is its own page, so a tag
- has a URL worth linking to and the index stays short. The `id` on each entry keeps the
- old `/tags/#audit` links landing in the right place — they scroll to the tag and its
- link, rather than 404ing or dumping the reader at the top of a long page. #}
+ Each tag is its own page, so a tag has a URL worth linking to and the index stays
+ short. The `id` on each entry keeps older `/tags/#audit` links landing in the right
+ place — they scroll to the tag and its link, rather than dumping the reader at the
+ top of the index. #}
{% extends "base.html" %}
{% block main %}
<h1>{{ page.title }}</h1>
diff --git a/content/uses/index.org b/content/uses/index.org
index d7f5db9..051a6eb 100644
--- a/content/uses/index.org
+++ b/content/uses/index.org
@@ -24,7 +24,7 @@ threat model justification.
| Operating System | [[https://ubuntu.com][Ubuntu]] + [[https://www.apple.com/os/macos/][macOS]] | Ubuntu: telemetry off, LTS, stable. macOS: workstation only. |
| Web Server | [[https://nginx.org][Nginx]] + [[https://community.torproject.org/onion-services/setup/][Tor]] | Static file serving. Tor layer for censored-network access. |
| SSL | [[https://certbot.eff.org][Certbot]] | Free automated TLS. No commercial CA dependency. Ensures Cloudflare can't view unencrypted traffic. |
-| Static Site Generator | [[https://github.com/emacs-love/weblorg][Weblorg]] + [[https://git.sr.ht/~ccleberg/cleberg.net/tree/main/item/build.py][build.py]] | Org-mode source compiles to plaintext HTML via native Lisp. |
+| Static Site Generator | [[https://github.com/krazywarez/orgo][orgo]] + [[https://git.sr.ht/~ccleberg/cleberg.net/tree/main/item/build.py][build.py]] | Org-mode source compiles to plaintext HTML. Single binary, no runtime. |
| Terminal | [[https://iterm2.com/][iTerm2]] | Functional. Inherited from macOS. |
| Shell | [[https://www.zsh.org/][Zsh]] | Portable, POSIX-adjacent, available on every target OS. |
| Editor | [[https://github.com/doomemacs/doomemacs][Doom Emacs]] | Editor and markup are the same tool. No proprietary format. |