cmc/cleberg.net

My personal web garden & blog.

clone: git clone https://gitbay.org/cmc/cleberg.net.git

f1b8d6d4a89c5d33615556af65aadeb71e7800cb

unsigned

author: Christian Cleberg <hello@cleberg.net> · 2026-08-11T07:20:00Z

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.
 content/index.org           |  2 -
 content/org-ssg.toml        | 95 +++++++++++++++++++++++++++++++++++++++++++++
 content/styles.css          | 23 +++++++++++
 content/templates/base.html | 52 +++++++++++++++++++++++++
 content/templates/home.html | 44 +++++++++++++++++++++
 content/templates/list.html | 13 +++++++
 content/templates/tags.html | 10 +++++
 7 files changed, 237 insertions(+), 2 deletions(-)

diff --git a/content/index.org b/content/index.org
deleted file mode 100644
index 32e428d..0000000
--- a/content/index.org
+++ /dev/null
@@ -1,2 +0,0 @@
-#+title: cleberg.net
-#+description: Stillness amidst the chaos.
diff --git a/content/org-ssg.toml b/content/org-ssg.toml
new file mode 100644
index 0000000..d88e309
--- /dev/null
+++ b/content/org-ssg.toml
@@ -0,0 +1,95 @@
+# org-ssg configuration for cleberg.net.
+#
+# Lives in content/ because that directory is the site's URL root: content/blog/post.org
+# publishes at /blog/post.html. Neither this file nor templates/ is published.
+#
+#   org-ssg serve content -o /tmp/preview      # write and preview
+#   org-ssg 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 org-ssg entirely.
+
+[site]
+title = "cleberg.net"
+description = "Stillness amidst the chaos."
+language = "en"
+# Empty keeps every URL relative, so the site works from a preview directory as well as
+# from its real origin. Set it to "https://cleberg.net" when you want canonical links, or
+# an RSS feed — a feed is read away from the site that served it, so its links cannot be
+# relative.
+base_url = ""
+
+[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 already links home from the site title, and listing
+# it here would print "cleberg.net" twice side by side. Source pages come first in the
+# order listed, then the collections that set `nav = true`.
+mode = "explicit"
+pages = ["salary/index.org"]
+
+[templates]
+dir = "templates"
+
+[highlight]
+theme = "InspiredGitHub"
+
+[html]
+# Level-1 org headings become <h2>, beneath the page title the layout renders as <h1> —
+# which is what the current site does too.
+heading_offset = 1
+toc = true
+section_numbers = false
+
+# --- Generated pages -------------------------------------------------------------
+# These have no source .org file. weblorg produces the equivalents today through its
+# blog-index, garden-index and tags routes.
+
+# 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.
+#
+# 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
+# `git checkout content/index.org`, but then this collection must output somewhere else —
+# two pages cannot claim one URL, and org-ssg fails the build rather than picking a winner.
+[[collections]]
+source = "blog"
+output = "index.html"
+template = "home.html"
+title = "cleberg.net"
+sort = "date"
+order = "desc"
+
+[[collections]]
+source = "blog"
+output = "blog/index.html"
+template = "list.html"
+title = "Blog"
+sort = "date"
+order = "desc"
+nav = true
+
+[[collections]]
+source = "garden"
+output = "garden/index.html"
+template = "list.html"
+title = "Garden"
+sort = "date"
+order = "desc"
+nav = true
+
+# One page per tag, plus the index the live site already has at /tags/. `source` is empty
+# so it groups across the whole site rather than one section, which is how #+FILETAGS: is
+# used here — tags appear on blog posts and garden notes alike.
+[[collections]]
+source = ""
+group_by = "tags"
+output = "tags/{tag}.html"
+template = "list.html"
+title = "Tagged: {tag}"
+index_output = "tags/index.html"
+index_template = "tags.html"
+index_title = "Tags"
+nav = true
diff --git a/content/styles.css b/content/styles.css
new file mode 100644
index 0000000..19e4711
--- /dev/null
+++ b/content/styles.css
@@ -0,0 +1,23 @@
+body { max-width: 800px; margin: 0 auto; padding: 1rem; }
+a, a:visited { color: #0000ff; }
+nav ul { list-style: none; padding: 0; display: flex; flex-direction: row; }
+nav ul li { padding-right: 0.5rem;}
+img { max-width: 100%; }
+.post-list { padding: 0; padding-inline-start: 0; }
+.post-list-item { display: flex; list-style: none; align-items: baseline; }
+.post-list-item time { padding-right: 0.5rem; white-space: nowrap; }
+.post-list-item a { flex-grow: 1; white-space: normal; }
+.post-list-year { list-style: none; margin-top: 1rem; margin-bottom: 0.5rem; font-weight: bold; }
+.skip-link { display: none; }
+footer { margin: 2rem 0;}
+pre, blockquote { margin: 1rem 0; padding: 0.5rem; background-color: #eee; }
+pre { overflow: auto; white-space: pre; }
+blockquote p { margin: 0; margin-bottom: 1rem; }
+blockquote p:last-child { margin-bottom: 0; }
+code { color: #942192; }
+@media (prefers-color-scheme: dark) {
+	body { background-color: #181a1b; color: #cccccc; }
+	a, a:visited { color: #68A9F8; }
+	code { color: #ff68e1; }
+	pre, blockquote { background-color: black; }
+}
diff --git a/content/templates/base.html b/content/templates/base.html
new file mode 100644
index 0000000..19c5509
--- /dev/null
+++ b/content/templates/base.html
@@ -0,0 +1,52 @@
+{# Page layout for org-ssg.
+
+   Styling comes from styles.css, a verbatim copy of theme/static/styles.css — the
+   stylesheet weblorg already serves. org-ssg publishes assets from inside its source
+   directory, so the copy lives here; `diff content/styles.css theme/static/styles.css`
+   should stay empty, or consolidate the two if you migrate for real.
+
+   Available here: page (.title .url .date .date_iso .tags .excerpt .toc .keywords),
+   site, nav, root, stylesheet. See the org-ssg guide chapter on Templates. #}
+<!DOCTYPE html>
+<html lang="{{ site.language }}">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>{{ page.title }} &middot; {{ site.title }}</title>
+{%- if site.base_url %}
+<link rel="canonical" href="{{ page.url | absolute }}">
+{%- endif %}
+{%- if page.excerpt %}
+<meta name="description" content="{{ page.excerpt | truncate(150) }}">
+{%- endif %}
+<link rel="stylesheet" href="{{ root }}styles.css">
+{%- if stylesheet %}
+<link rel="stylesheet" href="{{ stylesheet }}">
+{%- endif %}
+</head>
+<body>
+<header>
+<nav>
+<ul>
+<li><a href="{{ root }}index.html">{{ site.title }}</a></li>
+{%- for item in nav %}
+<li><a href="{{ item.url }}">{{ item.title }}</a></li>
+{%- endfor %}
+</ul>
+</nav>
+</header>
+<main>
+<h1>{{ page.title }}</h1>
+{%- if page.date_iso %}
+<p><time datetime="{{ page.date_iso }}">{{ page.date_iso }}</time></p>
+{%- endif %}
+{%- if page.tags %}
+<p>{% for tag in page.tags %}<a href="{{ root }}tags/{{ tag }}.html">#{{ tag }}</a> {% endfor %}</p>
+{%- endif %}
+{% block content %}{{ body | safe }}{% endblock %}
+</main>
+<footer>
+<p><a href="{{ root }}index.html">{{ site.title }}</a> &mdash; {{ site.description }}</p>
+</footer>
+</body>
+</html>
diff --git a/content/templates/home.html b/content/templates/home.html
new file mode 100644
index 0000000..be5a0e6
--- /dev/null
+++ b/content/templates/home.html
@@ -0,0 +1,44 @@
+{# 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. #}
+{% extends "base.html" %}
+{% block content %}
+<section>
+<p>[ <a href="https://krz.sh">krz</a> &middot; <a href="https://audit-labs.dev">audit labs</a> ]</p>
+<p>Privacy &middot; Self-Hosting &middot; iOS</p>
+<p>I build tools, write about systems, and run my own infrastructure. Focused on privacy,
+user control, and long-term utility.</p>
+<p><a href="{{ root }}uses/index.html">Uses</a> &middot; <a href="{{ root }}now/index.html">Now</a></p>
+</section>
+
+<section>
+<h2>Recent Posts</h2>
+<p>Things I&rsquo;ve written recently.</p>
+<ul class="post-list">
+{%- for entry in pages %}
+{%- if loop.index <= 3 %}
+<li class="post-list-item">
+{%- if entry.date_iso %}<time datetime="{{ entry.date_iso }}">{{ entry.date_iso }}</time>{% endif %}
+<a href="{{ root }}{{ entry.url }}">{{ entry.title }}</a>
+</li>
+{%- endif %}
+{%- endfor %}
+</ul>
+<p><a href="{{ root }}blog/index.html">All Posts &rarr;</a></p>
+</section>
+
+<section>
+<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>Email: <a href="mailto:hello@cleberg.net">hello@cleberg.net</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>
+<li>Pixelfed: <a href="https://gram.social/cmc">@cmc</a></li>
+</ul>
+</section>
+{% endblock %}
diff --git a/content/templates/list.html b/content/templates/list.html
new file mode 100644
index 0000000..6ff9968
--- /dev/null
+++ b/content/templates/list.html
@@ -0,0 +1,13 @@
+{# A listing page: the blog index, the garden index, and each tag page.
+   Uses the .post-list / .post-list-item classes styles.css already defines. #}
+{% extends "base.html" %}
+{% block content %}
+<ul class="post-list">
+{%- for entry in pages %}
+<li class="post-list-item">
+{%- if entry.date_iso %}<time datetime="{{ entry.date_iso }}">{{ entry.date_iso }}</time>{% endif %}
+<a href="{{ root }}{{ entry.url }}">{{ entry.title }}</a>
+</li>
+{%- endfor %}
+</ul>
+{% endblock %}
diff --git a/content/templates/tags.html b/content/templates/tags.html
new file mode 100644
index 0000000..9702f59
--- /dev/null
+++ b/content/templates/tags.html
@@ -0,0 +1,10 @@
+{# The tag index at /tags/. Receives `groups` — every tag with its count — rather than
+   `pages`, because it lists tags and not posts. #}
+{% extends "base.html" %}
+{% block content %}
+<ul>
+{%- for tag in groups %}
+<li><a href="{{ root }}{{ tag.url }}">{{ tag.name }}</a> ({{ tag.count }})</li>
+{%- endfor %}
+</ul>
+{% endblock %}