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] |
| 13 | title = "cleberg.net" |
| 14 | description = "Stillness amidst the chaos." |
| 15 | language = "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. |
| 20 | base_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`. |
| 28 | mode = "explicit" |
| 29 | pages = ["salary/index.org"] |
| 30 | |
| 31 | [templates] |
| 32 | dir = "templates" |
| 33 | |
| 34 | [highlight] |
| 35 | theme = "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. |
| 40 | heading_offset = 1 |
| 41 | toc = true |
| 42 | section_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]] |
| 58 | source = "blog" |
| 59 | output = "index.html" |
| 60 | template = "home.html" |
| 61 | title = "cleberg.net" |
| 62 | sort = "date" |
| 63 | order = "desc" |
| 64 | |
| 65 | [[collections]] |
| 66 | source = "blog" |
| 67 | output = "blog/index.html" |
| 68 | template = "list.html" |
| 69 | title = "Blog" |
| 70 | sort = "date" |
| 71 | order = "desc" |
| 72 | nav = true |
| 73 | |
| 74 | [[collections]] |
| 75 | source = "garden" |
| 76 | output = "garden/index.html" |
| 77 | template = "list.html" |
| 78 | title = "Garden" |
| 79 | sort = "date" |
| 80 | order = "desc" |
| 81 | nav = 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]] |
| 87 | source = "" |
| 88 | group_by = "tags" |
| 89 | output = "tags/{tag}.html" |
| 90 | template = "list.html" |
| 91 | title = "Tagged: {tag}" |
| 92 | index_output = "tags/index.html" |
| 93 | index_template = "tags.html" |
| 94 | index_title = "Tags" |
| 95 | nav = true |
content/styles.css
added
+23
| @@ -0,0 +1,23 @@ |
| 1 | body { max-width: 800px; margin: 0 auto; padding: 1rem; } |
| 2 | a, a:visited { color: #0000ff; } |
| 3 | nav ul { list-style: none; padding: 0; display: flex; flex-direction: row; } |
| 4 | nav ul li { padding-right: 0.5rem;} |
| 5 | img { 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; } |
| 12 | footer { margin: 2rem 0;} |
| 13 | pre, blockquote { margin: 1rem 0; padding: 0.5rem; background-color: #eee; } |
| 14 | pre { overflow: auto; white-space: pre; } |
| 15 | blockquote p { margin: 0; margin-bottom: 1rem; } |
| 16 | blockquote p:last-child { margin-bottom: 0; } |
| 17 | code { 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 }} · {{ 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> — {{ 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> · <a href="https://audit-labs.dev">audit labs</a> ]</p> |
| 10 | <p>Privacy · Self-Hosting · iOS</p> |
| 11 | <p>I build tools, write about systems, and run my own infrastructure. Focused on privacy, |
| 12 | user control, and long-term utility.</p> |
| 13 | <p><a href="{{ root }}uses/index.html">Uses</a> · <a href="{{ root }}now/index.html">Now</a></p> |
| 14 | </section> |
| 15 | |
| 16 | <section> |
| 17 | <h2>Recent Posts</h2> |
| 18 | <p>Things I’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 →</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 %} |