Files
blog/build.py
2026-02-15 19:28:37 -06:00

205 lines
6.1 KiB
Python

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "pyyaml",
# "markdown",
# ]
# ///
"""Build script for Quiet's Blog.
Reads Markdown files with YAML frontmatter from content/ and generates
static HTML in dist/ using templates from templates/.
"""
import glob
import os
import shutil
import markdown
import yaml
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CONTENT_DIR = os.path.join(BASE_DIR, "content")
TEMPLATE_DIR = os.path.join(BASE_DIR, "templates")
STATIC_DIR = os.path.join(BASE_DIR, "static")
DIST_DIR = os.path.join(BASE_DIR, "dist")
def parse_post(filepath):
"""Parse a Markdown file with YAML frontmatter. Returns dict with
title, date, description, content (HTML), and source path info."""
with open(filepath, "r") as f:
text = f.read()
if not text.startswith("---"):
raise ValueError(f"Missing frontmatter in {filepath}")
_, fm_raw, body = text.split("---", 2)
meta = yaml.safe_load(fm_raw)
md = markdown.Markdown(extensions=["fenced_code", "tables"])
html_content = md.convert(body.strip())
slug = os.path.splitext(os.path.basename(filepath))[0]
return {
"title": meta["title"],
"date": str(meta["date"]),
"description": meta.get("description", ""),
"content": html_content,
"slug": slug,
"source": filepath,
}
def load_template(name):
path = os.path.join(TEMPLATE_DIR, name)
with open(path, "r") as f:
return f.read()
def render(template_str, **kwargs):
result = template_str
for key, value in kwargs.items():
result = result.replace("{{" + key + "}}", str(value))
return result
def collect_posts(subdir):
"""Collect and parse all .md files in content/<subdir>/."""
pattern = os.path.join(CONTENT_DIR, subdir, "*.md")
posts = []
for filepath in sorted(glob.glob(pattern)):
post = parse_post(filepath)
post["category"] = subdir
posts.append(post)
posts.sort(key=lambda p: p["date"], reverse=True)
return posts
def build_post_page(post, template, output_dir, root_prefix):
"""Render a single post page and write it to output_dir."""
os.makedirs(output_dir, exist_ok=True)
html = render(
template,
title=post["title"],
date=post["date"],
content=post["content"],
css_path=root_prefix + "styles.css",
root=root_prefix,
)
out_path = os.path.join(output_dir, post["slug"] + ".html")
with open(out_path, "w") as f:
f.write(html)
return out_path
def build_post_card(post, href):
"""Generate HTML for a post card on the index page."""
return (
f' <article class="post-card">\n'
f' <h3>{post["title"]}</h3>\n'
f' <p class="post-date">{post["date"]}</p>\n'
f' <p>{post["description"]}</p>\n'
f' <a href="{href}" class="read-more">Read More &rarr;</a>\n'
f" </article>"
)
def build_list_section(title, posts, url_prefix):
"""Generate HTML for a section in the posts listing page."""
if not posts:
return ""
items = "\n".join(
f' <li><a href="{url_prefix}/{p["slug"]}.html">{p["title"]}</a> '
f'<span class="post-date">{p["date"]}</span></li>'
for p in posts
)
return (
f" <h3>{title}</h3>\n"
f" <ul>\n{items}\n"
f" </ul>"
)
def main():
# Clean dist
if os.path.exists(DIST_DIR):
shutil.rmtree(DIST_DIR)
os.makedirs(DIST_DIR)
# Load templates
post_template = load_template("post.html")
index_template = load_template("index.html")
posts_template = load_template("posts.html")
about_template = load_template("about.html")
# Collect posts by category
regular_posts = collect_posts("posts")
daily_posts = collect_posts("daily")
weekly_posts = collect_posts("weekly")
# Build individual post pages
for post in regular_posts:
build_post_page(post, post_template, os.path.join(DIST_DIR, "posts"), "../")
for post in daily_posts:
build_post_page(post, post_template, os.path.join(DIST_DIR, "daily"), "../")
for post in weekly_posts:
build_post_page(post, post_template, os.path.join(DIST_DIR, "weekly"), "../")
# Build index page with latest posts across all categories
all_posts = regular_posts + daily_posts + weekly_posts
all_posts.sort(key=lambda p: p["date"], reverse=True)
def post_href(post):
return f'{post["category"]}/{post["slug"]}.html'
post_cards = "\n\n".join(
build_post_card(p, post_href(p)) for p in all_posts
)
index_html = render(index_template, post_cards=post_cards)
with open(os.path.join(DIST_DIR, "index.html"), "w") as f:
f.write(index_html)
# Build posts listing page
daily_section = build_list_section("Daily Posts", daily_posts, "daily")
weekly_section = build_list_section("Weekly Posts", weekly_posts, "weekly")
posts_section = build_list_section("Posts", regular_posts, "posts")
posts_html = render(
posts_template,
daily_section=daily_section,
weekly_section=weekly_section,
posts_section=posts_section,
)
with open(os.path.join(DIST_DIR, "posts.html"), "w") as f:
f.write(posts_html)
# Build about page
about_post = parse_post(os.path.join(CONTENT_DIR, "about.md"))
about_html = render(
about_template,
title=about_post["title"],
date=about_post["date"],
content=about_post["content"],
)
with open(os.path.join(DIST_DIR, "about.html"), "w") as f:
f.write(about_html)
# Copy static files
shutil.copy2(
os.path.join(STATIC_DIR, "styles.css"),
os.path.join(DIST_DIR, "styles.css"),
)
# Summary
total = len(regular_posts) + len(daily_posts) + len(weekly_posts)
print(f"Built {total} posts ({len(regular_posts)} regular, "
f"{len(daily_posts)} daily, {len(weekly_posts)} weekly)")
print(f"Output: {DIST_DIR}/")
if __name__ == "__main__":
main()