173 lines
4.9 KiB
Python
173 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Build script for The Scrolls — generates blog index and RSS feed from
|
|
markdown posts in blog/posts/.
|
|
|
|
Each post is a markdown file with YAML frontmatter:
|
|
---
|
|
title: "Post Title"
|
|
date: 2026-03-18
|
|
---
|
|
Body content here.
|
|
|
|
Generates:
|
|
- blog/index.html with post listing
|
|
- blog/feed.xml Atom feed
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
SITE_URL = "https://alexanderwhitestone.com"
|
|
BLOG_DIR = Path(__file__).parent.parent / "blog"
|
|
POSTS_DIR = BLOG_DIR / "posts"
|
|
|
|
|
|
def parse_frontmatter(text):
|
|
"""Extract YAML frontmatter from markdown text."""
|
|
match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)", text, re.DOTALL)
|
|
if not match:
|
|
return {}, text
|
|
|
|
meta = {}
|
|
for line in match.group(1).strip().split("\n"):
|
|
if ":" in line:
|
|
key, val = line.split(":", 1)
|
|
meta[key.strip()] = val.strip().strip('"').strip("'")
|
|
return meta, match.group(2).strip()
|
|
|
|
|
|
def load_posts():
|
|
"""Load and sort all posts by date (newest first)."""
|
|
posts = []
|
|
if not POSTS_DIR.exists():
|
|
return posts
|
|
|
|
for path in sorted(POSTS_DIR.glob("*.md"), reverse=True):
|
|
text = path.read_text()
|
|
meta, body = parse_frontmatter(text)
|
|
if meta.get("title") and meta.get("date"):
|
|
posts.append(
|
|
{
|
|
"title": meta["title"],
|
|
"date": meta["date"],
|
|
"slug": path.stem,
|
|
"body": body,
|
|
"path": path,
|
|
}
|
|
)
|
|
return posts
|
|
|
|
|
|
def generate_index(posts):
|
|
"""Generate blog/index.html with post listing."""
|
|
if not posts:
|
|
print(" No posts found. Keeping placeholder index.")
|
|
return
|
|
|
|
items = []
|
|
for p in posts:
|
|
items.append(
|
|
f' <li>\n'
|
|
f' <span class="date">{p["date"]}</span>\n'
|
|
f' <span class="title">{p["title"]}</span>\n'
|
|
f" </li>"
|
|
)
|
|
|
|
post_list = "\n".join(items)
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>The Scrolls — The Wizard's Tower</title>
|
|
<link rel="alternate" type="application/rss+xml" title="The Scrolls" href="/blog/feed.xml">
|
|
<style>
|
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
body {{
|
|
max-width: 640px; margin: 0 auto; padding: 2rem 1rem;
|
|
background: #0a0a0f; color: #e0d8c8;
|
|
font-family: Georgia, serif; line-height: 1.6;
|
|
}}
|
|
header {{ margin-bottom: 3rem; border-bottom: 1px solid #2a2520; padding-bottom: 1rem; }}
|
|
header h1 {{ font-size: 1.2rem; font-weight: normal; letter-spacing: 0.1em; }}
|
|
header nav {{ margin-top: 0.5rem; font-size: 0.8rem; }}
|
|
header a {{ color: #8a7f6a; text-decoration: none; }}
|
|
header a:hover {{ color: #e0d8c8; }}
|
|
.posts {{ list-style: none; }}
|
|
.posts li {{ margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #1a1510; }}
|
|
.posts .date {{ font-size: 0.8rem; color: #6a6050; display: block; }}
|
|
.posts .title {{ font-size: 1rem; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>The Scrolls</h1>
|
|
<nav><a href="/">← The Tower</a> · <a href="/blog/feed.xml">RSS</a></nav>
|
|
</header>
|
|
<main>
|
|
<ul class="posts">
|
|
{post_list}
|
|
</ul>
|
|
</main>
|
|
</body>
|
|
</html>"""
|
|
|
|
(BLOG_DIR / "index.html").write_text(html)
|
|
print(f" Generated index with {len(posts)} post(s).")
|
|
|
|
|
|
def generate_feed(posts):
|
|
"""Generate blog/feed.xml Atom feed."""
|
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
entries = []
|
|
for p in posts[:20]: # Cap at 20 entries
|
|
# Simple HTML conversion: wrap paragraphs
|
|
html_body = "\n".join(
|
|
f"<p>{para}</p>" for para in p["body"].split("\n\n") if para.strip()
|
|
)
|
|
entries.append(
|
|
f""" <entry>
|
|
<title>{p["title"]}</title>
|
|
<link href="{SITE_URL}/blog/posts/{p["slug"]}.html"/>
|
|
<id>{SITE_URL}/blog/posts/{p["slug"]}</id>
|
|
<updated>{p["date"]}T00:00:00Z</updated>
|
|
<content type="html"><![CDATA[{html_body}]]></content>
|
|
</entry>"""
|
|
)
|
|
|
|
entry_block = "\n".join(entries)
|
|
|
|
feed = f"""<?xml version="1.0" encoding="utf-8"?>
|
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
<title>The Scrolls — Alexander Whitestone</title>
|
|
<link href="{SITE_URL}/blog/"/>
|
|
<link rel="self" href="{SITE_URL}/blog/feed.xml"/>
|
|
<id>{SITE_URL}/blog/</id>
|
|
<updated>{now}</updated>
|
|
<author>
|
|
<name>Alexander Whitestone</name>
|
|
</author>
|
|
<subtitle>Words from the Wizard's Tower</subtitle>
|
|
{entry_block}
|
|
</feed>"""
|
|
|
|
(BLOG_DIR / "feed.xml").write_text(feed)
|
|
print(f" Generated feed with {len(entries)} entry/entries.")
|
|
|
|
|
|
def main():
|
|
print("Building The Scrolls...")
|
|
posts = load_posts()
|
|
generate_index(posts)
|
|
generate_feed(posts)
|
|
print("Build complete.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|