- Go 46.9%
- JavaScript 26.9%
- HTML 15.3%
- CSS 9.2%
- Dockerfile 1.7%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| assets | ||
| exampleSite | ||
| i18n | ||
| layouts/partials | ||
| proxy | ||
| .gitignore | ||
| CLAUDE.md | ||
| go.mod | ||
| hugo.toml | ||
| LICENSE | ||
| README.md | ||
hugo-ap-comments
Embed Mastodon / Fediverse replies as a comment section on your static Hugo site — no backend, no database, no third-party tracker.
When you publish a post, you toot a link to it. Readers click Load comments and their browser fetches the replies to that toot straight from the Mastodon API and renders them as a threaded discussion. Based on Carl Schwan's approach.
Features
- Distributed as a Hugo Module — one import, one partial call.
- Click-to-load: zero network requests until the reader asks for comments.
- Remote HTML sanitized with DOMPurify (bundled, no CDN).
- Threaded replies, original-poster badge, custom-emoji rendering.
- Assets built, minified and fingerprinted (with SRI) via Hugo Pipes.
- Namespaced (
ap-) CSS and i18n strings you can override/translate.
Requirements
- Hugo extended ≥ 0.116.0 (uses
js.Build). - Go (for Hugo Modules).
Install
Initialise your site as a Hugo Module if you haven't already:
hugo mod init github.com/you/your-site
Add the import to your site config (hugo.toml):
[module]
[[module.imports]]
path = "forge.wynning.tech/james/hugo-ap-comments"
Then fetch it:
hugo mod get forge.wynning.tech/james/hugo-ap-comments
Usage
1. Call the partial from a single template, e.g. layouts/_default/single.html:
{{ partial "activitypub-comments.html" . }}
2. Set your instance and account once in site config — these are the same for every post:
# hugo.toml / config.yaml
params:
activitypub_comments:
host: floss.social # the toot's instance host
username: carlschwan # account that posts (used for the OP badge)
3. Toot a link to your post from that account.
4. Link each post to its toot in front matter — only id is required:
comments:
id: "109774012599031406" # the status id (quote it — it's a big number)
The id is the long number at the end of the toot's URL
(https://floss.social/@carlschwan/109774012599031406).
host and username can also be set per post (a per-post value overrides the
site config), which is handy if you toot from more than one account/instance.
Posts without a comments.id render nothing.
Configuration
Site-wide options under params.activitypub_comments:
| Key | Default | Description |
|---|---|---|
host |
— | Default instance host for all posts (override per post) |
username |
— | Default account for all posts; used for the OP badge |
maxDepth |
4 |
Maximum indentation depth for nested replies |
proxy |
— | Base URL of the comments proxy (see below); override per post |
platform |
mastodon |
Permalink format: mastodon (/@user/{id}) or gotosocial (/@user/statuses/{id}); override per post |
GoToSocial / instances that require auth
Mastodon serves the context endpoint for public statuses without a token, so the
browser can fetch it directly. GoToSocial rejects anonymous /api/v1/...
requests with 401. Run the bundled proxy/ — it holds a bearer
token server-side, adds CORS, and forwards only the context endpoint — then
point the widget at it:
params:
activitypub_comments:
host: social.example.org # still used for display links
username: you
proxy: https://comments.example.com
platform: gotosocial # so "Reply" links use /@user/statuses/{id}
With proxy set, the browser fetches
https://comments.example.com/api/v1/statuses/{id}/context instead of hitting
the instance directly; the "Reply on…" link and avatars still use host. See
proxy/README.md for deployment.
Theming
All styles are namespaced with ap- and driven by CSS custom properties.
Override them in your own stylesheet:
.ap-comments {
--ap-accent: #ff5500;
--ap-indent: 1rem;
}
Localisation
Copy i18n/en.toml from this module into your site's i18n/ directory and
translate the strings, or add a file for another language code.
How it works
The browser calls https://{host}/api/v1/statuses/{id}/context, reads the
descendants array (public replies), sorts them into a tree by
in_reply_to_id, sanitizes each with DOMPurify, and appends them to the list.
Only replies that are public or unlisted are returned by the API, so
direct/private messages never appear.
Local development
cd exampleSite
hugo server
The example site uses a replace directive to point the module import at the
repository root, so changes are picked up without publishing.
License
MIT