aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.build.yml.bkp44
-rw-r--r--.gitignore2
-rw-r--r--README.md100
-rwxr-xr-xbuild.py385
-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
-rw-r--r--publish.el128
-rw-r--r--theme/static/styles.css4
-rw-r--r--theme/templates/base.html46
-rw-r--r--theme/templates/blog.html13
-rw-r--r--theme/templates/feed.xml37
-rw-r--r--theme/templates/garden.html27
-rw-r--r--theme/templates/index.html33
-rw-r--r--theme/templates/page.html8
-rw-r--r--theme/templates/post.html19
-rw-r--r--theme/templates/tags.html6
23 files changed, 259 insertions, 774 deletions
diff --git a/.build.yml.bkp b/.build.yml.bkp
deleted file mode 100644
index 26193db..0000000
--- a/.build.yml.bkp
+++ /dev/null
@@ -1,44 +0,0 @@
-image: ubuntu/lts
-
-packages:
- - emacs-nox
- - rsync
- - openssh-client
- - git
- - curl
- - tar
-
-sources:
- - https://git.sr.ht/~ccleberg/cleberg.net
-
-secrets:
- - 303fbd30-2970-4d18-b88b-0f6abf822ea5
- - e8b41b71-c4b2-4bd6-8159-25ae1eb7d282
-
-environment:
- ENV: prod
- BUILD: "true"
-
-tasks:
- - setup: |
- curl -LsSf https://astral.sh/uv/install.sh | sh
- mkdir -p ~/.local/bin
- curl -sSfL https://github.com/tdewolff/minify/releases/latest/download/minify_linux_amd64.tar.gz \
- | tar -xz -C ~/.local/bin minify
- mkdir -p ~/.config/emacs/.local/straight/repos
- git clone --depth 1 https://github.com/emacsorphanage/htmlize \
- ~/.config/emacs/.local/straight/repos/htmlize
- git clone --depth 1 https://github.com/emacs-love/weblorg \
- ~/.config/emacs/.local/straight/repos/weblorg
- git clone --depth 1 https://github.com/emacs-love/templatel \
- ~/.config/emacs/.local/straight/repos/templatel
- - build: |
- source ~/.local/bin/env
- cd cleberg.net
- uv run python3 -u build.py
- - deploy: |
- set -euo pipefail
- cd cleberg.net
- chmod 600 ~/.ssh/id_rsa
- chmod 600 ~/.ssh/config
- rsync -r --delete-before .build/ homelab:/var/www/cleberg.net/
diff --git a/.gitignore b/.gitignore
index 3272079..5e1ce88 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.build/
+.build-dev/
.DS_Store
.gitlab-ci-local/
.ruff_cache/
@@ -6,5 +7,4 @@
public/
readme.html
venv/
-theme/static/styles.min.css
__pycache__/
diff --git a/README.md b/README.md
index abd8fa4..4b1df21 100644
--- a/README.md
+++ b/README.md
@@ -3,23 +3,26 @@
This repository holds the files for [cleberg.net](https://cleberg.net),
a static-site with blog posts, personal links, and more.
-This site uses [weblorg](https://github.com/emacs-love/weblorg) to build
-the static site, which relies on
-[emacs](https://www.gnu.org/software/emacs/).
+This site uses [orgo](https://github.com/krazywarez/orgo) to build the
+static site.
## Site Structure
I write content pages (e.g., blog posts) in org-mode and templates in
-HTML. I use Weblorg to build these files into a static site, and then
-deploy them to a web server.
+HTML. orgo builds these files into a static site, and then I deploy
+them to a web server.
The main site components are:
-- Org source files containing content, including blog posts and pages.
-- A configuration file (`publish.el`) that specifies publishing
- parameters like base URL, output directories, and export options.
+- Org source files in `content/`, containing blog posts and pages.
+ That directory is the site's URL root: `content/blog/post.org`
+ publishes at `/blog/post.html`.
+- A configuration file (`content/orgo.toml`) that specifies the base
+ URL, navigation, templates, and the generated pages (home, blog and
+ garden indexes, tags, and the RSS feed).
+- HTML templates in `content/templates/`.
- Assets such as images and style sheets, located in designated
- subdirectories.
+ subdirectories. `theme/static/` is published at `/`.
- Utility scripts (e.g., `build.py`) to facilitate building and
deployment.
@@ -27,24 +30,25 @@ The main site components are:
The publishing system depends on:
-- Emacs text editor with Org-Mode.
-- The weblorg package, available at
- [emacs-love/weblorg](https://github.com/emacs-love/weblorg), which
- provides advanced Org publishing functionality and theming support.
+- [orgo](https://github.com/krazywarez/orgo), the static site
+ generator, installed with `cargo install --git
+ https://github.com/krazywarez/orgo`.
+- `rsync` for deployment.
+- [uv](https://github.com/astral-sh/uv) to run `build.py`.
## Configuration
-You can customize site settings can within the `publish.el` file. This
-file establishes key variables such as:
+You can customize site settings within the `content/orgo.toml` file.
+This file establishes key variables such as:
- The base URL for links.
-- Output directories.
-- Publishing rules to define which files to convert and how.
-- Theme settings managed by weblorg.
+- Which pages appear in the navigation, and in what order.
+- Per-directory template rules.
+- The collections that generate the indexes, tag pages, and feed.
Users intending to modify site parameters should review and edit this
-file accordingly. The weblorg documentation contains extensive details
-on configuration options and expected formats.
+file accordingly. The orgo documentation contains extensive details on
+configuration options and expected formats.
## Setup Instructions
@@ -62,48 +66,54 @@ For users employing Doom Emacs, open any repository Org file using
## Building and Publishing the Site
-The publishing process involves invoking Emacs with the `publish.el`
-script, which performs the export of Org documents to HTML output.
+The `build.py` script wraps the build: it runs orgo, and then either
+deploys the result or serves it locally.
-Configure the environment variable `ENV` as follows:
+Two environment variables control what it does, and both default to
+off:
-- If you set `ENV` to `prod`, the script uses production base URL
- settings as defined in `publish.el`.
-- If you do not set `ENV` or set it differently, the script defaults to
- development settings, typically using `localhost:8000` as the base
- URL.
+- `BUILD=true` performs the build.
+- `DEPLOY=true` deploys in production, or starts a development server
+ on port 8000 otherwise.
-Example commands to build the site:
+A third, `ENV`, selects the mode. Setting `ENV=prod` rewrites image
+URLs to be root-relative (see Deployment below) and skips the `ruff`
+pass; anything else builds for development.
``` shell
# Production build:
-ENV=prod emacs --script publish.el
+ENV=prod BUILD=true uv run build.py
# Development build:
-emacs --script publish.el
+BUILD=true uv run build.py
+
+# Development build, then serve it on localhost:8000:
+BUILD=true DEPLOY=true uv run build.py
```
-Generated site files reside in the designated output directory, ready
-for deployment. You can deploy the resulting static site files via
-standard file transfer protocols such as `scp` or SFTP.
+Generated site files reside in `.build/` for production and
+`.build-dev/` for development, ready for deployment. You can deploy the
+resulting static site files via standard file transfer protocols such as
+`scp` or SFTP.
-The `build.py` script automates the build process. You can execute this
-script with or without the `ENV` variable to perform production or
-development builds respectively.
+Builds are incremental: orgo keeps an `.orgo-cache.json` inside the
+output directory and re-renders only what changed, so the directory is
+left in place between runs. Delete it for a clean build. The two
+environments use separate directories because production rewrites image
+URLs in the output and development does not.
-``` shell
-# Production build script:
-ENV=prod uv run build.py
+orgo can also build and preview on its own, without `build.py`:
-# Development build script:
-uv run build.py
+``` shell
+orgo serve content -o /tmp/preview
```
## Deployment
-Production builds emit root-relative URLs (`/styles.min.css`,
-`/img/blog/...`) instead of absolute `https://` ones so the site renders
-standalone on the onion service without fetching assets off-onion.
+Production builds rewrite image URLs to root-relative ones
+(`/img/blog/...`) instead of absolute `https://img.cleberg.net/` ones so
+the site renders standalone on the onion service without fetching assets
+off-onion. The stylesheet is already same-origin.
This means the web server must serve the image store at `/img/` on the
`cleberg.net` vhost. The images live at `/var/www/img/` (their own host,
diff --git a/build.py b/build.py
index 0d90e2b..61f6110 100755
--- a/build.py
+++ b/build.py
@@ -1,23 +1,23 @@
#!/usr/bin/env python3
"""
-This script automates the process of building, testing, and deploying the website.
+This script automates the process of building and deploying the website.
It handles tasks such as:
-- Removing and recreating the build directory.
-- Minifying CSS assets.
-- Running the Emacs publishing script to generate site content.
-- Updating the index.html file with the latest blog posts.
+- Running orgo to generate site content.
+- Rewriting image URLs for the onion service (production only).
- Optionally deploying the built site to a remote server.
- Starting a local development server for previewing changes.
Usage:
- Set the environment variable ENV to 'prod' for production builds.
- Run the script to perform the build process accordingly.
+ Set BUILD=true to build, DEPLOY=true to deploy or serve.
+ Set ENV=prod for production builds; anything else builds for development.
+
+ Builds are incremental. Production writes .build/ and development .build-dev/;
+ delete either one for a clean build.
Dependencies:
- Python 3
- - Emacs with the publish.el script
- - minify tool for CSS minification
+ - orgo, the static site generator (reads content/orgo.toml)
- rsync for deployment
Author:
@@ -26,202 +26,24 @@ Author:
import os
import re
-import shutil
import subprocess
import sys
-from datetime import datetime
-from html import escape
from pathlib import Path
-SITE_TEMPLATE_VARS = {
- "site_name": "cleberg.net",
- "site_owner": "Christian Cleberg <[email protected]>",
- "site_description": "Stillness amidst the chaos.",
-}
+
+def run(cmd, error):
+ """Run cmd quietly, exiting with its stderr if it fails."""
+ result = subprocess.run(cmd, capture_output=True, text=True, check=False)
+ if result.returncode != 0:
+ print(error, file=sys.stderr)
+ print(result.stderr, file=sys.stderr)
+ sys.exit(1)
def run_ruff():
print("Running ruff...")
for cmd in [["ruff", "check", "--fix"], ["ruff", "format"]]:
- result = subprocess.run(cmd, capture_output=True, text=True, check=False)
- if result.returncode != 0:
- print(f"ruff error ({' '.join(cmd)}):")
- print(result.stderr, file=sys.stderr)
- sys.exit(1)
-
-
-def render_base_template(main_html, subtitle="", title=None):
- """
- Render a small subset of the site's shared templates for Python-generated
- pages that still need to follow the common chrome.
- """
- base_template = Path("theme/templates/base.html").read_text(encoding="utf-8")
-
- if title is None:
- title = SITE_TEMPLATE_VARS["site_name"]
-
- rendered = base_template
- rendered = rendered.replace(
- "{% block subtitle %}{% endblock %}",
- escape(subtitle),
- )
- rendered = rendered.replace(
- '{% block title %}{{ site_name | default("cleberg.net") }}{% endblock %}',
- escape(title),
- )
- rendered = rendered.replace(
- '{% if site_owner is defined %}<meta name="author" content="{{ site_owner }}">{% endif %}',
- f'<meta name="author" content="{escape(SITE_TEMPLATE_VARS["site_owner"])}">',
- )
- rendered = rendered.replace(
- '{% if site_description is defined %}<meta name="description" content="{{ site_description }}">{% endif %}',
- f'<meta name="description" content="{escape(SITE_TEMPLATE_VARS["site_description"])}">',
- )
- rendered = rendered.replace(
- '{% if site_keywords is defined %}<meta name="keywords" content="{{ site_keywords }}">{% endif %}',
- "",
- )
- rendered = rendered.replace("{% block meta %}{% endblock %}", "")
- rendered = rendered.replace("{% block head %}", "")
- rendered = rendered.replace("{% endblock %}", "", 1)
- rendered = rendered.replace("{% block main %}{% endblock %}", main_html)
-
- return rendered
-
-
-def render_tags_page_html(tags_html):
- """
- Render the tags page by using the shared tags/base templates instead of a
- hand-authored standalone HTML document.
- """
- tags_template = Path("theme/templates/tags.html").read_text(encoding="utf-8")
-
- main_html = tags_template
- main_html = main_html.replace('{% extends "base.html" %}', "")
- main_html = main_html.replace(
- "{% block subtitle %}tags - {% endblock %}",
- "",
- )
- main_html = main_html.replace("{% block main %}", "")
- main_html = main_html.replace("{% endblock %}", "")
- main_html = main_html.replace("<!-- BEGIN_TAGS -->\n<!-- END_TAGS -->", tags_html)
-
- return render_base_template(main_html.strip(), subtitle="tags - ")
-
-
-def get_blog_posts(content_dir="./content/blog"):
- """
- Scan blog posts and return normalized metadata for non-draft entries.
- """
- posts = []
-
- header_patterns = {
- "title": re.compile(r"^#\+title:\s*(.+)$", re.IGNORECASE),
- "date": re.compile(r"^#\+date:\s*[\[<](\d{4}-\d{2}-\d{2})"),
- "slug": re.compile(r"^#\+slug:\s*(.+)$", re.IGNORECASE),
- "tags": re.compile(r"^#\+filetags:\s*(.+)$", re.IGNORECASE),
- "draft": re.compile(r"^#\+draft:\s*(.+)$", re.IGNORECASE),
- }
-
- for org_path in Path(content_dir).glob("*.org"):
- title = None
- date_str = None
- slug = None
- tags = []
- is_draft = False
-
- with org_path.open("r", encoding="utf-8") as f:
- for line in f:
- if title is None:
- m = header_patterns["title"].match(line)
- if m:
- title = m.group(1).strip()
- continue
-
- if date_str is None:
- m = header_patterns["date"].match(line)
- if m:
- # date_str is just YYYY-MM-DD
- date_str = m.group(1)
- continue
-
- if slug is None:
- m = header_patterns["slug"].match(line)
- if m:
- slug = m.group(1).strip()
- continue
-
- if not tags:
- m = header_patterns["tags"].match(line)
- if m:
- raw = m.group(1).strip().strip(":")
- tags = [t.strip() for t in raw.split(":") if t.strip()]
- continue
-
- m = header_patterns["draft"].match(line)
- if m:
- draft_value = m.group(1).strip().lower()
- if draft_value != "nil":
- is_draft = True
- break
- continue
-
- # Stop scanning once we have all required fields
- if title and date_str and slug:
- break
-
- if is_draft:
- continue
-
- if title and date_str and slug:
- try:
- date_obj = datetime.strptime(date_str, "%Y-%m-%d")
- date_full = date_obj.strftime("%Y-%m-%d")
- except ValueError:
- # Skip files with invalid date format
- continue
-
- posts.append(
- {
- "title": title,
- "date_str": date_str,
- "date_obj": date_obj,
- "date_full": date_full,
- "slug": slug,
- "tags": tags,
- }
- )
-
- posts.sort(key=lambda x: x["date_obj"], reverse=True)
- return posts
-
-
-def prompt(prompt_text):
- try:
- return input(prompt_text).strip()
- except EOFError:
- return ""
-
-
-def remove_build_directory(build_dir):
- if build_dir.exists():
- print(f"Removing previous build directory: {build_dir}/")
- shutil.rmtree(build_dir)
- build_dir.mkdir(parents=True, exist_ok=True)
-
-
-def minify_css(src_css, dest_css):
- print(f"Minifying CSS: {src_css} → {dest_css}")
- result = subprocess.run(
- ["minify", "-o", str(dest_css), str(src_css)],
- capture_output=True,
- text=True,
- check=False,
- )
- if result.returncode != 0:
- print("Error during CSS minification:")
- print(result.stderr, file=sys.stderr)
- sys.exit(1)
+ run(cmd, f"ruff error ({' '.join(cmd)}):")
def rewrite_img_urls(build_dir=".build"):
@@ -249,22 +71,26 @@ def rewrite_img_urls(build_dir=".build"):
print(f"Rewrote {count} img.cleberg.net references to /img/")
-def run_orgo_build(dev_mode=True):
+def run_orgo_build(build_dir):
"""
Build the site with orgo.
- Replaces the weblorg/Emacs publish. orgo reads content/orgo.toml, so the routes,
- templates and collections that used to live in publish.el live there now. Four of the
- steps this script used to perform afterwards are gone with it: the blog index groups
- itself by year, the tags page is a collection, the recent-posts list is generated from
- the blog collection, and the sitemap is written by orgo once base_url is set.
+ orgo reads content/orgo.toml, which holds the routes, templates and collections: the
+ blog index groups itself by year, the tags page is a collection, the recent-posts
+ list comes from the blog collection, and the sitemap is written once base_url is set.
+
+ The output directory is left in place between builds rather than wiped, because the
+ .orgo-cache.json inside it is what makes a build incremental: orgo re-renders only
+ the pages whose content, config or templates changed, re-emits any page whose output
+ is missing, and deletes the outputs of pages that have since been removed. Wiping the
+ directory would throw that away and force a full render every time. For a clean build
+ from scratch, delete the output directory by hand.
- What is left around it is what orgo does not do: minifying CSS, copying the org
- sources for readers who want them, and rewriting image URLs for the onion.
+ What is left around it is what orgo does not do: rewriting image URLs for the onion.
"""
print("Building with orgo...")
result = subprocess.run(
- ["orgo", "build", "content", "-o", ".build", "--strict"],
+ ["orgo", "build", "content", "-o", str(build_dir), "--strict"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
@@ -276,99 +102,10 @@ def run_orgo_build(dev_mode=True):
sys.exit(1)
-def copy_org_sources(content_dir="./content", build_dir="./.build/org"):
- print(f"Copying org sources: {content_dir} → {build_dir}")
- if os.path.exists(build_dir):
- shutil.rmtree(build_dir)
-
- slug_pattern = re.compile(r"^#\+slug:\s*(.+)$", re.IGNORECASE)
-
- for src_path in Path(content_dir).rglob("*.org"):
- rel_dir = src_path.parent.relative_to(content_dir)
- dest_dir = Path(build_dir) / rel_dir
- dest_dir.mkdir(parents=True, exist_ok=True)
-
- # Try to extract slug from file headers
- slug = None
- with src_path.open("r", encoding="utf-8") as f:
- for line in f:
- m = slug_pattern.match(line)
- if m:
- slug = m.group(1).strip()
- break
-
- dest_name = f"{slug}.org" if slug else src_path.name
- shutil.copy2(src_path, dest_dir / dest_name)
- if slug:
- print(f" {src_path.name} → {dest_name}")
-
-
-def get_tags_html(content_dir="./content/blog"):
- """
- Build the tag index HTML snippet for the rendered tags template.
- """
- preferred_tag_order = [
- "audit",
- "emacs",
- "development",
- "ios",
- "linux",
- "personal",
- "privacy",
- "security",
- "self-hosting",
- "web",
- ]
-
- tag_map = {}
-
- for post in get_blog_posts(content_dir):
- for tag in post["tags"]:
- tag_map.setdefault(tag, []).append(
- {
- "title": post["title"],
- "slug": post["slug"],
- "date_obj": post["date_obj"],
- "date_str": post["date_str"],
- }
- )
-
- ordered_tags = [tag for tag in preferred_tag_order if tag in tag_map]
- ordered_tags.extend(
- sorted(tag for tag in tag_map if tag not in preferred_tag_order)
- )
-
- for tag in ordered_tags:
- tag_map[tag].sort(key=lambda x: x["date_obj"], reverse=True)
-
- toc_items = "".join(
- f'<li><a href="#{tag}">{tag}</a> <span class="tag-count">({len(tag_map[tag])})</span></li>'
- for tag in ordered_tags
- )
-
- sections = []
- for tag in ordered_tags:
- posts = tag_map[tag]
- items = "\n".join(
- f'<li class="post-list-item">'
- f'<time datetime="{p["date_str"]}">{p["date_str"]}</time>'
- f'<a href="/blog/{p["slug"]}.html">{p["title"]}</a>'
- f"</li>"
- for p in posts
- )
- sections.append(
- f'<h2 id="{tag}">{tag}</h2>\n<ul class="post-list">\n{items}\n</ul>'
- )
-
- return f'<ul class="tag-toc">{toc_items}</ul>\n' + "".join(
- f"<section>{section}</section>" for section in sections
- )
-
-
def deploy_to_server(build_dir, server):
remote_path = f"{server}:/var/www/cleberg.net/"
- print(f"Deploying .build/ → {remote_path}")
- result = subprocess.run(
+ print(f"Deploying {build_dir}/ → {remote_path}")
+ run(
# The build cache lives in the output directory because it describes it, but it
# is not part of the site. Excluding it also stops --delete removing it locally.
[
@@ -380,14 +117,8 @@ def deploy_to_server(build_dir, server):
f"{build_dir}/",
remote_path,
],
- capture_output=True,
- text=True,
- check=False,
+ "Error during rsync deployment:",
)
- if result.returncode != 0:
- print("Error during rsync deployment:")
- print(result.stderr, file=sys.stderr)
- sys.exit(1)
def start_dev_server(build_dir):
@@ -404,38 +135,32 @@ def start_dev_server(build_dir):
def main():
- env = os.environ.get("ENV", "").casefold()
- if env != "prod":
+ prod = os.environ.get("ENV", "").casefold() == "prod"
+ if not prod:
run_ruff()
- build_dir = Path(".build")
- theme_dir = Path("theme/static")
- css_src = theme_dir / "styles.css"
- css_min = theme_dir / "styles.min.css"
-
- build = os.environ.get("BUILD", "").casefold() == "true"
- deploy = os.environ.get("DEPLOY", "").casefold() == "true"
-
- if env == "prod":
- print("Environment: Production")
- if build:
- remove_build_directory(build_dir)
- minify_css(css_src, css_min)
- run_orgo_build(dev_mode=False)
- copy_org_sources()
+ # One output directory per environment, because the two differ after orgo has run:
+ # production rewrites image URLs in place and development does not. Sharing a
+ # directory would let an incremental build reuse a page rendered for the other one —
+ # a dev preview showing /img/ paths that only resolve on the server. Production keeps
+ # .build/ because that is the directory the deploy and the CI manifest name.
+ build_dir = Path(".build" if prod else ".build-dev")
+
+ print(f"Environment: {'Production' if prod else 'Development'}")
+
+ if os.environ.get("BUILD", "").casefold() == "true":
+ run_orgo_build(build_dir)
+ # The onion needs same-origin images; dev previews keep the absolute URLs.
+ # Runs over every page, not just the re-rendered ones, so a page carried over
+ # from an earlier build is rewritten too.
+ if prod:
rewrite_img_urls(build_dir)
- if deploy:
+
+ if os.environ.get("DEPLOY", "").casefold() == "true":
+ if prod:
print("Deploying to production...")
deploy_to_server(build_dir, "homelab")
- return
- else:
- print("Environment: Development")
- if build:
- remove_build_directory(build_dir)
- minify_css(css_src, css_min)
- run_orgo_build(dev_mode=True)
- copy_org_sources()
- if deploy:
+ else:
start_dev_server(build_dir)
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. |
diff --git a/publish.el b/publish.el
deleted file mode 100644
index aecdb0f..0000000
--- a/publish.el
+++ /dev/null
@@ -1,128 +0,0 @@
-;;; -*- lexical-binding: t -*-
-(setq debug-on-error t)
-
-;; Allow for macOS (dev machine) & Linux (GitHub Actions) execution
-(defvar site-lisp-base
- (expand-file-name "~/.config/emacs/.local/straight/repos"))
-
-;; Explicitly load packages
-(add-to-list 'load-path (expand-file-name "htmlize" site-lisp-base))
-(add-to-list 'load-path (expand-file-name "weblorg" site-lisp-base))
-(add-to-list 'load-path (expand-file-name "templatel" site-lisp-base))
-
-(require 'htmlize)
-(require 'templatel)
-(require 'weblorg)
-
-;; Set default URL for Weblorg
-;; Only works if environment variable ENV=prod
-(if (string-equal-ignore-case (or (getenv "ENV") "") "prod")
- (setq weblorg-default-url "https://cleberg.net"))
-
-;; Define site metadata
-(weblorg-site
- :theme nil
- :template-vars '(("site_name" . "cleberg.net")
- ("site_owner" . "Christian Cleberg <[email protected]>")
- ("site_description" . "Stillness amidst the chaos.")))
-
-;; Define routes for rendering content
-
-;; Index page route
-(weblorg-route
- :name "index"
- :input-pattern "content/*.org"
- :template "index.html"
- :output ".build/index.html"
- :url "/")
-
-;; Blog post route
-(weblorg-route
- :name "blog"
- :input-pattern "content/blog/*.org"
- :template "post.html"
- :output ".build/blog/{{ slug }}.html"
- :url "/blog/{{ slug }}.html")
-
-;; Blog index page route
-(weblorg-route
- :name "blog-index"
- :input-pattern "content/blog/*.org"
- :input-aggregate #'weblorg-input-aggregate-all-desc
- :template "blog.html"
- :output ".build/blog/index.html"
- :url "/blog/")
-
-;; Garden post route
-(weblorg-route
- :name "garden"
- :input-pattern "content/garden/*.org"
- :template "post.html"
- :output ".build/garden/{{ slug }}.html"
- :url "/garden/{{ slug }}.html")
-
-;; Garden index page route
-(weblorg-route
- :name "garden-index"
- :input-pattern "content/garden/*.org"
- :input-aggregate #'weblorg-input-aggregate-all-desc
- :template "garden.html"
- :output ".build/garden/index.html"
- :url "/garden/")
-
-;; Page post route
-(weblorg-route
- :name "pages"
- :input-pattern "content/*.org"
- :template "page.html"
- :output ".build/{{ slug }}.html"
- :url "/{{ slug }}.html")
-
-;; Salary page route
-(weblorg-route
- :name "salary"
- :input-pattern "content/salary/*.org"
- :template "page.html"
- :output ".build/salary/{{ slug }}.html"
- :url "/salary/{{ slug }}.html")
-
-;; Now page route
-(weblorg-route
- :name "now"
- :input-pattern "content/now/*.org"
- :template "post.html"
- :output ".build/now/{{ slug }}.html"
- :url "/now/{{ slug }}.html")
-
-;; Uses page route
-(weblorg-route
- :name "uses"
- :input-pattern "content/uses/*.org"
- :template "page.html"
- :output ".build/uses/{{ slug }}.html"
- :url "/uses/{{ slug }}.html")
-
-;; Tips page route
-(weblorg-route
- :name "tips"
- :input-pattern "content/tips/*.org"
- :template "page.html"
- :output ".build/tips/{{ slug }}.html"
- :url "/tips/{{ slug }}.html")
-
-;; RSS feed route
-(weblorg-route
- :name "rss"
- :input-pattern "content/blog/*.org"
- :input-aggregate #'weblorg-input-aggregate-all-desc
- :template "feed.xml"
- :output ".build/feed.xml"
- :url "/feed.xml")
-
-;; Copy static assets and output to .build directory
-(weblorg-copy-static
- :output ".build/{{ file }}"
- :url "/{{ file }}")
-
-;; Export all content using Weblorg engine
-(weblorg-export)
diff --git a/theme/static/styles.css b/theme/static/styles.css
index 19e4711..6f9ef0c 100644
--- a/theme/static/styles.css
+++ b/theme/static/styles.css
@@ -9,6 +9,9 @@ img { max-width: 100%; }
.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; }
+table { border-collapse: collapse; }
+table, th, td { border: 1px solid #ccc; }
+th, td { padding: 0.25rem 0.5rem; text-align: left; }
footer { margin: 2rem 0;}
pre, blockquote { margin: 1rem 0; padding: 0.5rem; background-color: #eee; }
pre { overflow: auto; white-space: pre; }
@@ -20,4 +23,5 @@ code { color: #942192; }
a, a:visited { color: #68A9F8; }
code { color: #ff68e1; }
pre, blockquote { background-color: black; }
+ table, th, td { border-color: #444; }
}
diff --git a/theme/templates/base.html b/theme/templates/base.html
deleted file mode 100644
index 68ff487..0000000
--- a/theme/templates/base.html
+++ /dev/null
@@ -1,46 +0,0 @@
-<!doctype html>
-<html lang="en-us">
-<head>
- {% block head %}
- <meta charset="utf-8">
- <title>{% block subtitle %}{% endblock %}{% block title %}{{ site_name | default("cleberg.net") }}{% endblock %}</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- {% if site_owner is defined %}<meta name="author" content="{{ site_owner }}">{% endif %}
- {% if site_description is defined %}<meta name="description" content="{{ site_description }}">{% endif %}
- {% if site_keywords is defined %}<meta name="keywords" content="{{ site_keywords }}">{% endif %}
- <link rel="alternate" type="application/rss+xml" title="cleberg.net" href="/feed.xml">
- <link rel="stylesheet" href="/styles.min.css" type="text/css">
- <link rel="icon" href="data:,">
- {% block meta %}{% endblock %}
- {% endblock %}
- <link href="https://github.com/ccleberg" rel="me">
- <link href="https://gitlab.com/ccleberg" rel="me">
- <link href="mailto:[email protected]" rel="me">
- <a href="https://c.im/@cmc" rel="me">
-</head>
-<body>
- <a href="#main-content" class="skip-link">Skip to content</a>
-
- <nav aria-label="Site Navigation">
- <ul>
- <li><a href="/">Home</a></li>
- <li><a href="/blog/">Blog</a></li>
- <li><a href="/garden/">Garden</a></li>
- <li><a href="/salary/">Salary</a></li>
- </ul>
- </nav>
-
- <main id="main-content">
- {% block main %}{% endblock %}
- </main>
-
- <footer>
- <a href="https://git.krz.sh/cmc/cleberg.net.git/">Source</a> &middot;
- <a href="/feed.xml">RSS</a> &middot;
- <a href="http://paske4urhs6nttrtlkuwa5cowum3fjkc6yv6kl4ncx3mjxcd77764nqd.onion/">Onion</a> &middot;
- <a href="/tips/">Tips</a> &middot;
- <a href="https://iheartrss.com/">I &hearts; RSS</a>
- <p>[ <a href="https://krz.sh">krz</a> &middot; <a href="https://audit-labs.dev">audit labs</a> ]</p>
- </footer>
-</body>
-</html>
diff --git a/theme/templates/blog.html b/theme/templates/blog.html
deleted file mode 100644
index b2e2b72..0000000
--- a/theme/templates/blog.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "base.html" %} {% block subtitle %}blog - {% endblock %} {% block main %}
-<h1>Blog</h1>
-<p>Use <code>⌘ F</code> / <code>Ctrl F</code> to search &middot; <a href="/feed.xml">RSS Feed</a></p>
-<p>Want to browse by topic instead? Head over to the <a href="/tags/">tags</a> page.</p>
-<ul class="post-list">
- {% for post in posts %}
- <li class="post-list-item">
- <time datetime='{{ post.date | strftime("%Y-%m-%d") }}'>{{ post.date|strftime("%Y-%m-%d") }}</time>
- <a href='{{ url_for("blog", slug=post.slug) }}'>{{ post.title }}</a>
- </li>
- {% endfor %}
-</ul>
-{% endblock %}
diff --git a/theme/templates/feed.xml b/theme/templates/feed.xml
deleted file mode 100644
index 7b23994..0000000
--- a/theme/templates/feed.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<rss version="2.0"
- xmlns:content="http://purl.org/rss/1.0/modules/content/"
- xmlns:wfw="http://wellformedweb.org/CommentAPI/"
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:atom="http://www.w3.org/2005/Atom"
- xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
- xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
- xmlns:georss="http://www.georss.org/georss"
- xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
- xmlns:media="http://search.yahoo.com/mrss/">
- <channel>
- <title>{{ site_name | default("cleberg.net") }}</title>
- <atom:link
- href="{{ url_for("rss") }}"
- rel="self" type="application/rss+xml" />
- <link>{{ url_for("index") }}</link>
- <description><![CDATA[]]></description>
- <language>en</language>
- <pubDate>{{ posts | first | getattr("date") | strftime("%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
- <lastBuildDate>{{ now() | strftime("%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate>
- <generator>{{ meta.generator }}</generator>
- <webMaster>{{ site_owner | default("[email protected]") }}</webMaster>
-
- {% for post in posts %}
- <item>
- <title>{{ post.title }}</title>
- <link>{{ url_for("blog", slug=post.slug) }}</link>
- <author>{{ post.author|default("[email protected]") }}</author>
- <guid isPermaLink="false">{{ url_for("blog", slug=post.slug) }}</guid>
- <pubDate>{{ post.date|strftime("%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
- <description><![CDATA[{{ post.html|safe }}]]></description>
- </item>
- {% endfor %}
-
- </channel>
-</rss>
diff --git a/theme/templates/garden.html b/theme/templates/garden.html
deleted file mode 100644
index a14d50c..0000000
--- a/theme/templates/garden.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{% extends "base.html" %} {% block subtitle %}garden - {% endblock %} {% block main %}
-<h1>Garden</h1>
-<p>Notes in various states of growth. Some are stubs, some are developed. Browse
-around.</p>
-<section>
- <h2>🪴 The Growth Stages</h2>
- <p>Instead of dates, these metadata labels show how mature a page is:</p>
- <span><strong>🌱 Seedlings:</strong> Rough notes, raw ideas, or "half-baked"
- thoughts.</span><br>
- <span><strong>🌿 Budding:</strong> Notes that have some structure and a few
- internal links.</span><br>
- <span><strong>🌳 Evergreen:</strong> High-conviction ideas that are
- well-researched and stable.</span><br>
- <span><strong>🍂 Compost:</strong> Ideas you no longer agree with but want to
- keep for historical context.</span><br><br>
-</section>
-<ul>
- {% for post in posts %}
- <li>
- <a href='{{ url_for("garden", slug=post.slug) }}'>{{ post.title }}</a>
- {% if post.description %}
- <span>{{ post.description }}</span>
- {% endif %}
- </li>
- {% endfor %}
-</ul>
-{% endblock %}
diff --git a/theme/templates/index.html b/theme/templates/index.html
deleted file mode 100644
index 75fc83d..0000000
--- a/theme/templates/index.html
+++ /dev/null
@@ -1,33 +0,0 @@
-{% extends "base.html" %}
-{% block meta %}<meta name="bubbles:fediverse" content="@[email protected]">{% endblock %}
-{% block main %}
-<section>
- <h1>Christian Cleberg</h1>
- <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="/uses/">Uses</a> &middot; <a href="/now/">Now</a></p>
-</section>
-
-<section>
- <h2>Recent Posts</h2>
- <p>Things I've written recently.</p>
- <ul class="post-list">
- <!-- BEGIN_POSTS --><!-- END_POSTS -->
- </ul>
- <p><a href="/blog/">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:[email protected]">[email protected]</a> (<a href="/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>
- <li>Pixelfed: <a href="https://gram.social/cmc">@cmc</a></li>
- </ul>
-</section>
-{% endblock %}
diff --git a/theme/templates/page.html b/theme/templates/page.html
deleted file mode 100644
index 6c22589..0000000
--- a/theme/templates/page.html
+++ /dev/null
@@ -1,8 +0,0 @@
-{% extends "base.html" %}
-{% block subtitle %}{{ post.title }} - {% endblock %}
-{% block main %}
-<article>
- <h1>{{ post.title }}</h1>
- <div>{{ post.html | safe }}</div>
-</article>
-{% endblock %}
diff --git a/theme/templates/post.html b/theme/templates/post.html
deleted file mode 100644
index a2951b0..0000000
--- a/theme/templates/post.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{% extends "base.html" %} {% block subtitle %} {{ post.title }} - {% endblock %}
-{% block meta %}
-<meta property="og:title" content="{{ post.title }}" />
-<meta property="og:url" content="{{ post.url }}" />
-{% if post.description is defined %}
-<meta property="og:description" content="{{ post.description }}" />
-{% elif project_description is defined %}
-<meta property="og:description" content="{{ project_description }}" />
-{% endif %}
-{% endblock %}
-{% block main %}
-<article>
- <h1>{{ post.title }}</h1>
- <p><time datetime='{{ post.date|strftime("%Y-%m-%d") }}'>{{ post.date|strftime("%Y-%m-%d") }}</time>{% if post.description is defined %} &middot; {{ post.description }}{% endif %}</p>
- <div>{{ post.html | safe }}</div>
- <div><a href="mailto:[email protected]">Reply via email &rarr;</a></div>
- <div><i>...or, comment on this post on <a href="https://bubbles.town/source/cleberg.net">Bubbles</a>!</i></div>
-</article>
-{% endblock %}
diff --git a/theme/templates/tags.html b/theme/templates/tags.html
deleted file mode 100644
index 83619d4..0000000
--- a/theme/templates/tags.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{% extends "base.html" %} {% block subtitle %}tags - {% endblock %} {% block main %}
-<h1>Tags</h1>
-<p>Browse posts by topic.</p>
-<!-- BEGIN_TAGS -->
-<!-- END_TAGS -->
-{% endblock %}