cmc/cleberg.net

My personal web garden & blog.

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

a6ee2e0ca79b83b7f6bd4a521777cffdaf2260f2

unsigned

author: Christian Cleberg <hello@cleberg.net> · 2026-08-12T01:34:02Z

Finish the move off weblorg

Remove what the migration left behind, then cut build.py down to the work
orgo does not do: 443 lines to 168.

- Delete publish.el and theme/templates/, superseded by content/orgo.toml
  and content/templates/. theme/static/ stays; orgo publishes it as assets.
- Drop the tag builder, the org-source copy, and the helpers that only fed
  them. orgo generates /tags/ and a page per tag from a collection.
- Rewrite the README and the migration-era comments that explained the
  config by pointing at what weblorg used to do.
- Serve styles.css as authored. Minifying 1KB of CSS saved 52 bytes over
  the wire once compressed, which did not pay for a build step and a
  toolchain dependency.
- Stop wiping the output directory. The .orgo-cache.json inside it is what
  makes a build incremental, and orgo already prunes outputs whose source
  is gone and re-emits any that are missing. Production keeps .build/ and
  development gets .build-dev/, because production rewrites image URLs in
  the output and a shared directory would let one environment reuse pages
  rendered for the other.

An incremental build is byte-identical to a clean one, and the site output
is unchanged apart from the /uses/ stack row.
 .build.yml.bkp                |  44 -----
 .gitignore                    |   2 +-
 README.md                     | 100 ++++++-----
 build.py                      | 385 ++++++------------------------------------
 content/orgo.toml             |  16 +-
 content/templates/base.html   |  12 +-
 content/templates/blog.html   |   4 +-
 content/templates/feed.xml    |   9 +-
 content/templates/garden.html |   2 +-
 content/templates/home.html   |   5 +-
 content/templates/tags.html   |   9 +-
 content/uses/index.org        |   2 +-
 publish.el                    | 128 --------------
 theme/templates/base.html     |  46 -----
 theme/templates/blog.html     |  13 --
 theme/templates/feed.xml      |  37 ----
 theme/templates/garden.html   |  27 ---
 theme/templates/index.html    |  33 ----
 theme/templates/page.html     |   8 -
 theme/templates/post.html     |  19 ---
 theme/templates/tags.html     |   6 -
 21 files changed, 135 insertions(+), 772 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 <hello@cleberg.net>",
-    "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/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:hello@cleberg.net" 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..6a2cf33 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 %}
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 <hello@cleberg.net>")
-                  ("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/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:hello@cleberg.net" 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("hello@cleberg.net") }}</webMaster>
-
-    {% for post in posts %}
-    <item>
-      <title>{{ post.title }}</title>
-      <link>{{ url_for("blog", slug=post.slug) }}</link>
-      <author>{{ post.author|default("hello@cleberg.net") }}</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="@cmc@c.im">{% 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:hello@cleberg.net">hello@cleberg.net</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:hello@cleberg.net">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 %}