Commit a6ee2e0ca7
Unsigned
Layout: unified · split
.build.yml.bkp deleted −44
| @@ -1,44 +0,0 @@ | ||
| 1 | image: ubuntu/lts | |
| 2 | ||
| 3 | packages: | |
| 4 | - emacs-nox | |
| 5 | - rsync | |
| 6 | - openssh-client | |
| 7 | - git | |
| 8 | - curl | |
| 9 | - tar | |
| 10 | ||
| 11 | sources: | |
| 12 | - https://git.sr.ht/~ccleberg/cleberg.net | |
| 13 | ||
| 14 | secrets: | |
| 15 | - 303fbd30-2970-4d18-b88b-0f6abf822ea5 | |
| 16 | - e8b41b71-c4b2-4bd6-8159-25ae1eb7d282 | |
| 17 | ||
| 18 | environment: | |
| 19 | ENV: prod | |
| 20 | BUILD: "true" | |
| 21 | ||
| 22 | tasks: | |
| 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 | 1 | .build/ |
| 2 | .build-dev/ | |
| 2 | 3 | .DS_Store |
| 3 | 4 | .gitlab-ci-local/ |
| 4 | 5 | .ruff_cache/ |
| @@ -6,5 +7,4 @@ | ||
| 6 | 7 | public/ |
| 7 | 8 | readme.html |
| 8 | 9 | venv/ |
| 9 | theme/static/styles.min.css | |
| 10 | 10 | __pycache__/ |
README.md +55 −45
| @@ -3,23 +3,26 @@ | ||
| 3 | 3 | This repository holds the files for [cleberg.net](https://cleberg.net), |
| 4 | 4 | a static-site with blog posts, personal links, and more. |
| 5 | 5 | |
| 6 | This site uses [weblorg](https://github.com/emacs-love/weblorg) to build | |
| 7 | the static site, which relies on | |
| 8 | [emacs](https://www.gnu.org/software/emacs/). | |
| 6 | This site uses [orgo](https://github.com/krazywarez/orgo) to build the | |
| 7 | static site. | |
| 9 | 8 | |
| 10 | 9 | ## Site Structure |
| 11 | 10 | |
| 12 | 11 | I write content pages (e.g., blog posts) in org-mode and templates in |
| 13 | HTML. I use Weblorg to build these files into a static site, and then | |
| 14 | deploy them to a web server. | |
| 12 | HTML. orgo builds these files into a static site, and then I deploy | |
| 13 | them to a web server. | |
| 15 | 14 | |
| 16 | 15 | The main site components are: |
| 17 | 16 | |
| 18 | - Org source files containing content, including blog posts and pages. | |
| 19 | - A configuration file (`publish.el`) that specifies publishing | |
| 20 | parameters like base URL, output directories, and export options. | |
| 17 | - Org source files in `content/`, containing blog posts and pages. | |
| 18 | That directory is the site's URL root: `content/blog/post.org` | |
| 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 | 24 | - Assets such as images and style sheets, located in designated |
| 22 | subdirectories. | |
| 25 | subdirectories. `theme/static/` is published at `/`. | |
| 23 | 26 | - Utility scripts (e.g., `build.py`) to facilitate building and |
| 24 | 27 | deployment. |
| 25 | 28 | |
| @@ -27,24 +30,25 @@ The main site components are: | ||
| 27 | 30 | |
| 28 | 31 | The publishing system depends on: |
| 29 | 32 | |
| 30 | - Emacs text editor with Org-Mode. | |
| 31 | - The weblorg package, available at | |
| 32 | [emacs-love/weblorg](https://github.com/emacs-love/weblorg), which | |
| 33 | provides advanced Org publishing functionality and theming support. | |
| 33 | - [orgo](https://github.com/krazywarez/orgo), the static site | |
| 34 | generator, installed with `cargo install --git | |
| 35 | https://github.com/krazywarez/orgo`. | |
| 36 | - `rsync` for deployment. | |
| 37 | - [uv](https://github.com/astral-sh/uv) to run `build.py`. | |
| 34 | 38 | |
| 35 | 39 | ## Configuration |
| 36 | 40 | |
| 37 | You can customize site settings can within the `publish.el` file. This | |
| 38 | file establishes key variables such as: | |
| 41 | You can customize site settings within the `content/orgo.toml` file. | |
| 42 | This file establishes key variables such as: | |
| 39 | 43 | |
| 40 | 44 | - The base URL for links. |
| 41 | - Output directories. | |
| 42 | - Publishing rules to define which files to convert and how. | |
| 43 | - Theme settings managed by weblorg. | |
| 45 | - Which pages appear in the navigation, and in what order. | |
| 46 | - Per-directory template rules. | |
| 47 | - The collections that generate the indexes, tag pages, and feed. | |
| 44 | 48 | |
| 45 | 49 | Users intending to modify site parameters should review and edit this |
| 46 | file accordingly. The weblorg documentation contains extensive details | |
| 47 | on configuration options and expected formats. | |
| 50 | file accordingly. The orgo documentation contains extensive details on | |
| 51 | configuration options and expected formats. | |
| 48 | 52 | |
| 49 | 53 | ## Setup Instructions |
| 50 | 54 | |
| @@ -62,48 +66,54 @@ For users employing Doom Emacs, open any repository Org file using | ||
| 62 | 66 | |
| 63 | 67 | ## Building and Publishing the Site |
| 64 | 68 | |
| 65 | The publishing process involves invoking Emacs with the `publish.el` | |
| 66 | script, which performs the export of Org documents to HTML output. | |
| 69 | The `build.py` script wraps the build: it runs orgo, and then either | |
| 70 | deploys the result or serves it locally. | |
| 67 | 71 | |
| 68 | Configure the environment variable `ENV` as follows: | |
| 72 | Two environment variables control what it does, and both default to | |
| 73 | off: | |
| 69 | 74 | |
| 70 | - If you set `ENV` to `prod`, the script uses production base URL | |
| 71 | settings as defined in `publish.el`. | |
| 72 | - If you do not set `ENV` or set it differently, the script defaults to | |
| 73 | development settings, typically using `localhost:8000` as the base | |
| 74 | URL. | |
| 75 | - `BUILD=true` performs the build. | |
| 76 | - `DEPLOY=true` deploys in production, or starts a development server | |
| 77 | on port 8000 otherwise. | |
| 75 | 78 | |
| 76 | Example commands to build the site: | |
| 79 | A third, `ENV`, selects the mode. Setting `ENV=prod` rewrites image | |
| 80 | URLs to be root-relative (see Deployment below) and skips the `ruff` | |
| 81 | pass; anything else builds for development. | |
| 77 | 82 | |
| 78 | 83 | ``` shell |
| 79 | 84 | # Production build: |
| 80 | ENV=prod emacs --script publish.el | |
| 85 | ENV=prod BUILD=true uv run build.py | |
| 81 | 86 | |
| 82 | 87 | # Development build: |
| 83 | emacs --script publish.el | |
| 88 | BUILD=true uv run build.py | |
| 89 | ||
| 90 | # Development build, then serve it on localhost:8000: | |
| 91 | BUILD=true DEPLOY=true uv run build.py | |
| 84 | 92 | ``` |
| 85 | 93 | |
| 86 | Generated site files reside in the designated output directory, ready | |
| 87 | for deployment. You can deploy the resulting static site files via | |
| 88 | standard file transfer protocols such as `scp` or SFTP. | |
| 94 | Generated site files reside in `.build/` for production and | |
| 95 | `.build-dev/` for development, ready for deployment. You can deploy the | |
| 96 | resulting static site files via standard file transfer protocols such as | |
| 97 | `scp` or SFTP. | |
| 89 | 98 | |
| 90 | The `build.py` script automates the build process. You can execute this | |
| 91 | script with or without the `ENV` variable to perform production or | |
| 92 | development builds respectively. | |
| 99 | Builds are incremental: orgo keeps an `.orgo-cache.json` inside the | |
| 100 | output directory and re-renders only what changed, so the directory is | |
| 101 | left in place between runs. Delete it for a clean build. The two | |
| 102 | environments use separate directories because production rewrites image | |
| 103 | URLs in the output and development does not. | |
| 93 | 104 | |
| 94 | ``` shell | |
| 95 | # Production build script: | |
| 96 | ENV=prod uv run build.py | |
| 105 | orgo can also build and preview on its own, without `build.py`: | |
| 97 | 106 | |
| 98 | # Development build script: | |
| 99 | uv run build.py | |
| 107 | ``` shell | |
| 108 | orgo serve content -o /tmp/preview | |
| 100 | 109 | ``` |
| 101 | 110 | |
| 102 | 111 | ## Deployment |
| 103 | 112 | |
| 104 | Production builds emit root-relative URLs (`/styles.min.css`, | |
| 105 | `/img/blog/...`) instead of absolute `https://` ones so the site renders | |
| 106 | standalone on the onion service without fetching assets off-onion. | |
| 113 | Production builds rewrite image URLs to root-relative ones | |
| 114 | (`/img/blog/...`) instead of absolute `https://img.cleberg.net/` ones so | |
| 115 | the site renders standalone on the onion service without fetching assets | |
| 116 | off-onion. The stylesheet is already same-origin. | |
| 107 | 117 | |
| 108 | 118 | This means the web server must serve the image store at `/img/` on the |
| 109 | 119 | `cleberg.net` vhost. The images live at `/var/www/img/` (their own host, |
build.py +55 −330
| @@ -1,23 +1,23 @@ | ||
| 1 | 1 | #!/usr/bin/env python3 |
| 2 | 2 | """ |
| 3 | This script automates the process of building, testing, and deploying the website. | |
| 3 | This script automates the process of building and deploying the website. | |
| 4 | 4 | It handles tasks such as: |
| 5 | 5 | |
| 6 | - Removing and recreating the build directory. | |
| 7 | - Minifying CSS assets. | |
| 8 | - Running the Emacs publishing script to generate site content. | |
| 9 | - Updating the index.html file with the latest blog posts. | |
| 6 | - Running orgo to generate site content. | |
| 7 | - Rewriting image URLs for the onion service (production only). | |
| 10 | 8 | - Optionally deploying the built site to a remote server. |
| 11 | 9 | - Starting a local development server for previewing changes. |
| 12 | 10 | |
| 13 | 11 | Usage: |
| 14 | Set the environment variable ENV to 'prod' for production builds. | |
| 15 | Run the script to perform the build process accordingly. | |
| 12 | Set BUILD=true to build, DEPLOY=true to deploy or serve. | |
| 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 | |
| 17 | 18 | Dependencies: |
| 18 | 19 | - Python 3 |
| 19 | - Emacs with the publish.el script | |
| 20 | - minify tool for CSS minification | |
| 20 | - orgo, the static site generator (reads content/orgo.toml) | |
| 21 | 21 | - rsync for deployment |
| 22 | 22 | |
| 23 | 23 | Author: |
| @@ -26,202 +26,24 @@ Author: | ||
| 26 | 26 | |
| 27 | 27 | import os |
| 28 | 28 | import re |
| 29 | import shutil | |
| 30 | 29 | import subprocess |
| 31 | 30 | import sys |
| 32 | from datetime import datetime | |
| 33 | from html import escape | |
| 34 | 31 | from pathlib import Path |
| 35 | 32 | |
| 36 | SITE_TEMPLATE_VARS = { | |
| 37 | "site_name": "cleberg.net", | |
| 38 | "site_owner": "Christian Cleberg <hello@cleberg.net>", | |
| 39 | "site_description": "Stillness amidst the chaos.", | |
| 40 | } | |
| 33 | ||
| 34 | def run(cmd, error): | |
| 35 | """Run cmd quietly, exiting with its stderr if it fails.""" | |
| 36 | result = subprocess.run(cmd, capture_output=True, text=True, check=False) | |
| 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 | |
| 43 | 43 | def run_ruff(): |
| 44 | 44 | print("Running ruff...") |
| 45 | 45 | for cmd in [["ruff", "check", "--fix"], ["ruff", "format"]]: |
| 46 | result = subprocess.run(cmd, capture_output=True, text=True, check=False) | |
| 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 | ||
| 53 | def 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 | ||
| 92 | def 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 | ||
| 112 | def 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 | ||
| 199 | def prompt(prompt_text): | |
| 200 | try: | |
| 201 | return input(prompt_text).strip() | |
| 202 | except EOFError: | |
| 203 | return "" | |
| 204 | ||
| 205 | ||
| 206 | def 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 | ||
| 213 | def 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) | |
| 46 | run(cmd, f"ruff error ({' '.join(cmd)}):") | |
| 225 | 47 | |
| 226 | 48 | |
| 227 | 49 | def rewrite_img_urls(build_dir=".build"): |
| @@ -249,22 +71,26 @@ def rewrite_img_urls(build_dir=".build"): | ||
| 249 | 71 | print(f"Rewrote {count} img.cleberg.net references to /img/") |
| 250 | 72 | |
| 251 | 73 | |
| 252 | def run_orgo_build(dev_mode=True): | |
| 74 | def run_orgo_build(build_dir): | |
| 253 | 75 | """ |
| 254 | 76 | Build the site with orgo. |
| 255 | 77 | |
| 256 | Replaces the weblorg/Emacs publish. orgo reads content/orgo.toml, so the routes, | |
| 257 | templates and collections that used to live in publish.el live there now. Four of the | |
| 258 | steps this script used to perform afterwards are gone with it: the blog index groups | |
| 259 | itself by year, the tags page is a collection, the recent-posts list is generated from | |
| 260 | the blog collection, and the sitemap is written by orgo once base_url is set. | |
| 78 | orgo reads content/orgo.toml, which holds the routes, templates and collections: the | |
| 79 | blog index groups itself by year, the tags page is a collection, the recent-posts | |
| 80 | list comes from the blog collection, and the sitemap is written once base_url is set. | |
| 81 | ||
| 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 | |
| 263 | sources for readers who want them, and rewriting image URLs for the onion. | |
| 89 | What is left around it is what orgo does not do: rewriting image URLs for the onion. | |
| 264 | 90 | """ |
| 265 | 91 | print("Building with orgo...") |
| 266 | 92 | result = subprocess.run( |
| 267 | ["orgo", "build", "content", "-o", ".build", "--strict"], | |
| 93 | ["orgo", "build", "content", "-o", str(build_dir), "--strict"], | |
| 268 | 94 | stdout=subprocess.PIPE, |
| 269 | 95 | stderr=subprocess.STDOUT, |
| 270 | 96 | text=True, |
| @@ -276,99 +102,10 @@ def run_orgo_build(dev_mode=True): | ||
| 276 | 102 | sys.exit(1) |
| 277 | 103 | |
| 278 | 104 | |
| 279 | def 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 | ||
| 306 | def 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 | ||
| 368 | 105 | def deploy_to_server(build_dir, server): |
| 369 | 106 | remote_path = f"{server}:/var/www/cleberg.net/" |
| 370 | print(f"Deploying .build/ → {remote_path}") | |
| 371 | result = subprocess.run( | |
| 107 | print(f"Deploying {build_dir}/ → {remote_path}") | |
| 108 | run( | |
| 372 | 109 | # The build cache lives in the output directory because it describes it, but it |
| 373 | 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 | 117 | f"{build_dir}/", |
| 381 | 118 | remote_path, |
| 382 | 119 | ], |
| 383 | capture_output=True, | |
| 384 | text=True, | |
| 385 | check=False, | |
| 120 | "Error during rsync deployment:", | |
| 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 | |
| 393 | 124 | def start_dev_server(build_dir): |
| @@ -404,38 +135,32 @@ def start_dev_server(build_dir): | ||
| 404 | 135 | |
| 405 | 136 | |
| 406 | 137 | def main(): |
| 407 | env = os.environ.get("ENV", "").casefold() | |
| 408 | if env != "prod": | |
| 138 | prod = os.environ.get("ENV", "").casefold() == "prod" | |
| 139 | if not prod: | |
| 409 | 140 | run_ruff() |
| 410 | 141 | |
| 411 | build_dir = Path(".build") | |
| 412 | theme_dir = Path("theme/static") | |
| 413 | css_src = theme_dir / "styles.css" | |
| 414 | css_min = theme_dir / "styles.min.css" | |
| 415 | ||
| 416 | build = os.environ.get("BUILD", "").casefold() == "true" | |
| 417 | deploy = os.environ.get("DEPLOY", "").casefold() == "true" | |
| 418 | ||
| 419 | if env == "prod": | |
| 420 | print("Environment: Production") | |
| 421 | if build: | |
| 422 | remove_build_directory(build_dir) | |
| 423 | minify_css(css_src, css_min) | |
| 424 | run_orgo_build(dev_mode=False) | |
| 425 | copy_org_sources() | |
| 142 | # One output directory per environment, because the two differ after orgo has run: | |
| 143 | # production rewrites image URLs in place and development does not. Sharing a | |
| 144 | # directory would let an incremental build reuse a page rendered for the other one — | |
| 145 | # a dev preview showing /img/ paths that only resolve on the server. Production keeps | |
| 146 | # .build/ because that is the directory the deploy and the CI manifest name. | |
| 147 | build_dir = Path(".build" if prod else ".build-dev") | |
| 148 | ||
| 149 | print(f"Environment: {'Production' if prod else 'Development'}") | |
| 150 | ||
| 151 | if os.environ.get("BUILD", "").casefold() == "true": | |
| 152 | run_orgo_build(build_dir) | |
| 153 | # The onion needs same-origin images; dev previews keep the absolute URLs. | |
| 154 | # Runs over every page, not just the re-rendered ones, so a page carried over | |
| 155 | # from an earlier build is rewritten too. | |
| 156 | if prod: | |
| 426 | 157 | rewrite_img_urls(build_dir) |
| 427 | if deploy: | |
| 158 | ||
| 159 | if os.environ.get("DEPLOY", "").casefold() == "true": | |
| 160 | if prod: | |
| 428 | 161 | print("Deploying to production...") |
| 429 | 162 | deploy_to_server(build_dir, "homelab") |
| 430 | return | |
| 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: | |
| 163 | else: | |
| 439 | 164 | start_dev_server(build_dir) |
| 440 | 165 | |
| 441 | 166 | |
content/orgo.toml +4 −12
| @@ -5,9 +5,6 @@ | ||
| 5 | 5 | # |
| 6 | 6 | # orgo serve content -o /tmp/preview # write and preview |
| 7 | 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 | 9 | [site] |
| 13 | 10 | title = "cleberg.net" |
| @@ -21,8 +18,7 @@ base_url = "https://cleberg.net" | ||
| 21 | 18 | [nav] |
| 22 | 19 | # Explicit, because the pages that belong in the nav are not all top-level: /blog/ and |
| 23 | 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 | |
| 25 | # base.html does. | |
| 21 | # is deliberately absent — the layout writes the Home link itself. | |
| 26 | 22 | # |
| 27 | 23 | # Generated pages are named by their output path, authored ones by their source. Listing |
| 28 | 24 | # them together is what puts Salary last: a collection with `nav = true` that is not |
| @@ -32,15 +28,13 @@ mode = "explicit" | ||
| 32 | 28 | pages = ["blog/index.html", "garden/index.html", "salary/index.org"] |
| 33 | 29 | |
| 34 | 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 | 32 | # rather than needing its files copied next to the writing — one copy, not two. |
| 37 | 33 | assets = ["../theme/static"] |
| 38 | 34 | |
| 39 | 35 | [templates] |
| 40 | 36 | dir = "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 | 38 | # Blog posts render through post.html, which adds the reply snippet. Nothing else does: |
| 45 | 39 | # the live site also invites replies on garden notes and /now/, and that is a choice |
| 46 | 40 | # rather than something a directory should decide. Any page opts in for itself with |
| @@ -63,13 +57,11 @@ toc = true | ||
| 63 | 57 | section_numbers = true |
| 64 | 58 | |
| 65 | 59 | # --- Generated pages ------------------------------------------------------------- |
| 66 | # These have no source .org file. weblorg produces the equivalents today through its | |
| 67 | # blog-index, garden-index and tags routes. | |
| 60 | # These have no source .org file. | |
| 68 | 61 | |
| 69 | 62 | # The home page. Generated rather than authored, because it is a hand-written |
| 70 | 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 | |
| 72 | # it today too. | |
| 64 | # maintain by hand. The prose lives in templates/home.html. | |
| 73 | 65 | # |
| 74 | 66 | # content/index.org was deleted to make room: it held only #+title and #+description, |
| 75 | 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 | 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 | |
| 5 | copied. weblorg links the minified styles.min.css instead; that file is gitignored | |
| 6 | build output and the rules are the same, so this links the tracked one. | |
| 4 | orgo.toml and read in place rather than copied. It is served as authored: at ~1KB, | |
| 5 | minifying it saved 52 bytes over the wire once compressed, which did not pay for a | |
| 6 | build step and a toolchain dependency. | |
| 7 | 7 | |
| 8 | 8 | Available here: page (.title .url .source .date .date_iso .tags .excerpt .toc |
| 9 | 9 | .keywords), site, nav, root, stylesheet. See the orgo guide on Templates. #} |
| @@ -45,8 +45,8 @@ | ||
| 45 | 45 | <link rel="alternate" type="application/rss+xml" title="{{ site.title }}" href="{{ root }}feed.xml"> |
| 46 | 46 | <link rel="stylesheet" href="{{ root }}styles.css" type="text/css"> |
| 47 | 47 | <link rel="icon" href="data:,"> |
| 48 | {#- rel="me" identity links. weblorg's base.html writes the last one as <a>, which a | |
| 49 | browser hoists out of <head>; <link> is the spelling that stays where it is put. #} | |
| 48 | {#- rel="me" identity links, spelled as <link> rather than <a>: a browser hoists a | |
| 49 | stray <a> out of <head>, while <link> stays where it is put. #} | |
| 50 | 50 | <link href="https://github.com/ccleberg" rel="me"> |
| 51 | 51 | <link href="https://gitlab.com/ccleberg" rel="me"> |
| 52 | 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. | |
| 2 | Separate from list.html because the live page carries its own prose. #} | |
| 1 | {# The blog index at /blog/. | |
| 2 | Separate from list.html because the page carries its own prose. #} | |
| 3 | 3 | {% extends "base.html" %} |
| 4 | 4 | |
| 5 | 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 | 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 | |
| 3 | from the site that served it, so every URL here is absolute — which is what | |
| 4 | `| absolute` needs [site] base_url for. | |
| 2 | {#- The RSS feed at /feed.xml. A feed is read away from the site that served it, so | |
| 3 | every URL here is absolute — which is what `| absolute` needs [site] base_url for. | |
| 5 | 4 | |
| 6 | <description> carries each post's full rendered HTML, as weblorg's feed does — the | |
| 7 | collection sets `include_content` to ask for it. #} | |
| 5 | <description> carries each post's full rendered HTML — the collection sets | |
| 6 | `include_content` to ask for it. #} | |
| 8 | 7 | <rss version="2.0" |
| 9 | 8 | xmlns:content="http://purl.org/rss/1.0/modules/content/" |
| 10 | 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 | 2 | Notes are listed by title and description rather than by date. #} |
| 3 | 3 | {% extends "base.html" %} |
| 4 | 4 | {% block main %} |
content/templates/home.html +2 −3
| @@ -1,9 +1,8 @@ | ||
| 1 | 1 | {# The home page: authored prose plus the most recent posts. |
| 2 | 2 | |
| 3 | 3 | A generated page, so it has no source .org file — `pages` holds the blog collection's |
| 4 | entries, newest first, and the prose lives here. This mirrors what weblorg's | |
| 5 | theme/templates/index.html does today, including its bare <title>: the home page is | |
| 6 | "cleberg.net" and not "cleberg.net - cleberg.net". #} | |
| 4 | entries, newest first, and the prose lives here. The <title> is bare on purpose: the | |
| 5 | home page is "cleberg.net" and not "cleberg.net - cleberg.net". #} | |
| 7 | 6 | {% extends "base.html" %} |
| 8 | 7 | {% block subtitle %}{% endblock %} |
| 9 | 8 | {% block main %} |
content/templates/tags.html +4 −5
| @@ -1,11 +1,10 @@ | ||
| 1 | 1 | {# The tag index at /tags/. Receives `groups` — every tag with its count — rather than |
| 2 | 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 | |
| 5 | every post inlined under an `#anchor` per tag. Here each tag is its own page, so a tag | |
| 6 | has a URL worth linking to and the index stays short. The `id` on each entry keeps the | |
| 7 | old `/tags/#audit` links landing in the right place — they scroll to the tag and its | |
| 8 | link, rather than 404ing or dumping the reader at the top of a long page. #} | |
| 4 | Each tag is its own page, so a tag has a URL worth linking to and the index stays | |
| 5 | short. The `id` on each entry keeps older `/tags/#audit` links landing in the right | |
| 6 | place — they scroll to the tag and its link, rather than dumping the reader at the | |
| 7 | top of the index. #} | |
| 9 | 8 | {% extends "base.html" %} |
| 10 | 9 | {% block main %} |
| 11 | 10 | <h1>{{ page.title }}</h1> |
content/uses/index.org +1 −1
| @@ -24,7 +24,7 @@ threat model justification. | ||
| 24 | 24 | | Operating System | [[https://ubuntu.com][Ubuntu]] + [[https://www.apple.com/os/macos/][macOS]] | Ubuntu: telemetry off, LTS, stable. macOS: workstation only. | |
| 25 | 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 | 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 | 28 | | Terminal | [[https://iterm2.com/][iTerm2]] | Functional. Inherited from macOS. | |
| 29 | 29 | | Shell | [[https://www.zsh.org/][Zsh]] | Portable, POSIX-adjacent, available on every target OS. | |
| 30 | 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> · | |
| 39 | <a href="/feed.xml">RSS</a> · | |
| 40 | <a href="http://paske4urhs6nttrtlkuwa5cowum3fjkc6yv6kl4ncx3mjxcd77764nqd.onion/">Onion</a> · | |
| 41 | <a href="/tips/">Tips</a> · | |
| 42 | <a href="https://iheartrss.com/">I ♥ RSS</a> | |
| 43 | <p>[ <a href="https://krz.sh">krz</a> · <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 · <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 | |
| 4 | around.</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> · <a href="https://audit-labs.dev">audit labs</a> ]</p> | |
| 7 | <p>Privacy · Self-Hosting · 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> · <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 →</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 %} · {{ post.description }}{% endif %}</p> | |
| 15 | <div>{{ post.html | safe }}</div> | |
| 16 | <div><a href="mailto:hello@cleberg.net">Reply via email →</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 %} | |