]> git.frustrated-labs.net Git - frustrated-functor.dev.git/commitdiff
feat: add rss generator script
authorAlexander Goussas <[email protected]>
Mon, 18 May 2026 16:40:13 +0000 (11:40 -0500)
committerAlexander Goussas <[email protected]>
Mon, 18 May 2026 16:40:13 +0000 (11:40 -0500)
Copied from https://github.com/kassane/kassane.github.io/blob/main/tools/gen_rss.py

.gitignore
layouts/templates/base.shtml
public/2026-04-19-one-of-the-best-skills-ive-learned-as-a-programmer/index.html
public/2026-04-30-how-i-manage-my-blog/index.html
public/2026-05-01-how-i-cut-my-expenses-by-a-freaking-lot/index.html
public/index.html
scripts/gen_rss.py [new file with mode: 0644]

index 0bfa928d178312081ed7033228b49829001c9152..ff9037093f6fba51d76ae0f42117eaf653ac40c8 100644 (file)
@@ -3,3 +3,5 @@ zig-out
 !public/index.html
 public/*.html
 sample.rss
+public/*.rss
+public/*.xml
index 337d76e68fd1b4e0a20c12a0e85b75d131ae6e9c..e26e5ab4cf97919150895fef0b9856d585f158f6 100644 (file)
@@ -35,6 +35,9 @@
           <li class="nav-item" style="background-color: blue;">
             <a href="https://linkedin.com/in/alexander-goussas" target="_blank">linkedin</a>
           </li>
+          <li class="nav-item" style="background-color: red;">
+            <a href="/rss.xml" target="_blank">rss</a>
+          </li>
         </ul>
       </nav>
     </header>
index 926364f39cffc57c67d9191653cb4267d1cf97d3..d14f6c9965e475a38f3ddf7d17da183a563466ba 100644 (file)
@@ -36,6 +36,9 @@
           <li class="nav-item" style="background-color: blue;">
             <a href="https://linkedin.com/in/alexander-goussas" target="_blank">linkedin</a>
           </li>
+          <li class="nav-item" style="background-color: red;">
+            <a href="/rss.xml" target="_blank">rss</a>
+          </li>
         </ul>
       </nav>
     </header>
index 45c5c966ae0aa2ff6820eadbe9002f874f672446..b116533ea03da8fcedbf8269144e313f960b17a6 100644 (file)
@@ -36,6 +36,9 @@
           <li class="nav-item" style="background-color: blue;">
             <a href="https://linkedin.com/in/alexander-goussas" target="_blank">linkedin</a>
           </li>
+          <li class="nav-item" style="background-color: red;">
+            <a href="/rss.xml" target="_blank">rss</a>
+          </li>
         </ul>
       </nav>
     </header>
index 499c828a22da6690b6e0fa76568afe2b18385a24..ae0c693f0f44a5fc8305cece67e84ca9e3b461ff 100644 (file)
@@ -36,6 +36,9 @@
           <li class="nav-item" style="background-color: blue;">
             <a href="https://linkedin.com/in/alexander-goussas" target="_blank">linkedin</a>
           </li>
+          <li class="nav-item" style="background-color: red;">
+            <a href="/rss.xml" target="_blank">rss</a>
+          </li>
         </ul>
       </nav>
     </header>
index 09d116fee6ef69eb95996bfaa7d4082dcf245c54..03b89e4113ae6c3ab7b68f27de52ba8832e68656 100644 (file)
@@ -36,6 +36,9 @@
           <li class="nav-item" style="background-color: blue;">
             <a href="https://linkedin.com/in/alexander-goussas" target="_blank">linkedin</a>
           </li>
+          <li class="nav-item" style="background-color: red;">
+            <a href="/rss.xml" target="_blank">rss</a>
+          </li>
         </ul>
       </nav>
     </header>
diff --git a/scripts/gen_rss.py b/scripts/gen_rss.py
new file mode 100644 (file)
index 0000000..7f1e3af
--- /dev/null
@@ -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    <item>"
+            f"\n      <title>{escape(p['title'])}</title>"
+            f"\n      <link>{p['url']}</link>"
+            f"\n      <guid isPermaLink=\"true\">{p['url']}</guid>"
+            f"\n      <description>{escape(p['description'])}</description>"
+            f"\n      <author>{escape(p['author'])}</author>"
+            f"\n      <pubDate>{to_rfc822(p['date_iso'])}</pubDate>"
+            "\n    </item>"
+        )
+
+    return (
+        '<?xml version="1.0" encoding="UTF-8"?>\n'
+        '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n'
+        "  <channel>\n"
+        f"    <title>{escape(SITE_TITLE)}</title>\n"
+        f"    <link>{HOST}</link>\n"
+        f"    <description>{escape(SITE_DESCRIPTION)}</description>\n"
+        "    <language>en-us</language>\n"
+        f'    <atom:link href="{HOST}/rss.xml" rel="self" type="application/rss+xml"/>\n'
+        f"    <lastBuildDate>{last_build}</lastBuildDate>"
+        f"{items}\n"
+        "  </channel>\n"
+        "</rss>\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())