{"id":51331626,"url":"https://github.com/ryanlewis/hn-summaries","last_synced_at":"2026-07-01T23:04:59.176Z","repository":{"id":366254941,"uuid":"1275613129","full_name":"ryanlewis/hn-summaries","owner":"ryanlewis","description":"AI-summarized RSS feed of Hacker News's best stories — each entry summarizes the article plus the HN discussion, with links to both. Live at hn.rlew.io","archived":false,"fork":false,"pushed_at":"2026-06-21T00:27:55.000Z","size":51,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-21T02:09:54.777Z","etag":null,"topics":["ai","anthropic","claude","hacker-news","hackernews","llm","news","nodejs","rss","rss-feed","summarization","typescript"],"latest_commit_sha":null,"homepage":"https://hn.rlew.io","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ryanlewis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-20T23:33:04.000Z","updated_at":"2026-06-21T00:27:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ryanlewis/hn-summaries","commit_stats":null,"previous_names":["ryanlewis/hn-summaries"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/ryanlewis/hn-summaries","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlewis%2Fhn-summaries","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlewis%2Fhn-summaries/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlewis%2Fhn-summaries/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlewis%2Fhn-summaries/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ryanlewis","download_url":"https://codeload.github.com/ryanlewis/hn-summaries/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlewis%2Fhn-summaries/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35025993,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","anthropic","claude","hacker-news","hackernews","llm","news","nodejs","rss","rss-feed","summarization","typescript"],"created_at":"2026-07-01T23:04:57.000Z","updated_at":"2026-07-01T23:04:59.169Z","avatar_url":"https://github.com/ryanlewis.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# hn-summaries\n\nAn AI-summarized RSS feed of [Hacker News's \"best\"](https://news.ycombinator.com/best) stories. Every entry is a short summary of the **article *and* the HN discussion**, with links to both — a drop-in upgrade over [`hnrss.org/best`](https://hnrss.org/best) that tells you what a story is about before you click.\n\n**🔗 Live:** **\u003chttps://hn.rlew.io/feed\u003e** (paste into your RSS reader) · landing page at **\u003chttps://hn.rlew.io/\u003e**\n\n---\n\n## Query parameters\n\n| Param | Default | Notes |\n|---|---|---|\n| `?sort=date\\|points` | `date` | `date` = newest summary first, a rolling stream that keeps stories for a few days after they leave the best list. `points` = the live HN best-list rank (on-list only); a story drops out the moment it leaves the list, and each item is labelled with its rank and flagged when near the bottom. |\n| `?count=N` | `30` | How many stories to include (max `200`). |\n| `?min_points=N` | `0` | Only include stories with at least N points. |\n\nExamples: [`/feed?sort=points`](https://hn.rlew.io/feed?sort=points), [`/feed?count=10`](https://hn.rlew.io/feed?count=10), [`/feed?min_points=300`](https://hn.rlew.io/feed?min_points=300), `/feed?sort=points\u0026count=15\u0026min_points=200`.\n\n## How it works\n\n```mermaid\nflowchart TD\n    HN[\"HN Firebase API\"] --\u003e Fetch[\"fetch best IDs + stories + top comments\"]\n    Fetch --\u003e Extract[\"fetch \u0026amp; extract article text\u003cbr/\u003e(Readability/jsdom)\"]\n    Extract --\u003e|\"non-HTML / paywall / no URL\"| Fallback[\"fall back to the discussion\"]\n    Extract --\u003e Summarize[\"summarize\u003cbr/\u003e(exe.dev ChatGPT/Codex proxy — gpt-5.5)\"]\n    Fallback --\u003e Summarize\n    Summarize --\u003e Cache[\"JSON cache\u003cbr/\u003e(data/cache.json)\"]\n    Cache --\u003e Feed[\"/feed (RSS 2.0)\"]\n    Cache --\u003e Landing[\"/ (HTML landing)\"]\n```\n\nA single long-running Bun process refreshes the best list **hourly**, summarizing only stories it hasn't seen before, and serves the feed from an in-memory + on-disk cache. A story that temporarily drops off the best list keeps its summary, so it isn't re-summarized when it bounces back; it's dropped once it's been off the list past the retention window (`OFFLIST_RETENTION_MS`). A hard ceiling (`MAX_CACHE_STORIES`) caps total cache size as a backstop — on-list stories are never evicted, the oldest off-list summaries go first.\n\nArticle text is extracted in tiers: a plain fetch + [Readability](https://github.com/mozilla/readability), then — only on a recoverable failure — a headless-browser render (Chromium via `Bun.WebView`) for JS-heavy pages, and finally a discussion-only fallback. Stories stuck on the fallback are re-extracted on later cycles (a bounded self-healing pass), so a page that was transiently down or needs JS recovers without a manual nudge.\n\nSummaries are generated through the exe.dev internal proxies, which authenticate the VM automatically — **no API key is stored anywhere**. Two backends are selectable via `SUMMARY_PROVIDER`: the [ChatGPT/Codex proxy](https://exe.dev/docs/integrations-github) (`gpt-5.5`, default — draws on the ChatGPT subscription rather than the metered token allowance) or the [LLM gateway](https://exe.dev/docs/shelley/llm-gateway) (`claude-sonnet-4-6`).\n\n### Endpoints\n\n| Path | Description |\n|---|---|\n| `/feed` | RSS 2.0 feed (`?sort`, `?count`, `?min_points`). Also `/feed.xml`. |\n| `/` | HTML landing page: usage + latest 5 stories, with a Newest/Top-by-points toggle (`?sort`). |\n| `/healthz` | Liveness + cached story count. |\n| `/status` | Last refresh time + duration, next-refresh ETA, cache size (total / on-list / off-list / cap), last prune + eviction counts, last error, and a fallback breakdown (count/percent + tally by reason). |\n\n## Running locally\n\nRequires [Bun](https://bun.sh) ≥1.3.12 (pinned to 1.3.14 — `Bun.WebView` powers the browser extraction tier). Bun runs the TypeScript directly: no build step, no bundler, no `tsx`. Summarization needs to run on an exe.dev VM (for the keyless proxies) — or point the endpoints at your own OpenAI/Anthropic-compatible services. The browser tier additionally needs a Chrome/Chromium binary — install one with `bun run install-browser` (Playwright's Chromium), put one on `$PATH`, or point `BUN_CHROME_PATH` at it; the app auto-resolves whichever it finds at startup. Disable the tier with `BROWSER_FALLBACK_ENABLED=false`.\n\n```bash\nbun install\nbun start            # bun index.ts — serves on :8000, runs the first refresh on boot\nbun run typecheck    # tsc --noEmit\n```\n\nThe first boot summarizes the full best list (~200 stories, a few minutes); `/feed` returns `503` until the cache has entries. The cache persists to `data/cache.json` (gitignored), so restarts are instant.\n\n### Configuration\n\nEnvironment variables:\n\n| Var | Default | Purpose |\n|---|---|---|\n| `PORT` | `8000` | Listen port. |\n| `PUBLIC_URL` | `https://hn.rlew.io` | Canonical origin used in the feed's self-link and the landing page. |\n| `SUMMARY_PROVIDER` | `openai-responses` | Backend: `openai-responses` (ChatGPT/Codex proxy) or `anthropic` (LLM gateway). |\n| `OPENAI_ENDPOINT` / `OPENAI_MODEL` | ChatGPT proxy · `gpt-5.5` | Used when provider is `openai-responses`. |\n| `LLM_ENDPOINT` / `LLM_MODEL` | LLM gateway · `claude-sonnet-4-6` | Used when provider is `anthropic`. |\n\nEverything else — refresh interval, concurrency, article-size caps, per-refresh cost cap, off-list retention, cache size cap, comment count — lives in [`src/config.ts`](src/config.ts).\n\n## Project layout\n\n```\nindex.ts             entrypoint: start server, refresh on boot, schedule hourly\nsrc/config.ts        all checked-in tunables\nsrc/options.ts       local (gitignored) per-deployment options, e.g. \u003chead\u003e injection\nsrc/hn.ts            Hacker News Firebase API client\nsrc/extract.ts       article fetch (content-type/size guards) + Readability; HTML→text\nsrc/extract-browser.ts  headless-browser (Bun.WebView) extraction fallback tier\nsrc/summarize.ts     summarization backends (ChatGPT proxy + LLM gateway), prompts, retry\nsrc/cache.ts         JSON cache (in-memory singleton, atomic write, prune)\nsrc/refresh.ts       refresh pipeline (bounded concurrency, fallback-retry pass)\nsrc/feed.ts          RSS 2.0 rendering\nsrc/page.ts          HTML landing page\nsrc/html.ts          shared rendering helpers (escaping, domain, stats)\nsrc/server.ts        node:http server + static favicon assets\npublic/              favicons (orange \"AI\" mark)\nhn-summaries.service systemd unit\n```\n\n## Deployment\n\nRuns as a `systemd` service (`hn-summaries.service`) on an exe.dev VM, listening on `:8000`, published through the exe.dev HTTPS proxy with a `CNAME` for `hn.rlew.io` (TLS auto-issued). The hourly refresh runs in-process — no external cron.\n\n```bash\nsudo cp hn-summaries.service /etc/systemd/system/\nsudo systemctl enable --now hn-summaries\njournalctl -u hn-summaries -f\n```\n\n---\n\nStory content © its respective authors; summaries are AI-generated and may contain errors.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryanlewis%2Fhn-summaries","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fryanlewis%2Fhn-summaries","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryanlewis%2Fhn-summaries/lists"}