# furst-serve A local reader server. It fetches pages, strips them to what you came for, and serves them from `127.0.0.1` as small documents with no scripts, no stylesheets, no webfonts and no third-party requests. The point is the link rewriting. Every link on a served page points back at `/read?u=…`, so following one keeps you in the reader instead of dropping you onto the live site. Combined with the disk cache, that is what makes browsing on a Core2 feel immediate. ## Use ```sh furst-serve # run in the foreground on 127.0.0.1:7714 furst-serve --open # start it if needed, then open in a browser furst-serve --status furst-serve --init # write a starter home.toml furst-serve --clear # drop the page cache ``` `--open` is what [furst](../README.md)'s catch-all rule calls. It starts a detached server when nothing is listening, waits for the port, then execs `$FURST_BROWSER`, `$BROWSER`, or the first light browser on `$PATH`. ## Routes | | | |---|---| | `/` | home: pinned links and feeds, and the search box | | `/read?u=` | the page — article, site view, or link index | | `/search?q=` | search results | | `/feed?u=` | an RSS or Atom feed | | `/go?u=` | hand the original URL to the heavy browser | Add `&fresh=1` to `/read` or `/feed` to bypass the cache. Every page carries **refresh**, **original** and **browser** links in the bar, so nothing is a dead end. ## What it does with a page 1. **A feed** — rendered as a list of entries, whether it was requested at `/feed` or just turned out to be XML. 2. **A site with an adapter** — Hacker News gets stories with score and author, and comment threads with their indent preserved. 3. **An article** — extracted by [furst-read](../furst-read/README.md). 4. **Anything else** — if there is too little prose to be an article, the page's links are listed instead. A front page has no article to find, and saying so beats rendering an empty column. Non-documents (a PDF, an image) redirect to the original rather than being fed to an article extractor. ## Configuration `~/.config/furst/home.toml`: ```toml [[link]] name = "Arch Wiki" url = "https://wiki.archlinux.org/" [[feed]] name = "LWN" url = "https://lwn.net/headlines/newrss" [search] url = "https://html.duckduckgo.com/html/?q={q}" ``` | Environment | | |---|---| | `FURST_PORT` | default 7714 | | `FURST_BROWSER` / `BROWSER` | browser for `--open` | | `FURST_HEAVY_BROWSER` | the `/go` target, default `firefox` | | `XDG_CACHE_HOME` | pages land in `furst/pages` under it | ## On search Search is engine-agnostic on purpose. **Every free HTML endpoint eventually rate-limits a repeat visitor** — DuckDuckGo and searx.be both served this machine a challenge page while it was being written. So: - Known result shapes (DuckDuckGo, SearXNG) are tried first, then heading links, then any link. An unknown engine that serves plain HTML still works. - A challenge page is detected from the body, not the status, because they arrive as 200 or 202 rather than an error. You get a page saying which engine refused and where to change it, never a silently empty result list. - Blocked searches are not cached. If your engine rate-limits you, point `[search] url` at a SearXNG instance. Engines that render results client-side cannot work here — there is no JavaScript, by design. ## Design notes The HTTP layer is blocking, hand-rolled, GET only, one response per connection, with a fixed pool of four worker threads. No async runtime: predictable memory matters more than concurrency on a 4GB machine. Only the loopback interface is ever bound. Cache entries carry a schema number, so changing the renderer drops them rather than serving stale markup.