listnr gives a static blog a Fediverse presence.
It runs as a single ActivityPub actor, polls the blog's RSS/Atom feed for new posts, announces those posts to followers, receives replies/likes/boosts, and exposes a small API that a static blog can use to render Fediverse interactions.
The blog itself stays static. listnr runs separately, usually on a VPS behind TLS.
CGO_ENABLED=0 go build -o listnr .For a Linux VPS build from macOS:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o listnr .To inject a version string:
go build -ldflags "-X github.com/vrypan/listnr/cmd.Version=$(git describe --tags --always --dirty)" -o listnr .Create listnr.toml:
[actor]
username = "blog"
domain = "vrypan.net"
host = "ap.vrypan.net"
type = "Service"
name = "vrypan.net blog"
summary = "Posts from vrypan.net"
icon = "https://blog.vrypan.net/avatar.png"
header = "https://blog.vrypan.net/header.jpg"
blog_url = "https://blog.vrypan.net"
also_known_as = ["https://mastodon.example/@vrypan"]
[[actor.fields]]
name = "Website"
value = "<a href=\"https://blog.vrypan.net\" rel=\"me\">blog.vrypan.net</a>"
[[actor.fields]]
name = "RSS"
value = "<a href=\"https://blog.vrypan.net/index.xml\">Feed</a>"
[[actor.tags]]
name = "#blogging"
href = "https://mastodon.social/tags/blogging"
[actor.extra]
discoverable = true
indexable = true
manuallyApprovesFollowers = false
[feed]
url = "https://blog.vrypan.net/index.xml"
poll_interval = "5m"
backfill = 20
[server]
listen = "127.0.0.1:8420"
data_dir = "/var/lib/listnr"
log_requests = false
[admin]
token = "change-me"Important fields:
actor.domainis the handle domain. With the example above, the actor is@blog@vrypan.net.actor.hostis where listnr is served. With the example above, ActivityPub endpoints live underhttps://ap.vrypan.net.actor.typecontrols the ActivityPub actor type. UseServiceto signal an automated account to Mastodon-compatible servers; the default isPerson.actor.iconis the profile avatar.actor.headeris the optional profile banner/header image.actor.fieldsare Mastodon-style profile fields rendered asPropertyValueattachments.valuemay contain HTML.actor.also_known_asandactor.tagsare optional profile aliases and hashtags. Support varies by Fediverse server.actor.extrais an advanced escape hatch: keys under[actor.extra]are copied directly into the actor JSON. This lets you add Fediverse extensions without recompiling listnr. Use it carefully, because these values are emitted as-is.server.data_dirstores the SQLite database and RSA keypair. The keypair is generated automatically on first run.server.log_requestsenables HTTP access logs when set totrue. It logs method, path, status, bytes, duration, remote address, and user agent.- If
admin.tokenis empty, the admin API is disabled.
./listnr serve -c listnr.tomlOn startup, listnr:
- creates or opens the SQLite database;
- creates or loads
actor.pem; - starts the delivery queue worker;
- starts the feed poller;
- serves ActivityPub, public API, and admin API routes.
Logs are written to stderr. Under systemd, they are captured by journald. HTTP
request logging is disabled by default; enable [server] log_requests = true
if you want access-style request logs from the daemon itself.
Public endpoints include:
/.well-known/webfinger/actor/inbox/outbox/followers/posts/{id}/api/interactions?url=<post-url>
Every command except serve talks to the admin API.
Create ~/.config/listnr/cli.toml:
server = "https://ap.vrypan.net"
token = "change-me"You can also pass --server and --token on the command line.
Common commands:
listnr stats
listnr refresh # tell the server to fetch the RSS feed now (alias: poll)
listnr replies list
listnr replies list --post https://blog.vrypan.net/post/
listnr replies list --hidden
listnr replies hide 123
listnr replies unhide 123
listnr replies delete 123
listnr block list
listnr block add spam.example
listnr block add https://bad.example/users/spammer
listnr block rm spam.example
listnr followers list
listnr followers rm 42
listnr versionOn the first run, listnr imports the newest feed.backfill items as
federated history. These appear in the outbox, but listnr does not fan them
out to followers.
Older feed items are stored as seen-only rows. They do not appear in the outbox and are never announced later just because they were already present in the feed.
After the first run:
- unknown feed items become new ActivityPub
Notes and are announced withCreate; - changed feed items whose posts were federated are announced with
Update; - items missing from the feed are ignored because feeds often truncate.
Use docs/widget.js to render Fediverse interactions on static post pages:
<script src="/path/to/widget.js" data-endpoint="https://ap.vrypan.net"></script>To pin the post URL explicitly:
<script
src="/path/to/widget.js"
data-endpoint="https://ap.vrypan.net"
data-url="https://blog.vrypan.net/2026/07/example/"></script>The widget fetches:
https://ap.vrypan.net/api/interactions?url=<post-url>
It uses data-url when present and falls back to the current page URL. It
strips query strings and fragments before making the request. Replies are
sanitized on the server before they are stored and served.
A typical deployment is:
- Build the Linux binary.
- Install it as
/usr/local/bin/listnr. - Put
listnr.tomlin/etc/listnr/listnr.toml. - Run it with the systemd unit in
deploy/listnr.service. - Reverse proxy
https://ap.vrypan.netto127.0.0.1:8420. - Add a redirect rule for the handle domain:
vrypan.net/.well-known/webfinger* -> 302 https://ap.vrypan.net/.well-known/webfinger
Preserve the query string in that redirect.
See deploy/README.md for a compact deployment checklist.
- listnr is intentionally single-actor.
- The followers collection is count-only.
- Inbox handlers return
202 Acceptedfor ignored or blocked activities. - The project uses
modernc.org/sqlite, so builds must keep working withCGO_ENABLED=0.
MIT. See LICENSE.