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.
3.7 KiB
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
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'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
- A feed — rendered as a list of entries, whether it was requested at
/feedor just turned out to be XML. - A site with an adapter — Hacker News gets stories with score and author, and comment threads with their indent preserved.
- An article — extracted by furst-read.
- 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:
[[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.