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