#!/usr/bin/env python3 """Build static HTML blog site from markdown posts in data/reports/blog-series/.""" import re from pathlib import Path import markdown ROOT = Path(__file__).resolve().parent.parent POSTS_DIR = ROOT / "data" / "reports" / "blog-series" OUT_DIR = ROOT / "docs" / "blog" CSS_DIR = OUT_DIR / "css" POSTS_OUT = OUT_DIR / "posts" # Ordered list of posts to include POSTS = [ ("00-series-overview.md", "Series Overview"), ("01-gold-rush.md", "The Gold Rush"), ("02-who-writes-the-rules.md", "Who Writes the Rules"), ("03-oauth-wars.md", "The OAuth Wars"), ("04-what-nobody-builds.md", "What Nobody Builds"), ("05-1262-ideas.md", "Where Drafts Converge"), ("06-big-picture.md", "The Big Picture"), ("07-how-we-built-this.md", "How We Built This"), ("08-agents-building-the-analysis.md", "Agents Building the Agent Analysis"), ] CSS = """\ :root { --bg: #ffffff; --text: #1a1a1a; --muted: #6b7280; --border: #e5e7eb; --accent: #2563eb; --code-bg: #f3f4f6; } @media (prefers-color-scheme: dark) { :root { --bg: #111827; --text: #e5e7eb; --muted: #9ca3af; --border: #374151; --accent: #60a5fa; --code-bg: #1f2937; } } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; color: var(--text); background: var(--bg); line-height: 1.7; font-size: 17px; } .container { max-width: 720px; margin: 0 auto; padding: 2rem 1.5rem; } nav { border-bottom: 1px solid var(--border); padding: 1rem 0; margin-bottom: 2rem; } nav a { color: var(--accent); text-decoration: none; margin-right: 1.5rem; font-size: 0.9rem; } nav a:hover { text-decoration: underline; } nav .site-title { font-weight: 700; font-size: 1.1rem; } h1 { font-size: 2rem; margin: 1.5rem 0 1rem; line-height: 1.2; } h2 { font-size: 1.5rem; margin: 2rem 0 0.75rem; } h3 { font-size: 1.2rem; margin: 1.5rem 0 0.5rem; } p { margin: 0.75rem 0; } a { color: var(--accent); } blockquote { border-left: 3px solid var(--accent); padding-left: 1rem; color: var(--muted); margin: 1rem 0; } code { background: var(--code-bg); padding: 0.15rem 0.4rem; border-radius: 3px; font-size: 0.9em; } pre { background: var(--code-bg); padding: 1rem; border-radius: 6px; overflow-x: auto; margin: 1rem 0; } pre code { background: none; padding: 0; } table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.95rem; } th, td { padding: 0.5rem 0.75rem; border: 1px solid var(--border); text-align: left; } th { background: var(--code-bg); font-weight: 600; } ul, ol { padding-left: 1.5rem; margin: 0.75rem 0; } li { margin: 0.25rem 0; } .post-nav { display: flex; justify-content: space-between; margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: 0.9rem; } .post-list { list-style: none; padding: 0; } .post-list li { margin: 1rem 0; } .post-list a { font-size: 1.1rem; font-weight: 500; } .post-list .desc { color: var(--muted); font-size: 0.9rem; } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.85rem; } """ def slug(filename: str) -> str: return filename.replace(".md", ".html") def build_nav(current: str = "") -> str: links = ['IETF AI Agent Analysis'] for fn, title in POSTS[1:]: # skip overview in nav s = slug(fn) if fn == current: links.append(f"{title.split()[-1]}") else: links.append(f'{title.split()[-1]}') return "" def build_post_nav(idx: int) -> str: parts = [] if idx > 0: prev_fn, prev_title = POSTS[idx - 1] parts.append(f'← {prev_title}') else: parts.append("") if idx < len(POSTS) - 1: next_fn, next_title = POSTS[idx + 1] parts.append(f'{next_title} →') else: parts.append("") return f'
{parts[0]}{parts[1]}
' def wrap_html(title: str, body: str, nav: str, post_nav: str = "") -> str: return f""" {title} — IETF AI Agent Analysis
{nav} {body} {post_nav}
""" def extract_title(md_text: str) -> str: """Extract first H1 from markdown.""" m = re.search(r"^#\s+(.+)$", md_text, re.MULTILINE) return m.group(1) if m else "Untitled" def main(): md_ext = markdown.Markdown(extensions=["tables", "fenced_code", "toc"]) # Create output dirs CSS_DIR.mkdir(parents=True, exist_ok=True) POSTS_OUT.mkdir(parents=True, exist_ok=True) # Write CSS (CSS_DIR / "style.css").write_text(CSS) # Write .nojekyll (OUT_DIR / ".nojekyll").write_text("") # Build each post for idx, (fn, fallback_title) in enumerate(POSTS): src = POSTS_DIR / fn if not src.exists(): print(f" SKIP {fn} (not found)") continue md_text = src.read_text() md_ext.reset() html_body = md_ext.convert(md_text) title = extract_title(md_text) or fallback_title nav = build_nav(fn) post_nav = build_post_nav(idx) full_html = wrap_html(title, html_body, nav, post_nav) out_path = POSTS_OUT / slug(fn) out_path.write_text(full_html) print(f" BUILT {out_path.relative_to(ROOT)}") # Build index page post_links = [] for i, (fn, title) in enumerate(POSTS): if i == 0: continue # skip overview in index list post_links.append( f'
  • Post {i}: {title}
  • ' ) index_body = f"""

    The AI Agent Standards Gold Rush

    A data-driven analysis of {475} IETF Internet-Drafts on AI agents, autonomous systems, and machine learning protocols.

    The IETF is experiencing an unprecedented surge in AI/agent standardization activity. We built an automated analysis pipeline to make sense of it: {713} authors, {501} ideas, {132} cross-organizational convergent ideas, and {12} identified gaps.

    The Series

    About

    This analysis was produced using the IETF Draft Analyzer, an open-source Python tool that combines Claude for multi-dimensional rating and idea extraction with Ollama for semantic embeddings. Total API cost: ~$9-15.

    Read the series overview →

    """ index_html = wrap_html("Home", index_body, build_nav()) (OUT_DIR / "index.html").write_text(index_html) print(f" BUILT docs/blog/index.html") print(f"\nDone. {len(POSTS) + 1} pages built in docs/blog/") if __name__ == "__main__": main()