feat: serve rendered pages over http, spartan and nex

This commit is contained in:
randogoth 2026-10-04 19:55:21 +03:00
parent 46f12b10da
commit 2f38b8b929
30 changed files with 2896 additions and 90 deletions

View file

@ -8,15 +8,15 @@ A Rust re-implementation of [smolweb](https://code.randogoth.com/randogoth/smolw
## Status
Early. Configuration, path resolution and Markdown parsing are implemented and tested; nothing is served yet.
It serves. HTTP, Spartan and Nex are working, with gemtext, XHTML-MP and HTML output and virtual hosting across all three.
| Area | State |
| --- | --- |
| Server configuration, virtual hosts, `--check` | implemented |
| Path resolution, traversal containment, live reload | implemented |
| Markdown parsing, includes, ASCII art, card breaks | implemented |
| gemtext and HTML output | planned |
| HTTP, Spartan and Nex listeners | planned |
| gemtext, XHTML-MP and HTML output | implemented |
| HTTP, Spartan and Nex listeners, virtual hosting | implemented |
| 80-column text and Nex output | planned |
| WML and XHTML-MP decks | planned |
| Gemini and Gopher | planned |
@ -37,8 +37,16 @@ protocol = "http"
bind = "0.0.0.0:8080"
formats = ["xhtmlmp", "html"]
default_site = "smol"
[listener.nex]
protocol = "nex"
bind = "0.0.0.0:1900"
formats = ["gemtext"]
site = "smol"
```
A page is rendered into the union of what the enabled listeners can serve, and no more: a configuration with no HTTP listener never builds any HTML. HTTP and Spartan route by the hostname the request carries, falling back to `default_site`; Nex carries none, so its listener names one site outright.
A `.itsybitsy.toml` in any content directory says how the Markdown in **that directory** renders. There is no inheritance: a subdirectory without its own file uses the built-in defaults rather than its parent's. Repetition in a deep tree is the price of never having to look elsewhere to know how a page renders.
```toml
@ -92,6 +100,20 @@ Include and art targets resolve relative to the file holding the reference and m
smolweb inherits five invented directives from its two renderer libraries: `{.card}`, `{.include}`, `![[ ]]`, `#[label](art.txt)` and MultiMarkdown attribute lists (`{: .center}`) — two incompatible brace grammars, with art alignment expressible two different ways in one line. None of it appeared in real content, so itsybitsy expresses the same capabilities with standard constructs instead. Parsing once rather than once per library also means the text formats gain setext headings and the WAP formats gain tables and ASCII art, none of which their original parser handled.
## Output formats
Each format is a crate implementing one trait, compiled in behind a cargo feature. A renderer is handed the parsed document and may not open files or sockets, which is what keeps every path check in one place rather than in each format.
| Format | Served as | Notes |
| --- | --- | --- |
| `gemtext` | `text/gemini` | Unwrapped; Gemini and Spartan clients wrap for themselves |
| `xhtmlmp` | `application/vnd.wap.xhtml+xml` | Well-formed XML with the Mobile Profile doctype |
| `html` | `text/html` | The same markup without the XML declaration |
HTTP negotiates between them from `Accept`. Only a literal media type counts as a match, so a browser's `*/*` can never be read as willingness to receive a WAP format; `?format=<id>` overrides negotiation outright, and naming an id the listener does not serve is a bad request rather than a silent fallback.
A third party adds a format by writing a crate that depends on `itsybitsy-core`, adding it to the workspace and one `#[cfg]` arm in the binary's registry, then naming its id in `formats`. Every listener's formats are checked against the registry at startup, so a format switched off by a feature is a configuration error naming the missing id rather than a server error on the first request.
## URLs
Links should be root-relative and extensionless (`[about](/about)`, not `about.md`), so the same link resolves identically from every protocol.
@ -111,4 +133,7 @@ Nothing outside the content root is reachable. A request target is percent-decod
devbox run check # fmt --check, clippy -D warnings, test
devbox run test
devbox run build
devbox run run # serve ./content on localhost
```
`RUST_LOG` sets the log level, which defaults to `info`: one line per request, and the real cause on failure.