In this blog post, we will explain how to generate the outbox and notes, ready to be shared in the Fediverse.
You can also navigate the other parts of this series here.
Overview
In the previous blog post, we created the actor
endpoint as a static file. As explained there, this file has a pointer to the outbox
, which is a collection of notes. A note in the ActivityPub protocol represents an activity
, such as a toot, photo, comment, etc. (hence the “Activity” in ActivityPub), and in our case, it will be the representation of a blog post.
A Deep Dive into the Structure of Outbox and Notes
To generate the outbox
and the notes
from our static website, there are a few alternatives. The outbox
looks like this:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://maho.dev/socialweb/outbox",
"type": "OrderedCollection",
"summary": "Bite-sized pieces of Software Engineering from a Garbage Code Connoisseur by Maho Pacheco",
"totalItems": 24,
"orderedItems": [
// ... collection of notes
]
}
Note: The Mastodon implementation returns a paginated object. The ordered collection contains not the items themselves but links to where to obtain such items. For a blog site with fewer than ~100 notes, this does not represent a problem. When I hit the 100 mark, I may implement pagination in my outbox generation.
A note will look like this:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://maho.dev/socialweb/notes/1dff22b5faf3fbebc5aaf2bb5b5dbe2c",
"type": "Note",
"content": "The Gendered Lens of AI: Unpacking Bias in Language ... html content",
"url": "https://maho.dev/2024/02/the-gendered-lens-of-ai-unpacking-bias-in-language-models/",
"attributedTo": "https://maho.dev/@blog",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2024-02-18T21:06:38-08:00",
"tag": [
{
"Type": "Mention",
"Href": "https://hachyderm.io/users/mapache",
"Name": "@mapache@hachyderm.io"
},
{
"Type": "Hashtag",
"Href": "https://maho.dev/tags/ai",
"Name": "#ai"
},
...
],
"replies": {
"id": "https://maho.dev/socialweb/replies/1dff22b5faf3fbebc5aaf2bb5b5dbe2c",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": "https://maho.dev/socialweb/replies/1dff22b5faf3fbebc5aaf2bb5b5dbe2c?page=true",
"partOf": "https://maho.dev/socialweb/replies/1dff22b5faf3fbebc5aaf2bb5b5dbe2c",
"items": []
}
}
}
There are a few things to dissect here:
- The
id
is a unique URL per blog post where to obtain the notejson
when a request withAccept: activity+json
is made. In our case, it is the static location where such JSON lives. - Content is HTML content. Mastodon will sanitize it to avoid any HTML injection, so you cannot include things like
iframe
s. - The
url
field will contain the actual blog post URL in a human-readable way. - The
attributedTo
field points to theactor
endpoint. - The
tag
collection is used by Mastodon to show tags and mentions and must match what the HTML content has. - The
replies
section is an optional set of endpoints where we can retrieve a collection of replies/comments to the blog post. We will see how to implement these endpoints in following posts.
Generating Outbox and Notes
As I mentioned before, there are a few alternatives. If you are using Hugo, Paul Kinlan uses a Hugo template to generate these notes.
However, I decided to do something more generic, in case your site is not using Hugo. This is where RSS comes to light. RSS (Really Simple Syndication) is basically an XML representation of an outbox created for news websites or blogs. Most, if not all, the static web generators generate an RSS by default, so I leveraged that and created a tool, Rss2Outbox, to do this conversion.
- Download the ActivityPub Utils package from here for your Windows/Linux/OSX environment and add it to your PATH.
- Build your site (e.g.,
hugo
). - Execute the RSS2Outbox tool in a terminal; it will show you the usage help:
Description:
Usage:
Rss2Outbox [options]
Options:
--rssPath <rssPath> (REQUIRED) The path to the RSS feed, usually a local path to index.xml
--staticPath <staticPath> (REQUIRED) The path to the static folder where the outbox and notes will be
generated
--authorUsername <authorUsername> (REQUIRED) The author username if the human publishing the blog, e.g.,
@mapache@hachyderm.io
--siteActorUri <siteActorUri> (REQUIRED) The URI of the author (actor endpoint) of the blog, e.g.,
https://maho.dev/@blog
--domain <domain> The domain of the blog; if not provided, it will be extracted from the
RSS feed
--authorUri <authorUri> The author URI if the human
publishing the blog, e.g.,
https://hachyderm.io/users/mapache. If not provided, it will be guessed from the
authorUsername.
--version Show version information
-?, -h, --help Show help and usage information
Five parameters are mandatory, as explained. In my case, my execution looks like this from the root Hugo folder:
Rss2Outbox \
--rssPath "public/index.xml" \
--staticPath static \
--authorUsername "\@mapache@hachyderm.io" \
--siteActorUri "https://maho.dev/@blog" \
--domain "https://maho.dev"
A confirmation message will let you know that the outbox and notes were created in the static/socialweb/outbox
and static/socialweb/notes
folders.
At the time of writing this post, I have already thought of a few improvements I want to make. For example, being able to retrieve the RSS.xml from a URL, configure the content template, or even implement all these guides in a GitHub template ready to clone and be used. However, all your feedback is very welcome to help me understand where to focus my efforts.
- Publishing your notes. Don’t forget that if you are using Hugo, you probably need to run
hugo
again to copy all these static artifacts into thepublic
folder. After that, you can deploy your assets to the cloud container, and that’s it! You have notes ready to be shared.
How to Test
Now, if you follow your account in Mastodon, you will be able to see the number of posts written by such an account. This is coming from the outbox
. Not all Mastodon accounts will retrieve these posts; this is a configuration by instance. So, some instances import all the notes when you follow the account, but others will not.
However, your notes are now ready to be shared in Fediverse! You can test it by grabbing one of the URLs and searching in any Mastodon instance such URL. It will retrieve the post, and you can interact with it by boosting it or favoriting it.
This action will “import” such note to that mastodon instance, and will become part of the Fediverse. Also worth to note that each interaction (like, boost, reply) to the note is an activity and will generate a POST
request to your inbox. At this point, we are still not ready to show our site to the Fediverse because we will lose these interactions, but we are closer.
In the next blog post, I will explain how to make your static site handle these interactions and follow requests in the inbox. If you’re interested, follow @blog@maho.dev in the Fediverse and let me know what you think!
A few notes on the cards format
Posts in mastodon use certain methods to render a preview of the links shared. The method I am using is through metadata tags in the header
, so if you want your posts show nicely make sure these give exists:
<meta property="og:type" content="article">
<meta property="og:title" content="{{ .Title }}">
<meta property="og:description" content="{{ .Summary | plainify }}">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:image" content="{{ .Site.BaseURL }}{{ .Params.cover }}" />