Furst/furst-serve/README.md
nak0x b1d052fc79 Make the reader the default route and document the toolset
The catch-all rule now sends anything that did not match a more specific
rule to furst-serve, while `default` stays a plain browser. Because rules
fall through when their command is missing, that arrangement needs no new
mechanism: if furst-serve is not installed the reader rule is skipped and
the browser still gets the URL.

Firefox drops to a short list of sites that genuinely need it. Every reader
page carries a browser link, so the list can stay short.

Adds a README for furst-serve and rewrites the top-level one around the
whole pipeline: what it is for, what each piece does, measured output
sizes, and the limits — client-rendered pages, rate-limited search
endpoints, DRM.
2026-09-06 20:10:11 +02:00

105 lines
3.7 KiB
Markdown

# 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 <url> # start it if needed, then open <url> 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.