Commit f1b8d6d4a8

f1b8d6d4a89c5d33615556af65aadeb71e7800cb

parent: 8ec9cdfeae

Unsigned

cmc <hello@cleberg.net> · 2026-08-11 07:20 UTC

Add an org-ssg build alongside the weblorg one

Builds the same site with org-ssg instead of weblorg, reproducing all 182 URLs the
current .build output has, plus 13 per-tag pages it does not.

    org-ssg serve content -o /tmp/preview      # write, with live reload
    org-ssg build content -o _site --strict    # build, failing on broken links

content/ is the source because it is the URL root: content/blog/post.org publishes at
/blog/post.html. Pointing at the repository root instead would prefix every URL with
/content/ and copy build.py, publish.el and LICENSE into the site as assets.

Added:

- content/org-ssg.toml — site metadata, an explicit nav, and four generated pages:
  the home page, the blog index, the garden index, and tag pages with an index. URLs
  come from #+SLUG:, which is why 2018-11-28-aes-encryption.org still publishes at
  /blog/aes-encryption.html.
- content/templates/ — base, home, list and tags. Plain layouts using the .post-list
  classes styles.css already defines, not a port of theme/templates/, which are
  templatel and a different language.
- content/styles.css — a verbatim copy of theme/static/styles.css, because org-ssg
  publishes assets from inside its source directory. `diff` between the two should stay
  empty; consolidate them if this migration goes further.

Removed content/index.org. It held only #+title and #+description, both now [site]
settings, and a generated home page cannot share a URL with an authored one — org-ssg
fails that build rather than picking a winner. Restore with
`git checkout content/index.org`, but then the home collection needs a different output.

Nothing in the weblorg setup changed. publish.el, theme/ and build.py still work as
before, so both builds coexist until one is chosen.

Known difference: the nav renders Home, Salary, Blog, Garden, Tags — explicit source
pages come before generated ones, where the live site orders Salary last.

Layout: unified · split

