···11+# Leave unset on Cloudflare Pages when using the included same-origin functions.
22+# Set this for purely static hosts that use an external feed aggregation endpoint.
33+# VITE_FEED_API_URL=https://your-proxy.example.com/api/feed
44+55+# Set this for purely static hosts that need an external Lobsters discussion proxy.
66+# VITE_API_PROXY_URL=https://your-proxy.example.com/api/lobsters
···11+# Threadline
22+33+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).
44+55+Preferences are stored only in `localStorage`. The storage key remains `hnster.preferences.v1` intentionally for compatibility with older builds.
66+77+## Development
88+99+This repository uses pnpm and includes a Nix development shell:
1010+1111+```bash
1212+nix develop # supplies Node 22 and pnpm
1313+pnpm install
1414+pnpm dev
1515+```
1616+1717+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.
2121+2222+```bash
2323+pnpm typecheck
2424+pnpm test
2525+pnpm build
2626+```
2727+2828+## 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.
···11+import { Link } from 'react-router-dom';
22+33+export function NotFoundPage() {
44+ return <main className="empty-state"><h1>Not found</h1><Link to="/">Return to front page</Link></main>;
55+}