Mastodon

Adding Fediverse Comments to a Pelican Blog



Every static site eventually faces the comment question. Disqus tracks your readers. Self-hosted solutions like Commento or Isso need a server-side component and a database. Most options either compromise privacy or add operational complexity that feels disproportionate for a personal blog.

Then I came across Jan Wildeboer’s approach for his Jekyll blog: use Mastodon as the comment backend. Every blog post gets a corresponding Mastodon post. Replies to that post become the article’s comments, fetched client-side through the public Mastodon API. No tracking, no database, no server-side logic. Just the Fediverse doing what it already does.

I liked the idea enough to port it to Pelican. This article explains how it works, how it’s integrated, and how you could do the same.

The Concept

The Mastodon API exposes a public endpoint that returns the full conversation context for any post:

GET https://instance.tld/api/v1/statuses/{id}/context

The response includes an ancestors array (the thread above the post) and a descendants array (all replies). We only care about descendants - those are the comments. No authentication is required for public posts.

The workflow is straightforward:

  1. Publish your blog article
  2. Post about it on Mastodon
  3. Add the Mastodon post’s metadata to your article’s frontmatter
  4. Rebuild the site

From that point on, anyone who replies to your Mastodon post will have their reply appear as a comment on the blog.

Integration into Pelican

Article Metadata

Pelican lets you define arbitrary metadata fields in article frontmatter. I added three fields to link an article to its Mastodon post:

Mastodon_Host: burningboard.net
Mastodon_User: Larvitz
Mastodon_Id: 116035059515573876
  • Mastodon_Host - the domain of the Mastodon instance
  • Mastodon_User - your handle on that instance
  • Mastodon_Id - the numeric status ID (grab it from the post URL)

These become available in templates as article.mastodon_host, article.mastodon_user, and article.mastodon_id. Articles without these fields simply don’t get a comment section.

The Comment Template

The comment system lives in a single Jinja2 include file at themes/pelican-alchemy-custom/templates/include/comments.html. The entire thing is wrapped in a conditional:

{% if article.mastodon_id %}
<section class="fediverse-comments">
  ...
</section>
{% endif %}

This gets included at the bottom of the article template:

{% include 'include/comments.html' %}

Since my blog uses the Alchemy theme, I override the default article.html in a custom template directory loaded via Pelican’s THEME_TEMPLATES_OVERRIDES setting. This keeps the base theme untouched as a git submodule while allowing targeted modifications.

The JavaScript

The client-side logic is compact. On page load, it fetches the conversation context and renders each reply:

fetch('https://{{ article.mastodon_host }}/api/v1/statuses/{{ article.mastodon_id }}/context')
    .then(function(response) {
      return response.json();
    })
    .then(function(data) {
      if (data['descendants'] &&
          Array.isArray(data['descendants']) &&
          data['descendants'].length > 0) {
        data['descendants'].forEach(function(reply) {
          // Build and render each comment
        });
      } else {
        // "No comments yet" message
      }
    });

Each reply from the API includes the author’s display name, avatar, profile URL, the reply content (as HTML), a timestamp, and engagement counts (replies, boosts, favourites). The script builds an HTML structure for each comment and appends it to the page.

One nice detail: Mastodon custom emojis in display names. The API returns an emojis array for each account, with shortcodes and image URLs. The script replaces :shortcode: patterns in display names with the actual emoji images, so usernames render correctly.

XSS Protection

Since Mastodon post content arrives as HTML, rendering it directly would be a textbook XSS vulnerability. The implementation handles this in two layers:

  1. Manual escaping - all user-controlled strings (display names, URLs, avatar paths) are run through an escapeHtml() function before being placed into the comment markup
  2. DOMPurify - the assembled comment HTML is sanitized through DOMPurify before being injected into the DOM
document.getElementById('mastodon-comments-list').appendChild(
    DOMPurify.sanitize(comment, {RETURN_DOM_FRAGMENT: true})
);

The DOMPurify library is served as a local static file rather than loaded from a CDN, avoiding external dependencies. It’s registered in pelicanconf.py as a static path:

STATIC_PATHS = ['images', 'extra/custom.css', 'extra/robots.txt', 'extra/purify.min.js']
EXTRA_PATH_METADATA = {
    'extra/purify.min.js': {'path': 'purify.min.js'},
}

Interaction Flow

Readers who want to comment need a Mastodon (or other ActivityPub) account. The template provides a “Copy post link” button that copies the Mastodon post URL to the clipboard. They can then search for that URL on their own instance, find the post, and reply. Their reply will show up on the blog the next time someone loads the page - no rebuild required, since everything is fetched live.

Styling

The comment section is styled to match the blog’s Solarized Dark theme. Each comment is a flexbox row with a circular avatar on the left and the content on the right. Author names link to their Mastodon profiles. Dates link to the original reply. Engagement metrics (replies, boosts, favourites) are displayed in a muted footer.

Trade-offs

This approach has real limitations worth acknowledging:

  • JavaScript required - the comments won’t load without it. A <noscript> fallback tells the reader.
  • Content warnings are ignored - Mastodon’s CW feature isn’t reflected in the rendered comments.
  • Media attachments are excluded - only text content is displayed.
  • Moderation is coarse - you moderate through Mastodon itself. Blocking an account or deleting the source post affects all comments. There’s no per-comment moderation from the blog side.
  • The source post must stay up - if you delete the Mastodon post, the comments disappear. Bookmark your posts.
  • CORS dependency - the Mastodon instance must allow cross-origin requests to the API, which is the default for most instances.

On the other hand, the advantages are compelling for a personal blog: zero infrastructure, full reader privacy, decentralized identity, and comments from people who already have Fediverse accounts - which self-selects for a community I actually want to hear from.

The Full Picture

The implementation touches exactly four things:

  1. themes/pelican-alchemy-custom/templates/include/comments.html - the template with HTML structure and JavaScript logic
  2. themes/pelican-alchemy-custom/templates/article.html - the article template, with one {% include %} line added
  3. content/extra/purify.min.js - DOMPurify library as a static asset
  4. content/extra/custom.css - styling for the comment section

Plus three metadata fields in any article that should have comments enabled. That’s it. No plugins, no external services, no build-time processing. The comments are entirely a client-side feature.

You can see it in action on my BGP article.

Attribution

This implementation is directly inspired by Jan Wildeboer (@jwildeboer@social.wildeboer.net), who published this approach for his Jekyll blog in February 2023. I took his logic and ported it to Pelican’s Jinja2 templating system. The core idea - using Mastodon’s public conversation API as a comment backend - is entirely his.

References

Comments

You can use your Mastodon or other ActivityPub account to comment on this article by replying to the associated post.

Search for the copied link on your Mastodon instance to reply.

Loading comments...