No description
Find a file
2026-10-04 19:08:38 +03:00
bin feat: resolve request paths within contained site roots 2026-10-04 19:08:38 +03:00
core feat: resolve request paths within contained site roots 2026-10-04 19:08:38 +03:00
.gitignore feat: add workspace skeleton and server configuration validation 2026-10-04 19:01:35 +03:00
AGENTS.md feat: add workspace skeleton and server configuration validation 2026-10-04 19:01:35 +03:00
AI-DECLARATION.md feat: add workspace skeleton and server configuration validation 2026-10-04 19:01:35 +03:00
Cargo.lock feat: add workspace skeleton and server configuration validation 2026-10-04 19:01:35 +03:00
Cargo.toml feat: add workspace skeleton and server configuration validation 2026-10-04 19:01:35 +03:00
devbox.json feat: add workspace skeleton and server configuration validation 2026-10-04 19:01:35 +03:00
devbox.lock feat: add workspace skeleton and server configuration validation 2026-10-04 19:01:35 +03:00
LICENSE feat: add workspace skeleton and server configuration validation 2026-10-04 19:01:35 +03:00
README.md feat: resolve request paths within contained site roots 2026-10-04 19:08:38 +03:00
rustfmt.toml feat: add workspace skeleton and server configuration validation 2026-10-04 19:01:35 +03:00

🕸 itsybitsy

License: Apache-2.0 AI-DECLARATION: copilot

Serve folders of Markdown to different domains over HTTP, Spartan and Nex, rendered on the fly with no build step.

A Rust re-implementation of smolweb, which serves one folder from one process with its output formats hardcoded and its configuration in per-file frontmatter. itsybitsy changes all three: one process serves many domains, each output format is a separate crate behind a cargo feature, and configuration lives in a TOML file per content directory.

Status

Early. Configuration and path resolution are implemented and tested; nothing is served yet.

Area State
Server configuration, virtual hosts, --check implemented
Path resolution, traversal containment, live reload implemented
Markdown parsing planned
gemtext and HTML output planned
HTTP, Spartan and Nex listeners planned
80-column text and Nex output planned
WML and XHTML-MP decks planned
Gemini and Gopher planned

Configuration

Two files. A server file names the virtual hosts and the listeners:

version = 1

[site.smol]
root = "/srv/smol/content"
hosts = ["smol.place", "www.smol.place"]

[listener.web]
protocol = "http"
bind = "0.0.0.0:8080"
formats = ["xhtmlmp", "html"]
default_site = "smol"

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.

[defaults]
cache_control = 3600

[page."index.md"]
title = "Notes"
cache_control = 300

[defaults] applies to every Markdown file in the directory; a [page."name.md"] table overrides it for one file. title is per-page by nature, so setting it in [defaults] is an error; left unset, it is derived from the page's first level-1 heading. Each renderer's own keys arrive with that renderer. Editing the file re-renders that directory's pages on the next request, and a file that fails to parse makes them error rather than silently falling back to defaults.

Because the name begins with a dot, the same rule that keeps .secret.md out of the URL space keeps this file out of it too.

--check validates the configuration and reports the routing it resolved, without binding a port:

$ itsybitsy --config /etc/itsybitsy.toml --check
sites:
  smol: /srv/smol/content
hosts:
  smol.place -> smol
  www.smol.place -> smol
listeners:
  web: http on 0.0.0.0:8080 [xhtmlmp, html] by host, default smol
formats rendered per page: html, xhtmlmp

Everything the schema cannot express is checked here rather than on first request: a format no enabled feature provides, two sites claiming one hostname, a content root that does not exist, a server file sitting inside a content root where it would be served.

URLs

Links should be root-relative and extensionless ([about](/about), not about.md), so the same link resolves identically from every protocol.

Request Serves
/ index.md
/foo foo.md, else foo/index.md
/foo.md redirects to /foo
/img.png the file itself, by media type

Nothing outside the content root is reachable. A request target is percent-decoded before it is normalised, so an encoded .. becomes a real one and gets clamped at the root rather than quietly matching nothing; any path component beginning with a dot is refused outright; and whatever survives is canonicalised and required to still be inside the root, which is what defeats a symlink pointing out of it. There are no generated directory listings — only index.md.

Development

devbox run check   # fmt --check, clippy -D warnings, test
devbox run test
devbox run build