krz/hn

clone: git clone https://gitbay.org/krz/hn.git

main: hn.py · raw

  1#!/usr/bin/env python3
  2"""
  3Static Hacker News page generator.
  4
  5* Top stories → output/index.html (site root)
  6* Best, New, Ask, Show, Job → output/<section>/index.html
  7"""
  8
  9import json
 10import urllib.request
 11from pathlib import Path
 12from datetime import datetime
 13from typing import List, Dict
 14
 15BASE_URL = "https://hacker-news.firebaseio.com/v0"
 16OUTPUT_DIR = Path(__file__).parent / "output"
 17TEMPLATE_PATH = Path(__file__).parent / "templates" / "base.html"
 18
 19
 20# ----------------------------------------------------------------------
 21# Helper functions
 22# ----------------------------------------------------------------------
 23def fetch_json(url: str) -> dict:
 24    """GET a JSON endpoint and return the parsed object."""
 25    with urllib.request.urlopen(url) as resp:
 26        return json.load(resp)
 27
 28
 29def get_story_ids(endpoint: str, limit: int = 10) -> List[int]:
 30    """Return the first ``limit`` IDs for a given endpoint."""
 31    url = f"{BASE_URL}/{endpoint}.json"
 32    all_ids = fetch_json(url)
 33    return all_ids[:limit]
 34
 35
 36def get_item(item_id: int) -> Dict:
 37    """Fetch a single Hacker News item."""
 38    url = f"{BASE_URL}/item/{item_id}.json"
 39    return fetch_json(url)
 40
 41
 42def render_page(title: str, items_html: str, build_time: str) -> str:
 43    """
 44    Insert title, items, and the build timestamp into the base template.
 45    """
 46    template = TEMPLATE_PATH.read_text(encoding="utf-8")
 47    rendered = (
 48        template
 49        .replace("{{title}}", title)
 50        .replace("{{items}}", items_html)
 51        .replace("{{build}}", build_time)
 52    )
 53    return rendered
 54
 55
 56def build_list_item(story: Dict) -> str:
 57    """Turn a story dict into a single <li> element."""
 58    url = story.get("url") or f"https://news.ycombinator.com/item?id={story['id']}"
 59    title = story.get("title", "(no title)")
 60    score = story.get("score", 0)
 61    by = story.get("by", "unknown")
 62    return f'<li><a href="{url}">{title}</a> ({score} points) by {by}</li>'
 63
 64
 65# ----------------------------------------------------------------------
 66# Main generation logic
 67# ----------------------------------------------------------------------
 68def generate_static_pages():
 69    # Make sure the top‑level output folder exists
 70    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
 71
 72    # One timestamp for the whole run
 73    build_timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
 74
 75    # Mapping: endpoint → (human‑readable title, sub‑folder name or None for root)
 76    sections = {
 77        "topstories": ("Top Stories", None),      # None → write to root index.html
 78        "beststories": ("Best Stories", "best"),
 79        "newstories": ("New Stories", "new"),
 80        "askstories": ("Ask HN", "ask"),
 81        "showstories": ("Show HN", "show"),
 82        "jobstories": ("Jobs", "job"),
 83    }
 84
 85    for endpoint, (title, subdir) in sections.items():
 86        print(f"Fetching {title}")
 87        ids = get_story_ids(endpoint, limit=10)
 88        stories = [get_item(i) for i in ids]
 89
 90        items_html = "\n".join(build_list_item(s) for s in stories)
 91
 92        page_html = render_page(title, items_html, build_timestamp)
 93
 94        # Determine where to write the file
 95        if subdir is None:
 96            target_path = OUTPUT_DIR / "index.html"
 97        else:
 98            target_dir = OUTPUT_DIR / subdir
 99            target_dir.mkdir(parents=True, exist_ok=True)
100            target_path = target_dir / "index.html"
101
102        target_path.write_text(page_html, encoding="utf-8")
103        print(f" → wrote {target_path}")
104
105    print("All pages generated (built at", build_timestamp, ")")
106
107
108if __name__ == "__main__":
109    generate_static_pages()