Commit a6ee2e0ca7

a6ee2e0ca79b83b7f6bd4a521777cffdaf2260f2

parent: 0b3783a396

Unsigned

cmc <hello@cleberg.net> · 2026-08-12 01:34 UTC

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.

Layout: unified · split

.build.yml.bkp deleted −44
@@ -1,44 +0,0 @@
1image: ubuntu/lts
2
3packages:
4 - emacs-nox
5 - rsync
6 - openssh-client
7 - git
8 - curl
9 - tar
10
11sources:
12 - https://git.sr.ht/~ccleberg/cleberg.net
13
14secrets:
15 - 303fbd30-2970-4d18-b88b-0f6abf822ea5
16 - e8b41b71-c4b2-4bd6-8159-25ae1eb7d282
17
18environment:
19 ENV: prod
20 BUILD: "true"
21
22tasks:
23 - setup: |
24 curl -LsSf https://astral.sh/uv/install.sh | sh
25 mkdir -p ~/.local/bin
26 curl -sSfL https://github.com/tdewolff/minify/releases/latest/download/minify_linux_amd64.tar.gz \
27 | tar -xz -C ~/.local/bin minify
28 mkdir -p ~/.config/emacs/.local/straight/repos
29 git clone --depth 1 https://github.com/emacsorphanage/htmlize \
30 ~/.config/emacs/.local/straight/repos/htmlize
31 git clone --depth 1 https://github.com/emacs-love/weblorg \
32 ~/.config/emacs/.local/straight/repos/weblorg
33 git clone --depth 1 https://github.com/emacs-love/templatel \
34 ~/.config/emacs/.local/straight/repos/templatel
35 - build: |
36 source ~/.local/bin/env
37 cd cleberg.net
38 uv run python3 -u build.py
39 - deploy: |
40 set -euo pipefail
41 cd cleberg.net
42 chmod 600 ~/.ssh/id_rsa
43 chmod 600 ~/.ssh/config
44 rsync -r --delete-before .build/ homelab:/var/www/cleberg.net/
.gitignore +1 −1
@@ -1,4 +1,5 @@
1.build/ 1.build/
2.build-dev/
2.DS_Store 3.DS_Store
3.gitlab-ci-local/ 4.gitlab-ci-local/
4.ruff_cache/ 5.ruff_cache/
@@ -6,5 +7,4 @@
6public/ 7public/
7readme.html 8readme.html
8venv/ 9venv/
9theme/static/styles.min.css
10__pycache__/ 10__pycache__/
README.md +55 −45
@@ -3,23 +3,26 @@
3This repository holds the files for [cleberg.net](https://cleberg.net), 3This repository holds the files for [cleberg.net](https://cleberg.net),
4a static-site with blog posts, personal links, and more. 4a static-site with blog posts, personal links, and more.
5 5
6This site uses [weblorg](https://github.com/emacs-love/weblorg) to build 6This site uses [orgo](https://github.com/krazywarez/orgo) to build the
7the static site, which relies on 7static site.
8[emacs](https://www.gnu.org/software/emacs/).
9 8
10## Site Structure 9## Site Structure
11 10
12I write content pages (e.g., blog posts) in org-mode and templates in 11I write content pages (e.g., blog posts) in org-mode and templates in
13HTML. I use Weblorg to build these files into a static site, and then 12HTML. orgo builds these files into a static site, and then I deploy
14deploy them to a web server. 13them to a web server.
15 14
16The main site components are: 15The main site components are:
17 16
18- Org source files containing content, including blog posts and pages. 17- Org source files in `content/`, containing blog posts and pages.
19- A configuration file (`publish.el`) that specifies publishing 18 That directory is the site's URL root: `content/blog/post.org`
20 parameters like base URL, output directories, and export options. 19 publishes at `/blog/post.html`.
20- A configuration file (`content/orgo.toml`) that specifies the base
21 URL, navigation, templates, and the generated pages (home, blog and
22 garden indexes, tags, and the RSS feed).
23- HTML templates in `content/templates/`.
21- Assets such as images and style sheets, located in designated 24- Assets such as images and style sheets, located in designated
22 subdirectories. 25 subdirectories. `theme/static/` is published at `/`.
23- Utility scripts (e.g., `build.py`) to facilitate building and 26- Utility scripts (e.g., `build.py`) to facilitate building and
24 deployment. 27 deployment.
25 28
@@ -27,24 +30,25 @@ The main site components are:
27 30
28The publishing system depends on: 31The publishing system depends on:
29 32
30- Emacs text editor with Org-Mode. 33- [orgo](https://github.com/krazywarez/orgo), the static site
31- The weblorg package, available at 34 generator, installed with `cargo install --git
32 [emacs-love/weblorg](https://github.com/emacs-love/weblorg), which 35 https://github.com/krazywarez/orgo`.
33 provides advanced Org publishing functionality and theming support. 36- `rsync` for deployment.
37- [uv](https://github.com/astral-sh/uv) to run `build.py`.
34 38
35## Configuration 39## Configuration
36 40
37You can customize site settings can within the `publish.el` file. This 41You can customize site settings within the `content/orgo.toml` file.
38file establishes key variables such as: 42This file establishes key variables such as:
39 43
40- The base URL for links. 44- The base URL for links.
41- Output directories. 45- Which pages appear in the navigation, and in what order.
42- Publishing rules to define which files to convert and how. 46- Per-directory template rules.
43- Theme settings managed by weblorg. 47- The collections that generate the indexes, tag pages, and feed.
44 48
45Users intending to modify site parameters should review and edit this 49Users intending to modify site parameters should review and edit this
46file accordingly. The weblorg documentation contains extensive details 50file accordingly. The orgo documentation contains extensive details on
47on configuration options and expected formats. 51configuration options and expected formats.
48 52
49## Setup Instructions 53## Setup Instructions
50 54
@@ -62,48 +66,54 @@ For users employing Doom Emacs, open any repository Org file using
62 66
63## Building and Publishing the Site 67## Building and Publishing the Site
64 68
65The publishing process involves invoking Emacs with the `publish.el` 69The `build.py` script wraps the build: it runs orgo, and then either
66script, which performs the export of Org documents to HTML output. 70deploys the result or serves it locally.
67 71
68Configure the environment variable `ENV` as follows: 72Two environment variables control what it does, and both default to
73off:
69 74
70- If you set `ENV` to `prod`, the script uses production base URL 75- `BUILD=true` performs the build.
71 settings as defined in `publish.el`. 76- `DEPLOY=true` deploys in production, or starts a development server
72- If you do not set `ENV` or set it differently, the script defaults to 77 on port 8000 otherwise.
73 development settings, typically using `localhost:8000` as the base
74 URL.
75 78
76Example commands to build the site: 79A third, `ENV`, selects the mode. Setting `ENV=prod` rewrites image
80URLs to be root-relative (see Deployment below) and skips the `ruff`
81pass; anything else builds for development.
77 82
78``` shell 83``` shell
79# Production build: 84# Production build:
80ENV=prod emacs --script publish.el 85ENV=prod BUILD=true uv run build.py
81 86
82# Development build: 87# Development build:
83emacs --script publish.el 88BUILD=true uv run build.py
89
90# Development build, then serve it on localhost:8000:
91BUILD=true DEPLOY=true uv run build.py
84``` 92```
85 93
86Generated site files reside in the designated output directory, ready 94Generated site files reside in `.build/` for production and
87for deployment. You can deploy the resulting static site files via 95`.build-dev/` for development, ready for deployment. You can deploy the
88standard file transfer protocols such as `scp` or SFTP. 96resulting static site files via standard file transfer protocols such as
97`scp` or SFTP.
89 98
90The `build.py` script automates the build process. You can execute this 99Builds are incremental: orgo keeps an `.orgo-cache.json` inside the
91script with or without the `ENV` variable to perform production or 100output directory and re-renders only what changed, so the directory is
92development builds respectively. 101left in place between runs. Delete it for a clean build. The two
102environments use separate directories because production rewrites image
103URLs in the output and development does not.
93 104
94``` shell 105orgo can also build and preview on its own, without `build.py`:
95# Production build script:
96ENV=prod uv run build.py
97 106
98# Development build script: 107``` shell
99uv run build.py 108orgo serve content -o /tmp/preview
100``` 109```
101 110
102## Deployment 111## Deployment
103 112
104Production builds emit root-relative URLs (`/styles.min.css`, 113Production builds rewrite image URLs to root-relative ones
105`/img/blog/...`) instead of absolute `https://` ones so the site renders 114(`/img/blog/...`) instead of absolute `https://img.cleberg.net/` ones so
106standalone on the onion service without fetching assets off-onion. 115the site renders standalone on the onion service without fetching assets
116off-onion. The stylesheet is already same-origin.
107 117
108This means the web server must serve the image store at `/img/` on the 118This means the web server must serve the image store at `/img/` on the
109`cleberg.net` vhost. The images live at `/var/www/img/` (their own host, 119`cleberg.net` vhost. The images live at `/var/www/img/` (their own host,
build.py +55 −330
@@ -1,23 +1,23 @@
1#!/usr/bin/env python3 1#!/usr/bin/env python3
2""" 2"""
3This script automates the process of building, testing, and deploying the website. 3This script automates the process of building and deploying the website.
4It handles tasks such as: 4It handles tasks such as:
5 5
6- Removing and recreating the build directory. 6- Running orgo to generate site content.
7- Minifying CSS assets. 7- Rewriting image URLs for the onion service (production only).
8- Running the Emacs publishing script to generate site content.
9- Updating the index.html file with the latest blog posts.
10- Optionally deploying the built site to a remote server. 8- Optionally deploying the built site to a remote server.
11- Starting a local development server for previewing changes. 9- Starting a local development server for previewing changes.
12 10
13Usage: 11Usage:
14 Set the environment variable ENV to 'prod' for production builds. 12 Set BUILD=true to build, DEPLOY=true to deploy or serve.
15 Run the script to perform the build process accordingly. 13 Set ENV=prod for production builds; anything else builds for development.
14
15 Builds are incremental. Production writes .build/ and development .build-dev/;
16 delete either one for a clean build.
16 17
17Dependencies: 18Dependencies:
18 - Python 3 19 - Python 3
19 - Emacs with the publish.el script 20 - orgo, the static site generator (reads content/orgo.toml)
20 - minify tool for CSS minification
21 - rsync for deployment 21 - rsync for deployment
22 22
23Author: 23Author:
@@ -26,202 +26,24 @@ Author:
26 26
27import os 27import os
28import re 28import re
29import shutil
30import subprocess 29import subprocess
31import sys 30import sys
32from datetime import datetime
33from html import escape
34from pathlib import Path 31from pathlib import Path
35 32
36SITE_TEMPLATE_VARS = { 33
37 "site_name": "cleberg.net", 34def run(cmd, error):
38 "site_owner": "Christian Cleberg <hello@cleberg.net>", 35 """Run cmd quietly, exiting with its stderr if it fails."""
39 "site_description": "Stillness amidst the chaos.", 36 result = subprocess.run(cmd, capture_output=True, text=True, check=False)
40} 37 if result.returncode != 0:
38 print(error, file=sys.stderr)
39 print(result.stderr, file=sys.stderr)
40 sys.exit(1)
41 41
42 42
43def run_ruff(): 43def run_ruff():
44 print("Running ruff...") 44 print("Running ruff...")
45 for cmd in [["ruff", "check", "--fix"], ["ruff", "format"]]: 45 for cmd in [["ruff", "check", "--fix"], ["ruff", "format"]]:
46 result = subprocess.run(cmd, capture_output=True, text=True, check=False) 46 run(cmd, f"ruff error ({' '.join(cmd)}):")
47 if result.returncode != 0:
48 print(f"ruff error ({' '.join(cmd)}):")
49 print(result.stderr, file=sys.stderr)
50 sys.exit(1)
51
52
53def render_base_template(main_html, subtitle="", title=None):
54 """
55 Render a small subset of the site's shared templates for Python-generated
56 pages that still need to follow the common chrome.
57 """
58 base_template = Path("theme/templates/base.html").read_text(encoding="utf-8")
59
60 if title is None:
61 title = SITE_TEMPLATE_VARS["site_name"]
62
63 rendered = base_template
64 rendered = rendered.replace(
65 "{% block subtitle %}{% endblock %}",
66 escape(subtitle),
67 )
68 rendered = rendered.replace(
69 '{% block title %}{{ site_name | default("cleberg.net") }}{% endblock %}',
70 escape(title),
71 )
72 rendered = rendered.replace(
73 '{% if site_owner is defined %}<meta name="author" content="{{ site_owner }}">{% endif %}',
74 f'<meta name="author" content="{escape(SITE_TEMPLATE_VARS["site_owner"])}">',
75 )
76 rendered = rendered.replace(
77 '{% if site_description is defined %}<meta name="description" content="{{ site_description }}">{% endif %}',
78 f'<meta name="description" content="{escape(SITE_TEMPLATE_VARS["site_description"])}">',
79 )
80 rendered = rendered.replace(
81 '{% if site_keywords is defined %}<meta name="keywords" content="{{ site_keywords }}">{% endif %}',
82 "",
83 )
84 rendered = rendered.replace("{% block meta %}{% endblock %}", "")
85 rendered = rendered.replace("{% block head %}", "")
86 rendered = rendered.replace("{% endblock %}", "", 1)
87 rendered = rendered.replace("{% block main %}{% endblock %}", main_html)
88
89 return rendered
90
91
92def render_tags_page_html(tags_html):
93 """
94 Render the tags page by using the shared tags/base templates instead of a
95 hand-authored standalone HTML document.
96 """
97 tags_template = Path("theme/templates/tags.html").read_text(encoding="utf-8")
98
99 main_html = tags_template
100 main_html = main_html.replace('{% extends "base.html" %}', "")
101 main_html = main_html.replace(
102 "{% block subtitle %}tags - {% endblock %}",
103 "",
104 )
105 main_html = main_html.replace("{% block main %}", "")
106 main_html = main_html.replace("{% endblock %}", "")
107 main_html = main_html.replace("<!-- BEGIN_TAGS -->\n<!-- END_TAGS -->", tags_html)
108
109 return render_base_template(main_html.strip(), subtitle="tags - ")
110
111
112def get_blog_posts(content_dir="./content/blog"):
113 """
114 Scan blog posts and return normalized metadata for non-draft entries.
115 """
116 posts = []
117
118 header_patterns = {
119 "title": re.compile(r"^#\+title:\s*(.+)$", re.IGNORECASE),
120 "date": re.compile(r"^#\+date:\s*[\[<](\d{4}-\d{2}-\d{2})"),
121 "slug": re.compile(r"^#\+slug:\s*(.+)$", re.IGNORECASE),
122 "tags": re.compile(r"^#\+filetags:\s*(.+)$", re.IGNORECASE),
123 "draft": re.compile(r"^#\+draft:\s*(.+)$", re.IGNORECASE),
124 }
125
126 for org_path in Path(content_dir).glob("*.org"):
127 title = None
128 date_str = None
129 slug = None
130 tags = []
131 is_draft = False
132
133 with org_path.open("r", encoding="utf-8") as f:
134 for line in f:
135 if title is None:
136 m = header_patterns["title"].match(line)
137 if m:
138 title = m.group(1).strip()
139 continue
140
141 if date_str is None:
142 m = header_patterns["date"].match(line)
143 if m:
144 # date_str is just YYYY-MM-DD
145 date_str = m.group(1)
146 continue
147
148 if slug is None:
149 m = header_patterns["slug"].match(line)
150 if m:
151 slug = m.group(1).strip()
152 continue
153
154 if not tags:
155 m = header_patterns["tags"].match(line)
156 if m:
157 raw = m.group(1).strip().strip(":")
158 tags = [t.strip() for t in raw.split(":") if t.strip()]
159 continue
160
161 m = header_patterns["draft"].match(line)
162 if m:
163 draft_value = m.group(1).strip().lower()
164 if draft_value != "nil":
165 is_draft = True
166 break
167 continue
168
169 # Stop scanning once we have all required fields
170 if title and date_str and slug:
171 break
172
173 if is_draft:
174 continue
175
176 if title and date_str and slug:
177 try:
178 date_obj = datetime.strptime(date_str, "%Y-%m-%d")
179 date_full = date_obj.strftime("%Y-%m-%d")
180 except ValueError:
181 # Skip files with invalid date format
182 continue
183
184 posts.append(
185 {
186 "title": title,
187 "date_str": date_str,
188 "date_obj": date_obj,
189 "date_full": date_full,
190 "slug": slug,
191 "tags": tags,
192 }
193 )
194
195 posts.sort(key=lambda x: x["date_obj"], reverse=True)
196 return posts
197
198
199def prompt(prompt_text):
200 try:
201 return input(prompt_text).strip()
202 except EOFError:
203 return ""
204
205
206def remove_build_directory(build_dir):
207 if build_dir.exists():
208 print(f"Removing previous build directory: {build_dir}/")
209 shutil.rmtree(build_dir)
210 build_dir.mkdir(parents=True, exist_ok=True)
211
212
213def minify_css(src_css, dest_css):
214 print(f"Minifying CSS: {src_css} → {dest_css}")
215 result = subprocess.run(
216 ["minify", "-o", str(dest_css), str(src_css)],
217 capture_output=True,
218 text=True,
219 check=False,
220 )
221 if result.returncode != 0:
222 print("Error during CSS minification:")
223 print(result.stderr, file=sys.stderr)
224 sys.exit(1)
225 47
226 48
227def rewrite_img_urls(build_dir=".build"): 49def rewrite_img_urls(build_dir=".build"):
@@ -249,22 +71,26 @@ def rewrite_img_urls(build_dir=".build"):
249 print(f"Rewrote {count} img.cleberg.net references to /img/") 71 print(f"Rewrote {count} img.cleberg.net references to /img/")
250 72
251 73
252def run_orgo_build(dev_mode=True): 74def run_orgo_build(build_dir):
253 """ 75 """
254 Build the site with orgo. 76 Build the site with orgo.
255 77
256 Replaces the weblorg/Emacs publish. orgo reads content/orgo.toml, so the routes, 78 orgo reads content/orgo.toml, which holds the routes, templates and collections: the
257 templates and collections that used to live in publish.el live there now. Four of the 79 blog index groups itself by year, the tags page is a collection, the recent-posts
258 steps this script used to perform afterwards are gone with it: the blog index groups 80 list comes from the blog collection, and the sitemap is written once base_url is set.
259 itself by year, the tags page is a collection, the recent-posts list is generated from 81
260 the blog collection, and the sitemap is written by orgo once base_url is set. 82 The output directory is left in place between builds rather than wiped, because the
83 .orgo-cache.json inside it is what makes a build incremental: orgo re-renders only
84 the pages whose content, config or templates changed, re-emits any page whose output
85 is missing, and deletes the outputs of pages that have since been removed. Wiping the
86 directory would throw that away and force a full render every time. For a clean build
87 from scratch, delete the output directory by hand.
261 88
262 What is left around it is what orgo does not do: minifying CSS, copying the org 89 What is left around it is what orgo does not do: rewriting image URLs for the onion.
263 sources for readers who want them, and rewriting image URLs for the onion.
264 """ 90 """
265 print("Building with orgo...") 91 print("Building with orgo...")
266 result = subprocess.run( 92 result = subprocess.run(
267 ["orgo", "build", "content", "-o", ".build", "--strict"], 93 ["orgo", "build", "content", "-o", str(build_dir), "--strict"],
268 stdout=subprocess.PIPE, 94 stdout=subprocess.PIPE,
269 stderr=subprocess.STDOUT, 95 stderr=subprocess.STDOUT,
270 text=True, 96 text=True,
@@ -276,99 +102,10 @@ def run_orgo_build(dev_mode=True):
276 sys.exit(1) 102 sys.exit(1)
277 103
278 104
279def copy_org_sources(content_dir="./content", build_dir="./.build/org"):
280 print(f"Copying org sources: {content_dir} → {build_dir}")
281 if os.path.exists(build_dir):
282 shutil.rmtree(build_dir)
283
284 slug_pattern = re.compile(r"^#\+slug:\s*(.+)$", re.IGNORECASE)
285
286 for src_path in Path(content_dir).rglob("*.org"):
287 rel_dir = src_path.parent.relative_to(content_dir)
288 dest_dir = Path(build_dir) / rel_dir
289 dest_dir.mkdir(parents=True, exist_ok=True)
290
291 # Try to extract slug from file headers
292 slug = None
293 with src_path.open("r", encoding="utf-8") as f:
294 for line in f:
295 m = slug_pattern.match(line)
296 if m:
297 slug = m.group(1).strip()
298 break
299
300 dest_name = f"{slug}.org" if slug else src_path.name
301 shutil.copy2(src_path, dest_dir / dest_name)
302 if slug:
303 print(f" {src_path.name} → {dest_name}")
304
305
306def get_tags_html(content_dir="./content/blog"):
307 """
308 Build the tag index HTML snippet for the rendered tags template.
309 """
310 preferred_tag_order = [
311 "audit",
312 "emacs",
313 "development",
314 "ios",
315 "linux",
316 "personal",
317 "privacy",
318 "security",
319 "self-hosting",
320 "web",
321 ]
322
323 tag_map = {}
324
325 for post in get_blog_posts(content_dir):
326 for tag in post["tags"]:
327 tag_map.setdefault(tag, []).append(
328 {
329 "title": post["title"],
330 "slug": post["slug"],
331 "date_obj": post["date_obj"],
332 "date_str": post["date_str"],
333 }
334 )
335
336 ordered_tags = [tag for tag in preferred_tag_order if tag in tag_map]
337 ordered_tags.extend(
338 sorted(tag for tag in tag_map if tag not in preferred_tag_order)
339 )
340
341 for tag in ordered_tags:
342 tag_map[tag].sort(key=lambda x: x["date_obj"], reverse=True)
343
344 toc_items = "".join(
345 f'<li><a href="#{tag}">{tag}</a> <span class="tag-count">({len(tag_map[tag])})</span></li>'
346 for tag in ordered_tags
347 )
348
349 sections = []
350 for tag in ordered_tags:
351 posts = tag_map[tag]
352 items = "\n".join(
353 f'<li class="post-list-item">'
354 f'<time datetime="{p["date_str"]}">{p["date_str"]}</time>'
355 f'<a href="/blog/{p["slug"]}.html">{p["title"]}</a>'
356 f"</li>"
357 for p in posts
358 )
359 sections.append(
360 f'<h2 id="{tag}">{tag}</h2>\n<ul class="post-list">\n{items}\n</ul>'
361 )
362
363 return f'<ul class="tag-toc">{toc_items}</ul>\n' + "".join(
364 f"<section>{section}</section>" for section in sections
365 )
366
367
368def deploy_to_server(build_dir, server): 105def deploy_to_server(build_dir, server):
369 remote_path = f"{server}:/var/www/cleberg.net/" 106 remote_path = f"{server}:/var/www/cleberg.net/"
370 print(f"Deploying .build/ → {remote_path}") 107 print(f"Deploying {build_dir}/ → {remote_path}")
371 result = subprocess.run( 108 run(
372 # The build cache lives in the output directory because it describes it, but it 109 # The build cache lives in the output directory because it describes it, but it
373 # is not part of the site. Excluding it also stops --delete removing it locally. 110 # is not part of the site. Excluding it also stops --delete removing it locally.
374 [ 111 [
@@ -380,14 +117,8 @@ def deploy_to_server(build_dir, server):
380 f"{build_dir}/", 117 f"{build_dir}/",
381 remote_path, 118 remote_path,
382 ], 119 ],
383 capture_output=True, 120 "Error during rsync deployment:",
384 text=True,
385 check=False,
386 ) 121 )
387 if result.returncode != 0:
388 print("Error during rsync deployment:")
389 print(result.stderr, file=sys.stderr)
390 sys.exit(1)
391 122
392 123
393def start_dev_server(build_dir): 124def start_dev_server(build_dir):
@@ -404,38 +135,32 @@ def start_dev_server(build_dir):
404 135
405 136
406def main(): 137def main():
407 env = os.environ.get("ENV", "").casefold() 138 prod = os.environ.get("ENV", "").casefold() == "prod"
408 if env != "prod": 139 if not prod:
409 run_ruff() 140 run_ruff()
410 141
411 build_dir = Path(".build") 142 # One output directory per environment, because the two differ after orgo has run:
412 theme_dir = Path("theme/static") 143 # production rewrites image URLs in place and development does not. Sharing a
413 css_src = theme_dir / "styles.css" 144 # directory would let an incremental build reuse a page rendered for the other one —
414 css_min = theme_dir / "styles.min.css" 145 # a dev preview showing /img/ paths that only resolve on the server. Production keeps
415 146 # .build/ because that is the directory the deploy and the CI manifest name.
416 build = os.environ.get("BUILD", "").casefold() == "true" 147 build_dir = Path(".build" if prod else ".build-dev")
417 deploy = os.environ.get("DEPLOY", "").casefold() == "true" 148
418 149 print(f"Environment: {'Production' if prod else 'Development'}")
419 if env == "prod": 150
420 print("Environment: Production") 151 if os.environ.get("BUILD", "").casefold() == "true":
421 if build: 152 run_orgo_build(build_dir)
422 remove_build_directory(build_dir) 153 # The onion needs same-origin images; dev previews keep the absolute URLs.
423 minify_css(css_src, css_min) 154 # Runs over every page, not just the re-rendered ones, so a page carried over
424 run_orgo_build(dev_mode=False) 155 # from an earlier build is rewritten too.
425 copy_org_sources() 156 if prod:
426 rewrite_img_urls(build_dir) 157 rewrite_img_urls(build_dir)
427 if deploy: 158
159 if os.environ.get("DEPLOY", "").casefold() == "true":
160 if prod:
428 print("Deploying to production...") 161 print("Deploying to production...")
429 deploy_to_server(build_dir, "homelab") 162 deploy_to_server(build_dir, "homelab")
430 return 163 else:
431 else:
432 print("Environment: Development")
433 if build:
434 remove_build_directory(build_dir)
435 minify_css(css_src, css_min)
436 run_orgo_build(dev_mode=True)
437 copy_org_sources()
438 if deploy:
439 start_dev_server(build_dir) 164 start_dev_server(build_dir)
440 165
441 166
content/orgo.toml +4 −12
@@ -5,9 +5,6 @@
5# 5#
6# orgo serve content -o /tmp/preview # write and preview 6# orgo serve content -o /tmp/preview # write and preview
7# orgo build content -o _site --strict # build for real, failing on broken links 7# orgo build content -o _site --strict # build for real, failing on broken links
8#
9# This sits alongside the existing weblorg setup (publish.el, theme/) and changes nothing
10# about it. Delete this file and content/templates/ to remove orgo entirely.
11 8
12[site] 9[site]
13title = "cleberg.net" 10title = "cleberg.net"
@@ -21,8 +18,7 @@ base_url = "https://cleberg.net"
21[nav] 18[nav]
22# Explicit, because the pages that belong in the nav are not all top-level: /blog/ and 19# Explicit, because the pages that belong in the nav are not all top-level: /blog/ and
23# /garden/ are generated below, and salary/ is a section index one level down. index.org 20# /garden/ are generated below, and salary/ is a section index one level down. index.org
24# is deliberately absent — the layout writes the Home link itself, exactly as weblorg's 21# is deliberately absent — the layout writes the Home link itself.
25# base.html does.
26# 22#
27# Generated pages are named by their output path, authored ones by their source. Listing 23# Generated pages are named by their output path, authored ones by their source. Listing
28# them together is what puts Salary last: a collection with `nav = true` that is not 24# them together is what puts Salary last: a collection with `nav = true` that is not
@@ -32,15 +28,13 @@ mode = "explicit"
32pages = ["blog/index.html", "garden/index.html", "salary/index.org"] 28pages = ["blog/index.html", "garden/index.html", "salary/index.org"]
33 29
34[build] 30[build]
35# theme/static/ is weblorg's static root, published at /. orgo reads it from here 31# theme/static/ is the site's static root, published at /. orgo reads it from here
36# rather than needing its files copied next to the writing — one copy, not two. 32# rather than needing its files copied next to the writing — one copy, not two.
37assets = ["../theme/static"] 33assets = ["../theme/static"]
38 34
39[templates] 35[templates]
40dir = "templates" 36dir = "templates"
41 37
42# Blog posts render through post.html — the shared layout plus a reply footer — the way
43# weblorg routes them through theme/templates/post.html. Everything else uses base.html.
44# Blog posts render through post.html, which adds the reply snippet. Nothing else does: 38# Blog posts render through post.html, which adds the reply snippet. Nothing else does:
45# the live site also invites replies on garden notes and /now/, and that is a choice 39# the live site also invites replies on garden notes and /now/, and that is a choice
46# rather than something a directory should decide. Any page opts in for itself with 40# rather than something a directory should decide. Any page opts in for itself with
@@ -63,13 +57,11 @@ toc = true
63section_numbers = true 57section_numbers = true
64 58
65# --- Generated pages ------------------------------------------------------------- 59# --- Generated pages -------------------------------------------------------------
66# These have no source .org file. weblorg produces the equivalents today through its 60# These have no source .org file.
67# blog-index, garden-index and tags routes.
68 61
69# The home page. Generated rather than authored, because it is a hand-written 62# The home page. Generated rather than authored, because it is a hand-written
70# introduction *plus* the most recent posts — and the post list is not something to 63# introduction *plus* the most recent posts — and the post list is not something to
71# maintain by hand. The prose lives in templates/home.html, which is where weblorg keeps 64# maintain by hand. The prose lives in templates/home.html.
72# it today too.
73# 65#
74# content/index.org was deleted to make room: it held only #+title and #+description, 66# content/index.org was deleted to make room: it held only #+title and #+description,
75# both of which are now [site] settings above. Restore it any time with 67# both of which are now [site] settings above. Restore it any time with
content/templates/base.html +6 −6
@@ -1,9 +1,9 @@
1{# Page layout for orgo, mirroring theme/templates/base.html. 1{# Page layout for the site.
2 2
3 Styling comes from theme/static/styles.css, published by `[build] assets` in 3 Styling comes from theme/static/styles.css, published by `[build] assets` in
4 orgo.toml — the same directory weblorg serves at /, read in place rather than 4 orgo.toml and read in place rather than copied. It is served as authored: at ~1KB,
5 copied. weblorg links the minified styles.min.css instead; that file is gitignored 5 minifying it saved 52 bytes over the wire once compressed, which did not pay for a
6 build output and the rules are the same, so this links the tracked one. 6 build step and a toolchain dependency.
7 7
8 Available here: page (.title .url .source .date .date_iso .tags .excerpt .toc 8 Available here: page (.title .url .source .date .date_iso .tags .excerpt .toc
9 .keywords), site, nav, root, stylesheet. See the orgo guide on Templates. #} 9 .keywords), site, nav, root, stylesheet. See the orgo guide on Templates. #}
@@ -45,8 +45,8 @@
45<link rel="alternate" type="application/rss+xml" title="{{ site.title }}" href="{{ root }}feed.xml"> 45<link rel="alternate" type="application/rss+xml" title="{{ site.title }}" href="{{ root }}feed.xml">
46<link rel="stylesheet" href="{{ root }}styles.css" type="text/css"> 46<link rel="stylesheet" href="{{ root }}styles.css" type="text/css">
47<link rel="icon" href="data:,"> 47<link rel="icon" href="data:,">
48{#- rel="me" identity links. weblorg's base.html writes the last one as <a>, which a 48{#- rel="me" identity links, spelled as <link> rather than <a>: a browser hoists a
49 browser hoists out of <head>; <link> is the spelling that stays where it is put. #} 49 stray <a> out of <head>, while <link> stays where it is put. #}
50<link href="https://github.com/ccleberg" rel="me"> 50<link href="https://github.com/ccleberg" rel="me">
51<link href="https://gitlab.com/ccleberg" rel="me"> 51<link href="https://gitlab.com/ccleberg" rel="me">
52<link href="mailto:hello@cleberg.net" rel="me"> 52<link href="mailto:hello@cleberg.net" rel="me">
content/templates/blog.html +2 −2
@@ -1,5 +1,5 @@
1{# The blog index at /blog/, mirroring theme/templates/blog.html. 1{# The blog index at /blog/.
2 Separate from list.html because the live page carries its own prose. #} 2 Separate from list.html because the page carries its own prose. #}
3{% extends "base.html" %} 3{% extends "base.html" %}
4 4
5{#- Group the list under year headings, as the live site does. Set to false for one flat 5{#- Group the list under year headings, as the live site does. Set to false for one flat
content/templates/feed.xml +4 −5
@@ -1,10 +1,9 @@
1<?xml version="1.0" encoding="UTF-8" ?> 1<?xml version="1.0" encoding="UTF-8" ?>
2{#- The RSS feed at /feed.xml, mirroring theme/templates/feed.xml. A feed is read away 2{#- The RSS feed at /feed.xml. A feed is read away from the site that served it, so
3 from the site that served it, so every URL here is absolute — which is what 3 every URL here is absolute — which is what `| absolute` needs [site] base_url for.
4 `| absolute` needs [site] base_url for.
5 4
6 <description> carries each post's full rendered HTML, as weblorg's feed does — the 5 <description> carries each post's full rendered HTML — the collection sets
7 collection sets `include_content` to ask for it. #} 6 `include_content` to ask for it. #}
8<rss version="2.0" 7<rss version="2.0"
9 xmlns:content="http://purl.org/rss/1.0/modules/content/" 8 xmlns:content="http://purl.org/rss/1.0/modules/content/"
10 xmlns:dc="http://purl.org/dc/elements/1.1/" 9 xmlns:dc="http://purl.org/dc/elements/1.1/"
content/templates/garden.html +1 −1
@@ -1,4 +1,4 @@
1{# The garden index at /garden/, mirroring theme/templates/garden.html. 1{# The garden index at /garden/.
2 Notes are listed by title and description rather than by date. #} 2 Notes are listed by title and description rather than by date. #}
3{% extends "base.html" %} 3{% extends "base.html" %}
4{% block main %} 4{% block main %}
content/templates/home.html +2 −3
@@ -1,9 +1,8 @@
1{# The home page: authored prose plus the most recent posts. 1{# The home page: authored prose plus the most recent posts.
2 2
3 A generated page, so it has no source .org file — `pages` holds the blog collection's 3 A generated page, so it has no source .org file — `pages` holds the blog collection's
4 entries, newest first, and the prose lives here. This mirrors what weblorg's 4 entries, newest first, and the prose lives here. The <title> is bare on purpose: the
5 theme/templates/index.html does today, including its bare <title>: the home page is 5 home page is "cleberg.net" and not "cleberg.net - cleberg.net". #}
6 "cleberg.net" and not "cleberg.net - cleberg.net". #}
7{% extends "base.html" %} 6{% extends "base.html" %}
8{% block subtitle %}{% endblock %} 7{% block subtitle %}{% endblock %}
9{% block main %} 8{% block main %}
content/templates/tags.html +4 −5
@@ -1,11 +1,10 @@
1{# The tag index at /tags/. Receives `groups` — every tag with its count — rather than 1{# The tag index at /tags/. Receives `groups` — every tag with its count — rather than
2 `pages`, because it lists tags and not posts. 2 `pages`, because it lists tags and not posts.
3 3
4 This deliberately differs in shape from the live weblorg page, which is one page with 4 Each tag is its own page, so a tag has a URL worth linking to and the index stays
5 every post inlined under an `#anchor` per tag. Here each tag is its own page, so a tag 5 short. The `id` on each entry keeps older `/tags/#audit` links landing in the right
6 has a URL worth linking to and the index stays short. The `id` on each entry keeps the 6 place — they scroll to the tag and its link, rather than dumping the reader at the
7 old `/tags/#audit` links landing in the right place — they scroll to the tag and its 7 top of the index. #}
8 link, rather than 404ing or dumping the reader at the top of a long page. #}
9{% extends "base.html" %} 8{% extends "base.html" %}
10{% block main %} 9{% block main %}
11<h1>{{ page.title }}</h1> 10<h1>{{ page.title }}</h1>
content/uses/index.org +1 −1
@@ -24,7 +24,7 @@ threat model justification.
24| Operating System | [[https://ubuntu.com][Ubuntu]] + [[https://www.apple.com/os/macos/][macOS]] | Ubuntu: telemetry off, LTS, stable. macOS: workstation only. | 24| Operating System | [[https://ubuntu.com][Ubuntu]] + [[https://www.apple.com/os/macos/][macOS]] | Ubuntu: telemetry off, LTS, stable. macOS: workstation only. |
25| Web Server | [[https://nginx.org][Nginx]] + [[https://community.torproject.org/onion-services/setup/][Tor]] | Static file serving. Tor layer for censored-network access. | 25| Web Server | [[https://nginx.org][Nginx]] + [[https://community.torproject.org/onion-services/setup/][Tor]] | Static file serving. Tor layer for censored-network access. |
26| SSL | [[https://certbot.eff.org][Certbot]] | Free automated TLS. No commercial CA dependency. Ensures Cloudflare can't view unencrypted traffic. | 26| SSL | [[https://certbot.eff.org][Certbot]] | Free automated TLS. No commercial CA dependency. Ensures Cloudflare can't view unencrypted traffic. |
27| 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. | 27| 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. |
28| Terminal | [[https://iterm2.com/][iTerm2]] | Functional. Inherited from macOS. | 28| Terminal | [[https://iterm2.com/][iTerm2]] | Functional. Inherited from macOS. |
29| Shell | [[https://www.zsh.org/][Zsh]] | Portable, POSIX-adjacent, available on every target OS. | 29| Shell | [[https://www.zsh.org/][Zsh]] | Portable, POSIX-adjacent, available on every target OS. |
30| Editor | [[https://github.com/doomemacs/doomemacs][Doom Emacs]] | Editor and markup are the same tool. No proprietary format. | 30| Editor | [[https://github.com/doomemacs/doomemacs][Doom Emacs]] | Editor and markup are the same tool. No proprietary format. |
publish.el deleted −128
@@ -1,128 +0,0 @@
1;;; -*- lexical-binding: t -*-
2(setq debug-on-error t)
3
4;; Allow for macOS (dev machine) & Linux (GitHub Actions) execution
5(defvar site-lisp-base
6 (expand-file-name "~/.config/emacs/.local/straight/repos"))
7
8;; Explicitly load packages
9(add-to-list 'load-path (expand-file-name "htmlize" site-lisp-base))
10(add-to-list 'load-path (expand-file-name "weblorg" site-lisp-base))
11(add-to-list 'load-path (expand-file-name "templatel" site-lisp-base))
12
13(require 'htmlize)
14(require 'templatel)
15(require 'weblorg)
16
17;; Set default URL for Weblorg
18;; Only works if environment variable ENV=prod
19(if (string-equal-ignore-case (or (getenv "ENV") "") "prod")
20 (setq weblorg-default-url "https://cleberg.net"))
21
22;; Define site metadata
23(weblorg-site
24 :theme nil
25 :template-vars '(("site_name" . "cleberg.net")
26 ("site_owner" . "Christian Cleberg <hello@cleberg.net>")
27 ("site_description" . "Stillness amidst the chaos.")))
28
29;; Define routes for rendering content
30
31;; Index page route
32(weblorg-route
33 :name "index"
34 :input-pattern "content/*.org"
35 :template "index.html"
36 :output ".build/index.html"
37 :url "/")
38
39;; Blog post route
40(weblorg-route
41 :name "blog"
42 :input-pattern "content/blog/*.org"
43 :template "post.html"
44 :output ".build/blog/{{ slug }}.html"
45 :url "/blog/{{ slug }}.html")
46
47;; Blog index page route
48(weblorg-route
49 :name "blog-index"
50 :input-pattern "content/blog/*.org"
51 :input-aggregate #'weblorg-input-aggregate-all-desc
52 :template "blog.html"
53 :output ".build/blog/index.html"
54 :url "/blog/")
55
56;; Garden post route
57(weblorg-route
58 :name "garden"
59 :input-pattern "content/garden/*.org"
60 :template "post.html"
61 :output ".build/garden/{{ slug }}.html"
62 :url "/garden/{{ slug }}.html")
63
64;; Garden index page route
65(weblorg-route
66 :name "garden-index"
67 :input-pattern "content/garden/*.org"
68 :input-aggregate #'weblorg-input-aggregate-all-desc
69 :template "garden.html"
70 :output ".build/garden/index.html"
71 :url "/garden/")
72
73;; Page post route
74(weblorg-route
75 :name "pages"
76 :input-pattern "content/*.org"
77 :template "page.html"
78 :output ".build/{{ slug }}.html"
79 :url "/{{ slug }}.html")
80
81;; Salary page route
82(weblorg-route
83 :name "salary"
84 :input-pattern "content/salary/*.org"
85 :template "page.html"
86 :output ".build/salary/{{ slug }}.html"
87 :url "/salary/{{ slug }}.html")
88
89;; Now page route
90(weblorg-route
91 :name "now"
92 :input-pattern "content/now/*.org"
93 :template "post.html"
94 :output ".build/now/{{ slug }}.html"
95 :url "/now/{{ slug }}.html")
96
97;; Uses page route
98(weblorg-route
99 :name "uses"
100 :input-pattern "content/uses/*.org"
101 :template "page.html"
102 :output ".build/uses/{{ slug }}.html"
103 :url "/uses/{{ slug }}.html")
104
105;; Tips page route
106(weblorg-route
107 :name "tips"
108 :input-pattern "content/tips/*.org"
109 :template "page.html"
110 :output ".build/tips/{{ slug }}.html"
111 :url "/tips/{{ slug }}.html")
112
113;; RSS feed route
114(weblorg-route
115 :name "rss"
116 :input-pattern "content/blog/*.org"
117 :input-aggregate #'weblorg-input-aggregate-all-desc
118 :template "feed.xml"
119 :output ".build/feed.xml"
120 :url "/feed.xml")
121
122;; Copy static assets and output to .build directory
123(weblorg-copy-static
124 :output ".build/{{ file }}"
125 :url "/{{ file }}")
126
127;; Export all content using Weblorg engine
128(weblorg-export)
theme/templates/base.html deleted −46
@@ -1,46 +0,0 @@
1<!doctype html>
2<html lang="en-us">
3<head>
4 {% block head %}
5 <meta charset="utf-8">
6 <title>{% block subtitle %}{% endblock %}{% block title %}{{ site_name | default("cleberg.net") }}{% endblock %}</title>
7 <meta name="viewport" content="width=device-width, initial-scale=1">
8 {% if site_owner is defined %}<meta name="author" content="{{ site_owner }}">{% endif %}
9 {% if site_description is defined %}<meta name="description" content="{{ site_description }}">{% endif %}
10 {% if site_keywords is defined %}<meta name="keywords" content="{{ site_keywords }}">{% endif %}
11 <link rel="alternate" type="application/rss+xml" title="cleberg.net" href="/feed.xml">
12 <link rel="stylesheet" href="/styles.min.css" type="text/css">
13 <link rel="icon" href="data:,">
14 {% block meta %}{% endblock %}
15 {% endblock %}
16 <link href="https://github.com/ccleberg" rel="me">
17 <link href="https://gitlab.com/ccleberg" rel="me">
18 <link href="mailto:hello@cleberg.net" rel="me">
19 <a href="https://c.im/@cmc" rel="me">
20</head>
21<body>
22 <a href="#main-content" class="skip-link">Skip to content</a>
23
24 <nav aria-label="Site Navigation">
25 <ul>
26 <li><a href="/">Home</a></li>
27 <li><a href="/blog/">Blog</a></li>
28 <li><a href="/garden/">Garden</a></li>
29 <li><a href="/salary/">Salary</a></li>
30 </ul>
31 </nav>
32
33 <main id="main-content">
34 {% block main %}{% endblock %}
35 </main>
36
37 <footer>
38 <a href="https://git.krz.sh/cmc/cleberg.net.git/">Source</a> &middot;
39 <a href="/feed.xml">RSS</a> &middot;
40 <a href="http://paske4urhs6nttrtlkuwa5cowum3fjkc6yv6kl4ncx3mjxcd77764nqd.onion/">Onion</a> &middot;
41 <a href="/tips/">Tips</a> &middot;
42 <a href="https://iheartrss.com/">I &hearts; RSS</a>
43 <p>[ <a href="https://krz.sh">krz</a> &middot; <a href="https://audit-labs.dev">audit labs</a> ]</p>
44 </footer>
45</body>
46</html>
theme/templates/blog.html deleted −13
@@ -1,13 +0,0 @@
1{% extends "base.html" %} {% block subtitle %}blog - {% endblock %} {% block main %}
2<h1>Blog</h1>
3<p>Use <code>⌘ F</code> / <code>Ctrl F</code> to search &middot; <a href="/feed.xml">RSS Feed</a></p>
4<p>Want to browse by topic instead? Head over to the <a href="/tags/">tags</a> page.</p>
5<ul class="post-list">
6 {% for post in posts %}
7 <li class="post-list-item">
8 <time datetime='{{ post.date | strftime("%Y-%m-%d") }}'>{{ post.date|strftime("%Y-%m-%d") }}</time>
9 <a href='{{ url_for("blog", slug=post.slug) }}'>{{ post.title }}</a>
10 </li>
11 {% endfor %}
12</ul>
13{% endblock %}
theme/templates/feed.xml deleted −37
@@ -1,37 +0,0 @@
1<?xml version="1.0" encoding="UTF-8" ?>
2<rss version="2.0"
3 xmlns:content="http://purl.org/rss/1.0/modules/content/"
4 xmlns:wfw="http://wellformedweb.org/CommentAPI/"
5 xmlns:dc="http://purl.org/dc/elements/1.1/"
6 xmlns:atom="http://www.w3.org/2005/Atom"
7 xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
8 xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
9 xmlns:georss="http://www.georss.org/georss"
10 xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
11 xmlns:media="http://search.yahoo.com/mrss/">
12 <channel>
13 <title>{{ site_name | default("cleberg.net") }}</title>
14 <atom:link
15 href="{{ url_for("rss") }}"
16 rel="self" type="application/rss+xml" />
17 <link>{{ url_for("index") }}</link>
18 <description><![CDATA[]]></description>
19 <language>en</language>
20 <pubDate>{{ posts | first | getattr("date") | strftime("%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
21 <lastBuildDate>{{ now() | strftime("%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate>
22 <generator>{{ meta.generator }}</generator>
23 <webMaster>{{ site_owner | default("hello@cleberg.net") }}</webMaster>
24
25 {% for post in posts %}
26 <item>
27 <title>{{ post.title }}</title>
28 <link>{{ url_for("blog", slug=post.slug) }}</link>
29 <author>{{ post.author|default("hello@cleberg.net") }}</author>
30 <guid isPermaLink="false">{{ url_for("blog", slug=post.slug) }}</guid>
31 <pubDate>{{ post.date|strftime("%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
32 <description><![CDATA[{{ post.html|safe }}]]></description>
33 </item>
34 {% endfor %}
35
36 </channel>
37</rss>
theme/templates/garden.html deleted −27
@@ -1,27 +0,0 @@
1{% extends "base.html" %} {% block subtitle %}garden - {% endblock %} {% block main %}
2<h1>Garden</h1>
3<p>Notes in various states of growth. Some are stubs, some are developed. Browse
4around.</p>
5<section>
6 <h2>🪴 The Growth Stages</h2>
7 <p>Instead of dates, these metadata labels show how mature a page is:</p>
8 <span><strong>🌱 Seedlings:</strong> Rough notes, raw ideas, or "half-baked"
9 thoughts.</span><br>
10 <span><strong>🌿 Budding:</strong> Notes that have some structure and a few
11 internal links.</span><br>
12 <span><strong>🌳 Evergreen:</strong> High-conviction ideas that are
13 well-researched and stable.</span><br>
14 <span><strong>🍂 Compost:</strong> Ideas you no longer agree with but want to
15 keep for historical context.</span><br><br>
16</section>
17<ul>
18 {% for post in posts %}
19 <li>
20 <a href='{{ url_for("garden", slug=post.slug) }}'>{{ post.title }}</a>
21 {% if post.description %}
22 <span>{{ post.description }}</span>
23 {% endif %}
24 </li>
25 {% endfor %}
26</ul>
27{% endblock %}
theme/templates/index.html deleted −33
@@ -1,33 +0,0 @@
1{% extends "base.html" %}
2{% block meta %}<meta name="bubbles:fediverse" content="@cmc@c.im">{% endblock %}
3{% block main %}
4<section>
5 <h1>Christian Cleberg</h1>
6 <p>[ <a href="https://krz.sh">krz</a> &middot; <a href="https://audit-labs.dev">audit labs</a> ]</p>
7 <p>Privacy &middot; Self-Hosting &middot; iOS</p>
8 <p>I build tools, write about systems, and run my own infrastructure. Focused on privacy, user control, and long-term utility.</p>
9 <p><a href="/uses/">Uses</a> &middot; <a href="/now/">Now</a></p>
10</section>
11
12<section>
13 <h2>Recent Posts</h2>
14 <p>Things I've written recently.</p>
15 <ul class="post-list">
16 <!-- BEGIN_POSTS --><!-- END_POSTS -->
17 </ul>
18 <p><a href="/blog/">All Posts &rarr;</a></p>
19</section>
20
21<section>
22 <h2>Elsewhere</h2>
23 <p>Places to find me online.</p>
24 <ul>
25 <li>cgit (primary): <a href="https://git.krz.sh">~cmc</a></li>
26 <li>GitHub (mirror): <a href="https://github.com/ccleberg">@ccleberg</a></li>
27 <li>Email: <a href="mailto:hello@cleberg.net">hello@cleberg.net</a> (<a href="/gpg.txt">GPG</a>)</li>
28 <li>Lemmy: <a href="https://r.nf/u/cmc">@cmc</a></li>
29 <li>Mastodon: <a href="https://c.im/@cmc">@cmc</a></li>
30 <li>Pixelfed: <a href="https://gram.social/cmc">@cmc</a></li>
31 </ul>
32</section>
33{% endblock %}
theme/templates/page.html deleted −8
@@ -1,8 +0,0 @@
1{% extends "base.html" %}
2{% block subtitle %}{{ post.title }} - {% endblock %}
3{% block main %}
4<article>
5 <h1>{{ post.title }}</h1>
6 <div>{{ post.html | safe }}</div>
7</article>
8{% endblock %}
theme/templates/post.html deleted −19
@@ -1,19 +0,0 @@
1{% extends "base.html" %} {% block subtitle %} {{ post.title }} - {% endblock %}
2{% block meta %}
3<meta property="og:title" content="{{ post.title }}" />
4<meta property="og:url" content="{{ post.url }}" />
5{% if post.description is defined %}
6<meta property="og:description" content="{{ post.description }}" />
7{% elif project_description is defined %}
8<meta property="og:description" content="{{ project_description }}" />
9{% endif %}
10{% endblock %}
11{% block main %}
12<article>
13 <h1>{{ post.title }}</h1>
14 <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>
15 <div>{{ post.html | safe }}</div>
16 <div><a href="mailto:hello@cleberg.net">Reply via email &rarr;</a></div>
17 <div><i>...or, comment on this post on <a href="https://bubbles.town/source/cleberg.net">Bubbles</a>!</i></div>
18</article>
19{% endblock %}
theme/templates/tags.html deleted −6
@@ -1,6 +0,0 @@
1{% extends "base.html" %} {% block subtitle %}tags - {% endblock %} {% block main %}
2<h1>Tags</h1>
3<p>Browse posts by topic.</p>
4<!-- BEGIN_TAGS -->
5<!-- END_TAGS -->
6{% endblock %}