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()