···11# Threadline
2233-Threadline is a static-first, local-preference reader for community news sources. It currently combines Hacker News and Lobsters, with source metadata and feed contracts organized so additional sources can be added later. The app is a React single-page app with routes for `/` (feed), `/story/:source/:id` (discussion), and `/settings` (preferences).
33+Threadline is a compact, static-first reader for community news. It combines Hacker News and Lobsters into one local, customizable front page with fast feed loading, source filters, discussion reading, and an optional inline article reader on larger screens.
44+55+Preferences live in `localStorage`; no account or backend database is required.
66+77+## Screenshots
88+99+### List layout
1010+1111+
1212+1313+### Grid layout, sepia theme
1414+1515+
1616+1717+### Inline reader and discussion
1818+1919+
2020+2121+## Features
42255-Preferences are stored only in `localStorage`. The storage key remains `hnster.preferences.v1` intentionally for compatibility with older builds.
2323+- Combined Hacker News and Lobsters feed
2424+- List and newspaper-style grid layouts
2525+- Light, dark, and sepia themes
2626+- Local source, ranking, domain, and Lobsters tag preferences
2727+- Nested discussion view with collapse and comment navigation
2828+- Inline article reader on desktop/tablet
2929+- Static React app deployable on Cloudflare Pages
3030+- Cloudflare Pages Functions for feed aggregation and Lobsters CORS proxy
631732## Development
83399-This repository uses pnpm and includes a Nix development shell:
1010-1134```bash
1212-nix develop # supplies Node 22 and pnpm
3535+nix develop
1336pnpm install
1437pnpm dev
1538```
16391717-The Vite server mirrors the Cloudflare API surface in development:
1818-1919-- `/api/feed` fetches and normalizes the combined front page.
2020-- `/api/lobsters/*` proxies Lobsters JSON discussion/feed endpoints for CORS-safe comment loading.
4040+Checks:
21412242```bash
2343pnpm typecheck
···2545pnpm build
2646```
27472828-## Feed architecture
2929-3030-The browser first requests a same-origin aggregated feed:
3131-3232-```text
3333-GET /api/feed?hn=top&lobsters=hottest
3434-```
3535-3636-The Cloudflare Pages Function fetches Hacker News and Lobsters HTML front pages, parses the visible story metadata, and returns a normalized `FeedResult`. If scraping fails or returns suspiciously few stories, it falls back to the public APIs:
3737-3838-- Hacker News Firebase item API
3939-- Lobsters JSON feed API
4040-4141-Partial failures return the successful source plus a per-source error so the UI can still render available stories.
4242-4343-For purely static hosts without the feed aggregation function, the browser falls back to public feed APIs directly. Hacker News can work in that mode, but Lobsters discussions still require a CORS-safe proxy (`VITE_API_PROXY_URL`) unless the app is deployed with the included Cloudflare Pages Function.
4444-4545-## Lobsters proxy and deployment
4646-4747-Lobsters does not reliably permit browser cross-origin requests for discussions. The client therefore calls a proxy base, not Lobsters directly:
4848-4949-- Default: same-origin `/api/lobsters`, implemented by `functions/api/lobsters/[[path]].ts` on Cloudflare Pages.
5050-- External: set `VITE_API_PROXY_URL` at **build time** to a full proxy base, for example `https://reader-proxy.example.workers.dev/api/lobsters`. Do not include a trailing feed path.
5151-5252-The proxy only forwards safe endpoint forms:
5353-5454-- `hottest.json`
5555-- `newest.json`
5656-- `s/<id>.json`
5757-5858-## Optional environment variables
5959-6060-- `VITE_FEED_API_URL`: full URL for an external compatible `/api/feed` endpoint.
6161-- `VITE_API_PROXY_URL`: full URL for an external compatible `/api/lobsters` endpoint.
6262-6363-Leave both unset on Cloudflare Pages when using the included same-origin functions.
6464-6565-## Cloudflare Pages
6666-6767-Use build command `pnpm build` and output directory `dist`. The repo includes `public/_redirects`, copied into `dist`, to serve `index.html` for non-asset routes so direct visits to `/settings` and `/story/...` work. Cloudflare Pages Functions under `/api/*` are handled separately from this static SPA fallback.