diff options
| author | Christian Cleberg <[email protected]> | 2026-08-11 20:34:02 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-08-11 20:34:02 -0500 |
| commit | ab149c1ae4fe38048fc2d32edac6061e7f3b4638 (patch) | |
| tree | 984019a98b20789890566c595b52d5ddc8b21064 | |
| parent | 5be4598b09a2685d73665b403bf80aad50b15834 (diff) | |
| download | cleberg.net-migrate-to-orgo.tar.gz cleberg.net-migrate-to-orgo.tar.bz2 cleberg.net-migrate-to-orgo.zip | |
Finish the move off weblorgmigrate-to-orgo
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.
| -rw-r--r-- | .build.yml.bkp | 44 | ||||
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | README.md | 100 | ||||
| -rwxr-xr-x | build.py | 385 | ||||
| -rw-r--r-- | content/orgo.toml | 16 | ||||
| -rw-r--r-- | content/templates/base.html | 12 | ||||
| -rw-r--r-- | content/templates/blog.html | 4 | ||||
| -rw-r--r-- | content/templates/feed.xml | 9 | ||||
| -rw-r--r-- | content/templates/garden.html | 2 | ||||
| -rw-r--r-- | content/templates/home.html | 5 | ||||
| -rw-r--r-- | content/templates/tags.html | 9 | ||||
| -rw-r--r-- | content/uses/index.org | 2 | ||||
| -rw-r--r-- | publish.el | 128 | ||||
| -rw-r--r-- | theme/templates/base.html | 46 | ||||
| -rw-r--r-- | theme/templates/blog.html | 13 | ||||
| -rw-r--r-- | theme/templates/feed.xml | 37 | ||||
| -rw-r--r-- | theme/templates/garden.html | 27 | ||||
| -rw-r--r-- | theme/templates/index.html | 33 | ||||
| -rw-r--r-- | theme/templates/page.html | 8 | ||||
| -rw-r--r-- | theme/templates/post.html | 19 | ||||
| -rw-r--r-- | 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/ @@ -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__/ @@ -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, @@ -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/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..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 <[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/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> · - <a href="/feed.xml">RSS</a> · - <a href="http://paske4urhs6nttrtlkuwa5cowum3fjkc6yv6kl4ncx3mjxcd77764nqd.onion/">Onion</a> · - <a href="/tips/">Tips</a> · - <a href="https://iheartrss.com/">I ♥ RSS</a> - <p>[ <a href="https://krz.sh">krz</a> · <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 · <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> · <a href="https://audit-labs.dev">audit labs</a> ]</p> - <p>Privacy · Self-Hosting · 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> · <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 →</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 %} · {{ post.description }}{% endif %}</p> - <div>{{ post.html | safe }}</div> - <div><a href="mailto:[email protected]">Reply via email →</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 %} |
