From e4fc1f14a255d0bcb961c056357ca7eff5467a13 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 11 Mar 2026 03:00:50 -0500 Subject: add tag support --- build.py | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) (limited to 'build.py') diff --git a/build.py b/build.py index 29b8d44..57ad969 100644 --- a/build.py +++ b/build.py @@ -381,6 +381,141 @@ 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"): + """ + 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. + """ + TAG_ORDER = ["audit", "emacs", "linux", "personal", "privacy", "security", "self-hosting", "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: [] 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 tag in tags: + if tag in tag_map: + tag_map[tag].append({"title": title, "slug": slug, "date_obj": date_obj, "date_str": date_str}) + + for tag in tag_map: + tag_map[tag].sort(key=lambda x: x["date_obj"], reverse=True) + + # Build TOC + toc_items = "".join( + f'
  • {tag} ({len(tag_map[tag])})
  • ' + for tag in TAG_ORDER + ) + + # Build sections + sections = [] + for tag in TAG_ORDER: + posts = tag_map[tag] + items = "\n".join( + f'
  • ' + f'{p["title"]}' + f'' + f'
  • ' + for p in posts + ) + sections.append(f'

    {tag}

    \n') + + html = f""" + + + +tags - seijaku + + + + + + +
    +seijaku +

    stillness amidst the chaos

    + +
    +
    +

    Tags

    + +{"".join(f"
    {s}
    " for s in sections)} +
    +
    + + +""" + + 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") + print(f"Tags page written to {out_path}") + + def deploy_to_server(build_dir, server): remote_path = f"{server}:/var/www/cleberg.net/" print(f"Deploying .build/ → {remote_path}") @@ -430,6 +565,7 @@ def main(): copy_org_sources() update_index_html(html_snippet) inject_blog_year_separators() + generate_tags_page() minify_html("./.build/index.html", "./.build/index.html") generate_sitemap() if deploy: @@ -445,6 +581,7 @@ def main(): copy_org_sources() update_index_html(html_snippet) inject_blog_year_separators() + generate_tags_page() minify_html("./.build/index.html", "./.build/index.html") generate_sitemap() if deploy: -- cgit v1.2.3