Publish your Hugo site to the Fediverse



One of the unspoken rules of using Hugo to make your site is that you need to write an entry about having made your website with Hugo. Well, this is mine. In particular, this post is about how to make your Hugo site show up on the Fediverse. I’ll admit it is not the first one in the genre, but I promise an additional angle: how to do it without using any cloud hocus-pocus like vercel, github pages, firebase and gitlab CI. We’re doing it the old-school way by uploading a bunch of files to a webserver. In the process, I’ll show you how to configure that webserver to make everything work. This will not be full guide, instead I am filling the gaps in others’ work.

What will work?

First it is important to understand what this will, and won’t do on a conceptual level. Static-site generators like Hugo are machines for turning plain text Markdown files in to styled HTML pages. Your site already makes HTML and it probably makes RSS. Making your site show up in the fediverse means adding an other output format on top of those two, namely ActivityPub1. In other words add another format in which the same content can be seen and spread in different networks.

Static sites are machines that turn plain-text into different formats, storing the results as files. Yes, those were supposed to be gears but they are very hard to draw, then I tried wheels which also did not work now they are undefined machinery.
Static sites are machines that turn plain-text into different formats, storing the results as files. Yes, those were supposed to be gears but they are very hard to draw, then I tried wheels which also did not work now they are undefined machinery.

Instead of converting markdown files in to only two formats, your site will now produce three!
Instead of converting markdown files in to only two formats, your site will now produce three!

What will not work?

Static sites are read-only, but ActivityPub requires two-way traffic. Thus, on the fediverse, your site and content will only be half-interactive. While you can represent static content in a way that it will show up on the Fediverse, and while this content can for example be commented on or shared, your site’s account and posts will not be interactive the way other posts on the fediverse are. First, you can not be followed2 and comments and replies won’t make it back to you.

This is why people have been relying on the cloud-stuff mentioned before. And while it is possible to roll one’s own Inbox and other software that can process and respond to requests, that is not the scope of this article (but it is of this one!).

Static sites work in pull-based flows. A browser or feed-reader will reach out and GET-request a specific file to a web server (1). The server will resolve that request (2) and return the file asked for (3).

However, ActivityPub and the fediverse are based on push-based flows and handling remote input. So applications will make POST-requests that will go unanswered by the static site.

Despite those limitations, we can at least make static content legible in the fediverse. For that, we need to represent the site as ActivityPub JSON objects.

All the moving parts

ActivityPub is based around Actors interacting with Objects such as posts. Furthermore, both Objects and Actors can have different adresses or representation necessitating redirects from one representation to another.

ActivitPub is basically like this:

Person does Like on Note

Person does Undo on Like

Person does Follow on Person

Actor

For our site to be something people on the fediverse can interact with, we need to make an Actor for it. To do so, the guide by Maho Pacheco is very helpful and I will not repeat him. Basically, you make a json file describing the actor and a web-finger file pointing that actor (this is a redirect between two address formats, that I expand on below). Look at the actor file of this blog for inspiration. Or that of any Mastodon account, which you can do appending .json to a username’s address in the browser: https://post.lurk.org/@rra.json.

Watch out for two broken stairs though: First, for a profile to show up (on Mastodon) you need to make sure the fields following and followers resolve. Maho does this by just linking to another profile of his, I do the same for the actor for this site. Second, is that inbox and outbox also need to resolve (see). If you do not have these or they do not resolve, looking up your account on Mastodon will silently fail. At first, these inbox and outbox can point to (empty) static files on your site such as in the case of this site’s actor:

"inbox": "https://test.roelof.info/inbox.ajson",
"outbox": "https://test.roelof.info/outbox.ajson", 

Finally, you have to make sure to return all such files with ContentType application/activity+json.

Redirects

The redirects involve a few things. First that different types of identifiers all resolve to the Actor. In other words, this blog can be found on the fediverse through actor@test.roelof.info as well as by searching for https://test.roelof.info/actor.ajson. The webfinger protocol is what provides the redirect from one to the other and involves creating a .well-known/webfinger in your website’s root. It can look like this:

  "subject": "acct:actor@test.roelof.info",
  "aliases": ["https://test.roelof.info/actor.ajson"],
  "links": [
    {
      "rel": "http://webfinger.net/rel/profile-page",
      "type": "text/html",
      "href": "https://test.roelof.info/"
    },
    {
      "rel": "self",
      "type": "application/activity+json",
      "href": "https://test.roelof.info/actor.ajson"
    }
  ]
}

Dit heeft ooit gewerkt maar nu niet meer, mijn eigen repo is te ingewikkeld..

https://github.com/RangerMauve/staticpub.mauve.moe/blob/a7e784538e28d33e6febdd8c9822434cb68b79c0/about.jsonld

ik heb dus about.jsonld die is static van rangermauve maar dan heb ik ook de partials zoals index.jsonld die zijn fedipage

in de config zijn er speciale mediatypes die er voor zorgen dat bestanden met .jsonld de juiste header type hebben: application/activity+json

[mediaTypes]
[mediaTypes."application/activity+json"]
suffixes = ["jsonld", "ajson"]
[mediaTypes."application/json"]
suffixes = ["json"]

https://maho.dev/2024/02/a-guide-to-implementing-activitypub-in-a-static-site-or-any-website-part-3/

you hardcoded hte mimetype in nginx :)))) so ajson for activity+json jsonld voor jsonld

the followers, following and posts have to resolve!! (i.e. https://github.com/mastodon/mastodon/discussions/17265#discussioncomment-1935984)

Content Negotiation in NGINX

map $http_accept $accept_ext{
	default index.html;
	~*application/activity\+json status.ajson;
	~*application/ld\+json status.ajson;
}

server {
	...

	try_files $uri $uri$accept_ext $accept_ext =404;
	...

}

Debugging

The hardest part of all of this is debugging why certain things don’t work. Looking up documents or actors silently fails on most software, so you have no idea why.

Luckily there are some debugging tools:

https://activitypub.academy/

The accompanying articles are also very solid primers not only on the ActivityPub model but specifically the Mastodon implementation: https://seb.jambor.dev/posts/understanding-activitypub/

In addition, you want to check whether you accidentally malformed your json in some way. Browsers can do this natively.

Browser pub https://browser.pub/

Sources

https://git.qoto.org/fedipage/fedipage-site

https://blog.mauve.moe/posts/distributed-press-social-inbox

https://s3lph.me/activitypub-static-site.html

https://github.com/PaulKinlan/paul.kinlan.me/blob/main/layouts/index.activity_outbox.ajson

https://maho.dev/2024/02/a-guide-to-implementing-activitypub-in-a-static-site-or-any-website-part-4/


  1. You can also think of it this way: normal web pages as served as ContentType text/html, your RSS feed as text/xml or application/rss+xml while your ActivityPub representation will be as application/activity+json↩︎

  2. Following in AP is a two-step process. A follow is requested and then granted. A fully static site will not grant, and thus the site will show up as “follow request pending”. ↩︎

Page(/log/todo/2023-static-activitypub-with-hugo)