feat: add Nex and Gopher listeners

Reuses md2txt's existing nex and text renderers (both already ship
unmodified) -- rendered unwrapped gemtext still works for Gemini/Spartan
clients that wrap themselves, but Nex and Gopher clients don't, so those
two get the classic 80-column pre-wrap instead.

Neither protocol has a redirect status of its own, so Site.resolve_flat()
follows the /foo.md -> /foo canonical bounce and the WML-card-only ->
parent redirect server-side and hands back real content directly, rather
than leaving the two new listeners to invent a redirect convention that
doesn't exist on the wire.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
randogoth 2026-09-26 15:13:43 +03:00
parent 12956a5034
commit 0b67069760
8 changed files with 290 additions and 13 deletions

View file

@ -1,16 +1,19 @@
# smolweb
Serve a folder of Markdown as [Gemini](https://geminiprotocol.net/) capsules,
[Spartan](https://portal.mozz.us/gemini/spartan.mozz.us/) capsules, and HTTP
pages content-negotiated between WML (WAP 1.x) and XHTML-MP (WAP 2.0 and
modern browsers) — all rendered on the fly, no build step.
[Spartan](https://portal.mozz.us/gemini/spartan.mozz.us/) capsules,
[Nex](https://nightfall.city/nex/info/specification.txt) and
[Gopher](https://www.rfc-editor.org/rfc/rfc1436) documents, and HTTP pages
content-negotiated between WML (WAP 1.x) and XHTML-MP (WAP 2.0 and modern
browsers) — all rendered on the fly, no build step.
```bash
smolweb --root ./content --host example.com
```
Every listener binds by default: Gemini on `:1965` (TLS), Spartan on `:300`,
HTTP on `:8080`. An empty address disables one:
HTTP on `:8080`, Nex on `:1900`, Gopher on `:70`. An empty address disables
one:
```bash
smolweb --root ./content --host example.com --spartan ""
@ -21,9 +24,19 @@ smolweb --root ./content --host example.com --spartan ""
Markdown is parsed once per file (cached by mtime, re-parsed the moment the
file changes) and rendered into every format from the same source:
- **gemtext** via [md2txt](https://code.randogoth.com/randogoth/md2txt)'s `gemini` renderer.
- **gemtext**, **Nex**, and **Gopher text** via [md2txt](https://code.randogoth.com/randogoth/md2txt)'s `gemini`, `nex`, and `text` renderers respectively.
- **WML** and **XHTML-MP** via [wapdown](https://code.randogoth.com/randogoth/wapdown).
Gemini and Spartan clients wrap gemtext themselves, so it's rendered
unwrapped (`width=0`). Nex and Gopher clients don't, so those two are
pre-wrapped at the classic 80-column convention instead.
Neither Nex nor Gopher has a redirect status of its own, so the `/foo.md ->
/foo` canonical-URL bounce and the WML-card-only-path-redirects-to-its-
parent behaviour (see below) are both resolved server-side instead of
bounced back to the client — the client always gets real content back
directly, on the first request.
Both libraries read the same leading `---` frontmatter block independently
and ignore keys they don't recognise, so one file can freely mix wapdown's
deck-structure keys (`title`, `split_level`, `deck_per_card`, ...) with
@ -53,6 +66,8 @@ A document with `deck_per_card: true` gets its own URL space for WML only:
`/trail/weather` serves one card's own deck directly. Gemini, Spartan, and
an HTTP client negotiating gemtext/XHTML-MP all redirect `/trail/weather`
back to `/trail` instead — that URL space only ever makes sense for WML.
Nex and Gopher serve `/trail`'s own content directly for the same request,
since neither has a redirect status to bounce with.
## HTTP content negotiation