aboutsummaryrefslogtreecommitdiff
path: root/build.py
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-12 10:45:16 -0500
committerChristian Cleberg <[email protected]>2026-04-12 10:45:16 -0500
commit36e16946cfe11b907e0144ada68228d3c4bf7b97 (patch)
tree5d5c08825f0ad22c66d92d242e61f7878a6d829f /build.py
parentb41f62c2851f2ac1b4eeb8acad483c2ef8af9be5 (diff)
downloadcleberg.net-36e16946cfe11b907e0144ada68228d3c4bf7b97.tar.gz
cleberg.net-36e16946cfe11b907e0144ada68228d3c4bf7b97.tar.bz2
cleberg.net-36e16946cfe11b907e0144ada68228d3c4bf7b97.zip
fix: refactor tags page rendering and auto-include new blog tags
Diffstat (limited to 'build.py')
-rw-r--r--build.py269
1 files changed, 133 insertions, 136 deletions
diff --git a/build.py b/build.py
index 3dac877..d3147bc 100644
--- a/build.py
+++ b/build.py
@@ -29,11 +29,19 @@ import re
import shutil
import subprocess
import sys
+from html import escape
from urllib.parse import quote
from datetime import datetime
from pathlib import Path
+SITE_TEMPLATE_VARS = {
+ "site_name": "cleberg.net",
+ "site_owner": "Christian Cleberg <[email protected]>",
+ "site_description": "Stillness amidst the chaos.",
+}
+
+
def run_ruff():
print("Running ruff...")
for cmd in [["ruff", "check", "--fix"], ["ruff", "format"]]:
@@ -46,19 +54,20 @@ def run_ruff():
sys.exit(1)
-def update_index_html(html_snippet, template_path="./.build/index.html"):
+def update_marked_section(
+ html_snippet,
+ template_path="./.build/index.html",
+ begin_marker="<!-- BEGIN_POSTS -->",
+ end_marker="<!-- END_POSTS -->",
+):
"""
- Read the index.html file at `template_path`, replace everything between
- <!-- BEGIN_POSTS --> and <!-- END_POSTS --> with the provided html_snippet,
- and write the updated content back to the same file.
+ Read the file at `template_path`, replace everything between `begin_marker`
+ and `end_marker` with the provided html_snippet, and write the updated
+ content back to the same file.
"""
- # Read the current contents of index.html
with open(template_path, "r", encoding="utf-8") as f:
content = f.read()
- begin_marker = "<!-- BEGIN_POSTS -->"
- end_marker = "<!-- END_POSTS -->"
-
# Find the indices of the markers
begin_index = content.find(begin_marker)
end_index = content.find(end_marker)
@@ -105,18 +114,72 @@ def update_index_html(html_snippet, template_path="./.build/index.html"):
f.write(new_content)
-def get_recent_posts_html(content_dir="./content/blog", num_posts=3):
+def render_base_template(main_html, subtitle="", title=None):
+ """
+ Render a small subset of the site's shared templates for Python-generated
+ pages that still need to follow the common chrome.
+ """
+ base_template = Path("theme/templates/base.html").read_text(encoding="utf-8")
+
+ if title is None:
+ title = SITE_TEMPLATE_VARS["site_name"]
+
+ rendered = base_template
+ rendered = rendered.replace(
+ "{% block subtitle %}{% endblock %}",
+ escape(subtitle),
+ )
+ rendered = rendered.replace(
+ '{% block title %}{{ site_name | default("cleberg.net") }}{% endblock %}',
+ escape(title),
+ )
+ rendered = rendered.replace(
+ '{% if site_owner is defined %}<meta name="author" content="{{ site_owner }}">{% endif %}',
+ f'<meta name="author" content="{escape(SITE_TEMPLATE_VARS["site_owner"])}">',
+ )
+ rendered = rendered.replace(
+ '{% if site_description is defined %}<meta name="description" content="{{ site_description }}">{% endif %}',
+ f'<meta name="description" content="{escape(SITE_TEMPLATE_VARS["site_description"])}">',
+ )
+ rendered = rendered.replace(
+ '{% if site_keywords is defined %}<meta name="keywords" content="{{ site_keywords }}">{% endif %}',
+ "",
+ )
+ rendered = rendered.replace(
+ '{{ url_for("static", file="styles.min.css") }}',
+ "/styles.min.css",
+ )
+ rendered = rendered.replace("{% block meta %}{% endblock %}", "")
+ rendered = rendered.replace("{% block head %}", "")
+ rendered = rendered.replace("{% endblock %}", "", 1)
+ rendered = rendered.replace("{% block main %}{% endblock %}", main_html)
+
+ return rendered
+
+
+def render_tags_page_html(tags_html):
"""
- Scan the `content_dir` for .org blog post files, extract their headers,
- and return an HTML snippet for the `num_posts` most recent posts.
+ Render the tags page by using the shared tags/base templates instead of a
+ hand-authored standalone HTML document.
+ """
+ tags_template = Path("theme/templates/tags.html").read_text(encoding="utf-8")
+
+ main_html = tags_template
+ main_html = main_html.replace('{% extends "base.html" %}', "")
+ main_html = main_html.replace(
+ "{% block subtitle %}tags - {% endblock %}",
+ "",
+ )
+ main_html = main_html.replace("{% block main %}", "")
+ main_html = main_html.replace("{% endblock %}", "")
+ main_html = main_html.replace("<!-- BEGIN_TAGS -->\n<!-- END_TAGS -->", tags_html)
- Expects each .org file to contain headers of the form:
- #+title: Post Title
- #+date: <YYYY-MM-DD Day HH:MM:SS>
- #+slug: post-slug
+ return render_base_template(main_html.strip(), subtitle="tags - ")
- Returns a string containing the section with the three most recent posts,
- formatted like the snippet in the prompt.
+
+def get_blog_posts(content_dir="./content/blog"):
+ """
+ Scan blog posts and return normalized metadata for non-draft entries.
"""
posts = []
@@ -124,6 +187,7 @@ def get_recent_posts_html(content_dir="./content/blog", num_posts=3):
"title": re.compile(r"^#\+title:\s*(.+)$", re.IGNORECASE),
"date": re.compile(r"^#\+date:\s*[\[<](\d{4}-\d{2}-\d{2})"),
"slug": re.compile(r"^#\+slug:\s*(.+)$", re.IGNORECASE),
+ "tags": re.compile(r"^#\+filetags:\s*(.+)$", re.IGNORECASE),
"draft": re.compile(r"^#\+draft:\s*(.+)$", re.IGNORECASE),
}
@@ -131,6 +195,7 @@ def get_recent_posts_html(content_dir="./content/blog", num_posts=3):
title = None
date_str = None
slug = None
+ tags = []
is_draft = False
with org_path.open("r", encoding="utf-8") as f:
@@ -154,6 +219,13 @@ def get_recent_posts_html(content_dir="./content/blog", num_posts=3):
slug = m.group(1).strip()
continue
+ if not tags:
+ m = header_patterns["tags"].match(line)
+ if m:
+ raw = m.group(1).strip().strip(":")
+ tags = [t.strip() for t in raw.split(":") if t.strip()]
+ continue
+
m = header_patterns["draft"].match(line)
if m:
draft_value = m.group(1).strip().lower()
@@ -184,16 +256,20 @@ def get_recent_posts_html(content_dir="./content/blog", num_posts=3):
"date_obj": date_obj,
"date_full": date_full,
"slug": slug,
+ "tags": tags,
}
)
- # Sort posts by date (newest first)
posts.sort(key=lambda x: x["date_obj"], reverse=True)
+ return posts
- # Take the top `num_posts`
- recent = posts[:num_posts]
- # Build HTML lines
+def get_recent_posts_html(content_dir="./content/blog", num_posts=3):
+ """
+ Return an HTML snippet for the `num_posts` most recent blog posts.
+ """
+ recent = get_blog_posts(content_dir)[:num_posts]
+
lines = []
for post in recent:
lines.append('\t<li class="post-list-item">')
@@ -384,15 +460,15 @@ def inject_blog_year_separators(blog_index_path="./.build/blog/index.html"):
print(f"Blog year separators injected into {blog_index_path}")
-def generate_tags_page(content_dir="./content/blog", build_dir="./.build"):
+def get_tags_html(content_dir="./content/blog"):
"""
- Scans all blog org files for #+filetags, then writes .build/tags/index.html
- with one section per tag, each post listed under its tags, sorted newest first.
- Tags link to anchor IDs on the same page.
+ Build the tag index HTML snippet for the rendered tags template.
"""
- TAG_ORDER = [
+ preferred_tag_order = [
"audit",
"emacs",
+ "development",
+ "ios",
"linux",
"personal",
"privacy",
@@ -401,82 +477,32 @@ def generate_tags_page(content_dir="./content/blog", build_dir="./.build"):
"web",
]
- header_patterns = {
- "title": re.compile(r"^#\+title:\s*(.+)$", re.IGNORECASE),
- "date": re.compile(r"^#\+date:\s*[\[<](\d{4}-\d{2}-\d{2})"),
- "slug": re.compile(r"^#\+slug:\s*(.+)$", re.IGNORECASE),
- "tags": re.compile(r"^#\+filetags:\s*(.+)$", re.IGNORECASE),
- "draft": re.compile(r"^#\+draft:\s*(.+)$", re.IGNORECASE),
- }
+ tag_map = {}
- tag_map = {tag: [] for tag in TAG_ORDER}
-
- for org_path in Path(content_dir).glob("*.org"):
- title = date_str = slug = None
- tags = []
- is_draft = False
-
- with org_path.open("r", encoding="utf-8") as f:
- for line in f:
- if not title:
- m = header_patterns["title"].match(line)
- if m:
- title = m.group(1).strip()
- continue
- if not date_str:
- m = header_patterns["date"].match(line)
- if m:
- date_str = m.group(1)
- continue
- if not slug:
- m = header_patterns["slug"].match(line)
- if m:
- slug = m.group(1).strip()
- continue
- if not tags:
- m = header_patterns["tags"].match(line)
- if m:
- raw = m.group(1).strip().strip(":")
- tags = [t.strip() for t in raw.split(":") if t.strip()]
- continue
- m = header_patterns["draft"].match(line)
- if m and m.group(1).strip().lower() != "nil":
- is_draft = True
- break
- if title and date_str and slug and tags:
- break
-
- if is_draft or not (title and date_str and slug):
- continue
-
- try:
- date_obj = datetime.strptime(date_str, "%Y-%m-%d")
- except ValueError:
- continue
+ for post in get_blog_posts(content_dir):
+ for tag in post["tags"]:
+ tag_map.setdefault(tag, []).append(
+ {
+ "title": post["title"],
+ "slug": post["slug"],
+ "date_obj": post["date_obj"],
+ "date_str": post["date_str"],
+ }
+ )
- for tag in tags:
- if tag in tag_map:
- tag_map[tag].append(
- {
- "title": title,
- "slug": slug,
- "date_obj": date_obj,
- "date_str": date_str,
- }
- )
+ ordered_tags = [tag for tag in preferred_tag_order if tag in tag_map]
+ ordered_tags.extend(sorted(tag for tag in tag_map if tag not in preferred_tag_order))
- for tag in tag_map:
+ for tag in ordered_tags:
tag_map[tag].sort(key=lambda x: x["date_obj"], reverse=True)
- # Build TOC
toc_items = "".join(
f'<li><a href="#{tag}">{tag}</a> <span class="tag-count">({len(tag_map[tag])})</span></li>'
- for tag in TAG_ORDER
+ for tag in ordered_tags
)
- # Build sections
sections = []
- for tag in TAG_ORDER:
+ for tag in ordered_tags:
posts = tag_map[tag]
items = "\n".join(
f'<li class="post-list-item">'
@@ -489,48 +515,19 @@ def generate_tags_page(content_dir="./content/blog", build_dir="./.build"):
f'<h2 id="{tag}">{tag}</h2>\n<ul class="post-list">\n{items}\n</ul>'
)
- html = f"""<!doctype html>
-<html lang=en-us>
-<head>
-<meta charset=utf-8>
-<title>cleberg.net</title>
-<meta name=viewport content="width=device-width,initial-scale=1">
-<meta name=author content="Christian Cleberg <[email protected]>">
-<meta name=description content="Stillness amidst the chaos.">
-<link rel=stylesheet href=/styles.min.css>
-<link rel=icon href=data:,>
-</head>
-<body>
-<a href=#main-content class=skip-link>Skip to content</a>
-<nav aria-label="Site Navigation">
-<ul>
-<li><a href=/>Home</a>
-<li><a href=/apps/>Apps</a>
-<li><a href=/blog/>Blog</a>
-<li><a href=/garden/>Garden</a>
-<li><a href=/guides/>Guides</a>
-<li><a href=/salary/>Salary</a>
-<li><a href=/tags/>Tags</a>
-</ul>
-</nav>
-<main id=main-content>
-<h1>Tags</h1>
-<ul class="tag-toc">{toc_items}</ul>
-{"".join(f"<section>{s}</section>" for s in sections)}
-</main>
-<footer>
-<a href=https://sr.ht/~ccleberg/cleberg.net>Source</a>&#183;
-<a href=/feed.xml>RSS</a>&#183;
-<a href=http://paske4urhs6nttrtlkuwa5cowum3fjkc6yv6kl4ncx3mjxcd77764nqd.onion/>Onion</a>&#183;
-<a href=/tips/>Tips</a>
-</footer>
-</body>
-</html>"""
-
- out_dir = Path(build_dir) / "tags"
- out_dir.mkdir(parents=True, exist_ok=True)
- out_path = out_dir / "index.html"
- out_path.write_text(html, encoding="utf-8")
+ return f'<ul class="tag-toc">{toc_items}</ul>\n' + "".join(
+ f"<section>{section}</section>" for section in sections
+ )
+
+
+def generate_tags_page(content_dir="./content/blog", build_dir="./.build"):
+ """
+ Render the tags page using the shared template structure.
+ """
+ tags_html = get_tags_html(content_dir)
+ out_path = Path(build_dir) / "tags" / "index.html"
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+ out_path.write_text(render_tags_page_html(tags_html), encoding="utf-8")
print(f"Tags page written to {out_path}")
@@ -583,7 +580,7 @@ def main():
minify_css(css_src, css_min)
run_emacs_publish(dev_mode=False)
copy_org_sources()
- update_index_html(html_snippet)
+ update_marked_section(html_snippet)
inject_blog_year_separators()
generate_tags_page()
# minify_html("./.build/index.html", "./.build/index.html")
@@ -599,7 +596,7 @@ def main():
minify_css(css_src, css_min)
run_emacs_publish(dev_mode=True)
copy_org_sources()
- update_index_html(html_snippet)
+ update_marked_section(html_snippet)
inject_blog_year_separators()
generate_tags_page()
minify_html("./.build/index.html", "./.build/index.html")