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 | .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 @@ | |||
| 6 | public/ | 7 | public/ |
| 7 | readme.html | 8 | readme.html |
| 8 | venv/ | 9 | venv/ |
| 9 | theme/static/styles.min.css | ||
| 10 | __pycache__/ | 10 | __pycache__/ |
README.md +55 −45
| @@ -3,23 +3,26 @@ | |||
| 3 | This repository holds the files for [cleberg.net](https://cleberg.net), | 3 | This repository holds the files for [cleberg.net](https://cleberg.net), |
| 4 | a static-site with blog posts, personal links, and more. | 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 | 6 | This site uses [orgo](https://github.com/krazywarez/orgo) to build the |
| 7 | the static site, which relies on | 7 | static site. |
| 8 | [emacs](https://www.gnu.org/software/emacs/). | ||
| 9 | 8 | ||
| 10 | ## Site Structure | 9 | ## Site Structure |
| 11 | 10 | ||
| 12 | I write content pages (e.g., blog posts) in org-mode and templates in | 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 | 12 | HTML. orgo builds these files into a static site, and then I deploy |
| 14 | deploy them to a web server. | 13 | them to a web server. |
| 15 | 14 | ||
| 16 | The main site components are: | 15 | The 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 | ||
| 28 | The publishing system depends on: | 31 | The 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 | ||
| 37 | You can customize site settings can within the `publish.el` file. This | 41 | You can customize site settings within the `content/orgo.toml` file. |
| 38 | file establishes key variables such as: | 42 | This 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 | ||
| 45 | Users intending to modify site parameters should review and edit this | 49 | Users intending to modify site parameters should review and edit this |
| 46 | file accordingly. The weblorg documentation contains extensive details | 50 | file accordingly. The orgo documentation contains extensive details on |
| 47 | on configuration options and expected formats. | 51 | configuration 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 | ||
| 65 | The publishing process involves invoking Emacs with the `publish.el` | 69 | The `build.py` script wraps the build: it runs orgo, and then either |
| 66 | script, which performs the export of Org documents to HTML output. | 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 | 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 | ||
| 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 | ``` shell | 83 | ``` shell |
| 79 | # Production build: | 84 | # Production build: |
| 80 | ENV=prod emacs --script publish.el | 85 | ENV=prod BUILD=true uv run build.py |
| 81 | 86 | ||
| 82 | # Development build: | 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 | 94 | Generated site files reside in `.build/` for production and |
| 87 | for deployment. You can deploy the resulting static site files via | 95 | `.build-dev/` for development, ready for deployment. You can deploy the |
| 88 | standard file transfer protocols such as `scp` or SFTP. | 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 | 99 | Builds are incremental: orgo keeps an `.orgo-cache.json` inside the |
| 91 | script with or without the `ENV` variable to perform production or | 100 | output directory and re-renders only what changed, so the directory is |
| 92 | development builds respectively. | 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 | 105 | orgo can also build and preview on its own, without `build.py`: |
| 95 | # Production build script: | ||
| 96 | ENV=prod uv run build.py | ||
| 97 | 106 | ||
| 98 | # Development build script: | 107 | ``` shell |
| 99 | uv run build.py | 108 | orgo serve content -o /tmp/preview |
| 100 | ``` | 109 | ``` |
| 101 | 110 | ||
| 102 | ## Deployment | 111 | ## Deployment |
| 103 | 112 | ||
| 104 | Production builds emit root-relative URLs (`/styles.min.css`, | 113 | Production 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 |
| 106 | standalone on the onion service without fetching assets off-onion. | 115 | the site renders standalone on the onion service without fetching assets |
| 116 | off-onion. The stylesheet is already same-origin. | ||
| 107 | 117 | ||
| 108 | This means the web server must serve the image store at `/img/` on the | 118 | This 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 | """ |
| 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 | It handles tasks such as: | 4 | It 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 | ||
| 13 | Usage: | 11 | Usage: |
| 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 | ||
| 17 | Dependencies: | 18 | Dependencies: |
| 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 | ||
| 23 | Author: | 23 | Author: |
| @@ -26,202 +26,24 @@ Author: | |||
| 26 | 26 | ||
| 27 | import os | 27 | import os |
| 28 | import re | 28 | import re |
| 29 | import shutil | ||
| 30 | import subprocess | 29 | import subprocess |
| 31 | import sys | 30 | import sys |
| 32 | from datetime import datetime | ||
| 33 | from html import escape | ||
| 34 | from pathlib import Path | 31 | from pathlib import Path |
| 35 | 32 | ||
| 36 | SITE_TEMPLATE_VARS = { | 33 | |
| 37 | "site_name": "cleberg.net", | 34 | def 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 | ||
| 43 | def run_ruff(): | 43 | def 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 | |||
| 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) | ||
| 225 | 47 | ||
| 226 | 48 | ||
| 227 | def rewrite_img_urls(build_dir=".build"): | 49 | def 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 | ||
| 252 | def run_orgo_build(dev_mode=True): | 74 | def 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 | ||
| 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 | def deploy_to_server(build_dir, server): | 105 | def 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 | ||
| 393 | def start_dev_server(build_dir): | 124 | def start_dev_server(build_dir): |
| @@ -404,38 +135,32 @@ def start_dev_server(build_dir): | |||
| 404 | 135 | ||
| 405 | 136 | ||
| 406 | def main(): | 137 | def 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] |
| 13 | title = "cleberg.net" | 10 | title = "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" | |||
| 32 | pages = ["blog/index.html", "garden/index.html", "salary/index.org"] | 28 | pages = ["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. |
| 37 | assets = ["../theme/static"] | 33 | assets = ["../theme/static"] |
| 38 | 34 | ||
| 39 | [templates] | 35 | [templates] |
| 40 | dir = "templates" | 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 | # 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 | |||
| 63 | section_numbers = true | 57 | section_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> · | ||
| 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 %} | ||