From: Alexander Goussas Date: Mon, 18 May 2026 16:40:13 +0000 (-0500) Subject: feat: add rss generator script X-Git-Url: http://git.frustrated-labs.net/?a=commitdiff_plain;h=fabf6d6014fec1c2a97dc07da43f19028004c10e;p=frustrated-functor.dev.git feat: add rss generator script Copied from https://github.com/kassane/kassane.github.io/blob/main/tools/gen_rss.py --- diff --git a/.gitignore b/.gitignore index 0bfa928..ff90370 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ zig-out !public/index.html public/*.html sample.rss +public/*.rss +public/*.xml diff --git a/layouts/templates/base.shtml b/layouts/templates/base.shtml index 337d76e..e26e5ab 100644 --- a/layouts/templates/base.shtml +++ b/layouts/templates/base.shtml @@ -35,6 +35,9 @@ + diff --git a/public/2026-04-19-one-of-the-best-skills-ive-learned-as-a-programmer/index.html b/public/2026-04-19-one-of-the-best-skills-ive-learned-as-a-programmer/index.html index 926364f..d14f6c9 100644 --- a/public/2026-04-19-one-of-the-best-skills-ive-learned-as-a-programmer/index.html +++ b/public/2026-04-19-one-of-the-best-skills-ive-learned-as-a-programmer/index.html @@ -36,6 +36,9 @@ + diff --git a/public/2026-04-30-how-i-manage-my-blog/index.html b/public/2026-04-30-how-i-manage-my-blog/index.html index 45c5c96..b116533 100644 --- a/public/2026-04-30-how-i-manage-my-blog/index.html +++ b/public/2026-04-30-how-i-manage-my-blog/index.html @@ -36,6 +36,9 @@ + diff --git a/public/2026-05-01-how-i-cut-my-expenses-by-a-freaking-lot/index.html b/public/2026-05-01-how-i-cut-my-expenses-by-a-freaking-lot/index.html index 499c828..ae0c693 100644 --- a/public/2026-05-01-how-i-cut-my-expenses-by-a-freaking-lot/index.html +++ b/public/2026-05-01-how-i-cut-my-expenses-by-a-freaking-lot/index.html @@ -36,6 +36,9 @@ + diff --git a/public/index.html b/public/index.html index 09d116f..03b89e4 100644 --- a/public/index.html +++ b/public/index.html @@ -36,6 +36,9 @@ + diff --git a/scripts/gen_rss.py b/scripts/gen_rss.py new file mode 100644 index 0000000..7f1e3af --- /dev/null +++ b/scripts/gen_rss.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Generate an RSS 2.0 feed from Zine blog posts. + +Parses the .smd frontmatter from content and writes public/rss.xml. + +Usage: + python3 scripts/gen_rss.py +""" + +import re +import sys +from pathlib import Path +from datetime import datetime, timezone +from xml.sax.saxutils import escape + +HOST = "https://frustrated-functor.dev" +SITE_TITLE = "Alexander Goussas" +SITE_DESCRIPTION = "Blog about programming and (human) languages" + + +def parse_smd(path: Path) -> dict | None: + """Extract frontmatter fields from a .smd file. + + Returns None for draft pages or files without valid frontmatter. + """ + text = path.read_text(encoding="utf-8") + m = re.match(r"^---\n(.*?)\n---", text, re.DOTALL) + if not m: + return None + front = m.group(1) + + def get(key: str) -> str: + hit = re.search(rf'\.{key}\s*=\s*"([^"]*)"', front) + return hit.group(1) if hit else "" + + def get_bool(key: str) -> bool: + hit = re.search(rf'\.{key}\s*=\s*(true|false)', front) + return bool(hit and hit.group(1) == "true") + + def get_date(key: str) -> str: + hit = re.search(rf'\.{key}\s*=\s*@date\("([^"]+)"\)', front) + return hit.group(1) if hit else "" + + if get_bool("draft"): + return None + + return { + "title": get("title"), + "description": get("description"), + "author": get("author"), + "date_iso": get_date("date"), + "layout": get("layout"), + } + + +def to_rfc822(iso: str) -> str: + """Convert an ISO 8601 datetime string to RFC 822 format for RSS.""" + try: + dt = datetime.fromisoformat(iso).replace(tzinfo=timezone.utc) + return dt.strftime("%a, %d %b %Y %H:%M:%S +0000") + except (ValueError, AttributeError): + return "" + + +def build_feed(posts: list[dict]) -> str: + """Render the complete RSS 2.0 XML string.""" + last_build = to_rfc822(posts[0]["date_iso"]) if posts else "" + + items = "" + for p in posts: + items += ( + "\n " + f"\n {escape(p['title'])}" + f"\n {p['url']}" + f"\n {p['url']}" + f"\n {escape(p['description'])}" + f"\n {escape(p['author'])}" + f"\n {to_rfc822(p['date_iso'])}" + "\n " + ) + + return ( + '\n' + '\n' + " \n" + f" {escape(SITE_TITLE)}\n" + f" {HOST}\n" + f" {escape(SITE_DESCRIPTION)}\n" + " en-us\n" + f' \n' + f" {last_build}" + f"{items}\n" + " \n" + "\n" + ) + + +def main() -> int: + content_dir = Path("content") + out_path = Path("public/rss.xml") + + if not content_dir.is_dir(): + print(f"ERROR: {content_dir} does not exist", file=sys.stderr) + return 1 + + posts = [] + for smd in content_dir.glob("*.smd"): + if smd.stem.startswith("index"): continue + + meta = parse_smd(smd) + if meta is None or not meta["title"]: + continue + meta["slug"] = smd.stem + meta["url"] = f"{HOST}/{smd.stem}/" + posts.append(meta) + + # Newest first + posts.sort(key=lambda p: p["date_iso"], reverse=True) + + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(build_feed(posts), encoding="utf-8") + print(f"rss.xml: {len(posts)} item(s) → {out_path}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())