content/index.org deleted −2
@@ -1,2 +0,0 @@
1#+title: cleberg.net
2#+description: Stillness amidst the chaos.
content/org-ssg.toml added +95
@@ -0,0 +1,95 @@
1# org-ssg configuration for cleberg.net.
2#
3# Lives in content/ because that directory is the site's URL root: content/blog/post.org
4# publishes at /blog/post.html. Neither this file nor templates/ is published.
5#
6# org-ssg serve content -o /tmp/preview # write and preview
7# org-ssg build content -o _site --strict # build for real, failing on broken links
8#
9# This sits alongside the existing weblorg setup (publish.el, theme/) and changes nothing
10# about it. Delete this file and content/templates/ to remove org-ssg entirely.
11
12[site]
13title = "cleberg.net"
14description = "Stillness amidst the chaos."
15language = "en"
16# Empty keeps every URL relative, so the site works from a preview directory as well as
17# from its real origin. Set it to "https://cleberg.net" when you want canonical links, or
18# an RSS feed — a feed is read away from the site that served it, so its links cannot be
19# relative.
20base_url = ""
21
22[nav]
23# Explicit, because the pages that belong in the nav are not all top-level: /blog/ and
24# /garden/ are generated below, and salary/ is a section index one level down. index.org
25# is deliberately absent — the layout already links home from the site title, and listing
26# it here would print "cleberg.net" twice side by side. Source pages come first in the
27# order listed, then the collections that set `nav = true`.
28mode = "explicit"
29pages = ["salary/index.org"]
30
31[templates]
32dir = "templates"
33
34[highlight]
35theme = "InspiredGitHub"
36
37[html]
38# Level-1 org headings become <h2>, beneath the page title the layout renders as <h1> —
39# which is what the current site does too.
40heading_offset = 1
41toc = true
42section_numbers = false
43
44# --- Generated pages -------------------------------------------------------------
45# These have no source .org file. weblorg produces the equivalents today through its
46# blog-index, garden-index and tags routes.
47
48# The home page. Generated rather than authored, because it is a hand-written
49# introduction *plus* the most recent posts — and the post list is not something to
50# maintain by hand. The prose lives in templates/home.html, which is where weblorg keeps
51# it today too.
52#
53# content/index.org was deleted to make room: it held only #+title and #+description,
54# both of which are now [site] settings above. Restore it any time with
55# `git checkout content/index.org`, but then this collection must output somewhere else —
56# two pages cannot claim one URL, and org-ssg fails the build rather than picking a winner.
57[[collections]]
58source = "blog"
59output = "index.html"
60template = "home.html"
61title = "cleberg.net"
62sort = "date"
63order = "desc"
64
65[[collections]]
66source = "blog"
67output = "blog/index.html"
68template = "list.html"
69title = "Blog"
70sort = "date"
71order = "desc"
72nav = true
73
74[[collections]]
75source = "garden"
76output = "garden/index.html"
77template = "list.html"
78title = "Garden"
79sort = "date"
80order = "desc"
81nav = true
82
83# One page per tag, plus the index the live site already has at /tags/. `source` is empty
84# so it groups across the whole site rather than one section, which is how #+FILETAGS: is
85# used here — tags appear on blog posts and garden notes alike.
86[[collections]]
87source = ""
88group_by = "tags"
89output = "tags/{tag}.html"
90template = "list.html"
91title = "Tagged: {tag}"
92index_output = "tags/index.html"
93index_template = "tags.html"
94index_title = "Tags"
95nav = true
content/styles.css added +23
@@ -0,0 +1,23 @@
1body { max-width: 800px; margin: 0 auto; padding: 1rem; }
2a, a:visited { color: #0000ff; }
3nav ul { list-style: none; padding: 0; display: flex; flex-direction: row; }
4nav ul li { padding-right: 0.5rem;}
5img { max-width: 100%; }
6.post-list { padding: 0; padding-inline-start: 0; }
7.post-list-item { display: flex; list-style: none; align-items: baseline; }
8.post-list-item time { padding-right: 0.5rem; white-space: nowrap; }
9.post-list-item a { flex-grow: 1; white-space: normal; }
10.post-list-year { list-style: none; margin-top: 1rem; margin-bottom: 0.5rem; font-weight: bold; }
11.skip-link { display: none; }
12footer { margin: 2rem 0;}
13pre, blockquote { margin: 1rem 0; padding: 0.5rem; background-color: #eee; }
14pre { overflow: auto; white-space: pre; }
15blockquote p { margin: 0; margin-bottom: 1rem; }
16blockquote p:last-child { margin-bottom: 0; }
17code { color: #942192; }
18@media (prefers-color-scheme: dark) {
19 body { background-color: #181a1b; color: #cccccc; }
20 a, a:visited { color: #68A9F8; }
21 code { color: #ff68e1; }
22 pre, blockquote { background-color: black; }
23}
content/templates/base.html added +52
@@ -0,0 +1,52 @@
1{# Page layout for org-ssg.
2
3 Styling comes from styles.css, a verbatim copy of theme/static/styles.css — the
4 stylesheet weblorg already serves. org-ssg publishes assets from inside its source
5 directory, so the copy lives here; `diff content/styles.css theme/static/styles.css`
6 should stay empty, or consolidate the two if you migrate for real.
7
8 Available here: page (.title .url .date .date_iso .tags .excerpt .toc .keywords),
9 site, nav, root, stylesheet. See the org-ssg guide chapter on Templates. #}
10<!DOCTYPE html>
11<html lang="{{ site.language }}">
12<head>
13<meta charset="utf-8">
14<meta name="viewport" content="width=device-width, initial-scale=1">
15<title>{{ page.title }} &middot; {{ site.title }}</title>
16{%- if site.base_url %}
17<link rel="canonical" href="{{ page.url | absolute }}">
18{%- endif %}
19{%- if page.excerpt %}
20<meta name="description" content="{{ page.excerpt | truncate(150) }}">
21{%- endif %}
22<link rel="stylesheet" href="{{ root }}styles.css">
23{%- if stylesheet %}
24<link rel="stylesheet" href="{{ stylesheet }}">
25{%- endif %}
26</head>
27<body>
28<header>
29<nav>
30<ul>
31<li><a href="{{ root }}index.html">{{ site.title }}</a></li>
32{%- for item in nav %}
33<li><a href="{{ item.url }}">{{ item.title }}</a></li>
34{%- endfor %}
35</ul>
36</nav>
37</header>
38<main>
39<h1>{{ page.title }}</h1>
40{%- if page.date_iso %}
41<p><time datetime="{{ page.date_iso }}">{{ page.date_iso }}</time></p>
42{%- endif %}
43{%- if page.tags %}
44<p>{% for tag in page.tags %}<a href="{{ root }}tags/{{ tag }}.html">#{{ tag }}</a> {% endfor %}</p>
45{%- endif %}
46{% block content %}{{ body | safe }}{% endblock %}
47</main>
48<footer>
49<p><a href="{{ root }}index.html">{{ site.title }}</a> &mdash; {{ site.description }}</p>
50</footer>
51</body>
52</html>
content/templates/home.html added +44
@@ -0,0 +1,44 @@
1{# The home page: authored prose plus the most recent posts.
2
3 A generated page, so it has no source .org file — `pages` holds the blog collection's
4 entries, newest first, and the prose lives here. This mirrors what weblorg's
5 theme/templates/index.html does today. #}
6{% extends "base.html" %}
7{% block content %}
8<section>
9<p>[ <a href="https://krz.sh">krz</a> &middot; <a href="https://audit-labs.dev">audit labs</a> ]</p>
10<p>Privacy &middot; Self-Hosting &middot; iOS</p>
11<p>I build tools, write about systems, and run my own infrastructure. Focused on privacy,
12user control, and long-term utility.</p>
13<p><a href="{{ root }}uses/index.html">Uses</a> &middot; <a href="{{ root }}now/index.html">Now</a></p>
14</section>
15
16<section>
17<h2>Recent Posts</h2>
18<p>Things I&rsquo;ve written recently.</p>
19<ul class="post-list">
20{%- for entry in pages %}
21{%- if loop.index <= 3 %}
22<li class="post-list-item">
23{%- if entry.date_iso %}<time datetime="{{ entry.date_iso }}">{{ entry.date_iso }}</time>{% endif %}
24<a href="{{ root }}{{ entry.url }}">{{ entry.title }}</a>
25</li>
26{%- endif %}
27{%- endfor %}
28</ul>
29<p><a href="{{ root }}blog/index.html">All Posts &rarr;</a></p>
30</section>
31
32<section>
33<h2>Elsewhere</h2>
34<p>Places to find me online.</p>
35<ul>
36<li>cgit (primary): <a href="https://git.krz.sh">~cmc</a></li>
37<li>GitHub (mirror): <a href="https://github.com/ccleberg">@ccleberg</a></li>
38<li>Email: <a href="mailto:hello@cleberg.net">hello@cleberg.net</a></li>
39<li>Lemmy: <a href="https://r.nf/u/cmc">@cmc</a></li>
40<li>Mastodon: <a href="https://c.im/@cmc">@cmc</a></li>
41<li>Pixelfed: <a href="https://gram.social/cmc">@cmc</a></li>
42</ul>
43</section>
44{% endblock %}
content/templates/list.html added +13
@@ -0,0 +1,13 @@
1{# A listing page: the blog index, the garden index, and each tag page.
2 Uses the .post-list / .post-list-item classes styles.css already defines. #}
3{% extends "base.html" %}
4{% block content %}
5<ul class="post-list">
6{%- for entry in pages %}
7<li class="post-list-item">
8{%- if entry.date_iso %}<time datetime="{{ entry.date_iso }}">{{ entry.date_iso }}</time>{% endif %}
9<a href="{{ root }}{{ entry.url }}">{{ entry.title }}</a>
10</li>
11{%- endfor %}
12</ul>
13{% endblock %}
content/templates/tags.html added +10
@@ -0,0 +1,10 @@
1{# The tag index at /tags/. Receives `groups` — every tag with its count — rather than
2 `pages`, because it lists tags and not posts. #}
3{% extends "base.html" %}
4{% block content %}
5<ul>
6{%- for tag in groups %}
7<li><a href="{{ root }}{{ tag.url }}">{{ tag.name }}</a> ({{ tag.count }})</li>
8{%- endfor %}
9</ul>
10{% endblock %}