For some time, I’ve used Giscus for comments on my blog. It’s a solid alternative to Utterances , and both are popular solutions for comments on statically generated websites like mine. They both depend on GitHub to handle authentication and allow users to post “issues” or “discussions,” which are then presented as comments on my site via an API. This works quite well, but the required login seems to scare people off. I rarely get comments or even reactions, and this happens even for posts that land on Hacker News and get tens of thousands of visits.
But it’s not that people don’t comment-they do, just on Reddit, Facebook, X, and other social platforms. The problem is, I’m not on them. I’ve never been a fan of social media, but it’s hard to function in a world where everyone else is. I’ve tried a few platforms over the years, but none of them really stuck. On the other hand, I have to admit, they’re the perfect place for the discussions, comments, and rants I’d love to have under my posts 😁.
Lately, with SEO shifting and AI changing how search works, this trend has only strengthened. I decided to give social media another look to see where, if anywhere, my time would be well spent.
I started by implementing my own approach to cross-posting
. I used a crosspost tool that allows you to post articles to multiple social networks and wrapped it as a Github Action to integrate with my static site. I can trigger it at the end of a build or run it daily to spread the news about my new posts. This approach have one con - it’s hard to track what I already published, so I have to be extra carefull to not flood my socials with old posts.
I started blogging around 20 years ago, and this blog contains about 15 years of my posts. During this time, many social networks have been born and have died. I don’t want my content to disappear with them, so I prefer to publish on my own site first and syndicate from there. While looking for better ways to configure this on a static site, I came across services like Brid.gy and Webmention.io , which eventually led me to the IndieWeb Wiki . There, I read about the “POSSE approach” :
POSSE is an abbreviation for Publish (on your) Own Site, Syndicate Elsewhere, the practice of posting content on your own site first, then publishing copies or sharing links to third parties (like social media silos) with original post links to provide viewers a path to directly interacting with your content.
That was a “Holy crap!” moment - it was exactly what I was already trying to do. I took some time to get more familiar with what the IndieWeb is about. Initially, I thought it was just about the “small web,” but I soon discovered it’s a world of specific data formats. Let me explain.
Microformats
The first step is adding “Microformats” . My site already used JSON-LD to provide Breadcrumbs for Google, OpenGraph for Facebook, and Twitter Cards for X, but IndieWeb tools like Brid.gy don’t look at those at all. They parse the HTML itself, looking for specific class names like h-entry, p-name, and u-url.
This was my first challenge. My theme (PaperMod) is great, but it wasn’t built for the IndieWeb. I had to dig into my theme’s layouts and start marking everything up. The post title became a p-name, the content an e-content, and I had to ensure the author’s information was wrapped in an h-card.
I started with the post content itself:
...
- <article class="post-single">
+ <article class="post-single h-entry">
<header class="post-header">
{{ partial "breadcrumbs.html" . }}
- <h1 class="post-title entry-hint-parent">
+ <h1 class="post-title entry-hint-parent p-name">
...
- <div class="post-content">
+ <div class="post-content e-content">
...
<ul class="post-tags">
{{- range ($.GetTerms $tags) }}
- <li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li>
+ <li><a href="{{ .Permalink }}" class="p-category">{{ .LinkTitle }}</a></li>
{{- end }}
</ul>
...
One tricky part was the author’s photo. I wanted it to be semantically present in the HTML but invisible on the page (since my bio already displays it). My pages is listed on (250kb.club)[https://250kb.club] and I want to keep amount of external dependencies under the control. I ended up writing some clever Hugo logic to fetch the photo from the author’s bundle and embed it as a hidden u-photo within the author metadata. I also had to fix my archive and list pages. It turns out Brid.gy complains if a page listing multiple posts doesn’t have an h-feed class to wrap them. A few div wraps later, each post in a list became a proper h-entry within that feed.
{{- with site.Params.homeInfoParams }}
- <article class="first-entry home-info">
+ <article class="first-entry home-info h-card">
<header class="entry-header">
<h1>{{ .Title | markdownify }}</h1>
</header>
- <div class="entry-content">
+ <div class="entry-content p-note">
{{ .Content | markdownify }}
</div>
+ <a class="u-url u-uid p-name" href="{{ "" | absLangURL }}" hidden>{{ site.Params.author }}</a>
+ {{- $author := site.GetPage (printf "/authors/%s" (site.Params.author | urlize)) }}
+ {{- if $author }}
+ {{- $photo := $author.Resources.GetMatch $author.Params.photo }}
+ {{- $photoUrl := "" }}
+ {{- if $photo }}
+ {{- $photoUrl = $photo.Permalink }}
+ {{- else if $author.Params.photo }}
+ {{- $photoUrl = $author.Params.photo | absURL }}
+ {{- end }}
+ {{- if $photoUrl }}
+ <img src="{{ $photoUrl }}" alt="{{ site.Params.author }}" class="u-photo" style="display:none" hidden>
+ {{- end }}
+ {{- end }}
...
Webmentions and Syndication
With my site’s data now machine-readable, the next step was to receive “pings” whenever someone interacted with my posts elsewhere. That’s where Webmention.io comes in. It acts as a hosted service that catches mentions and stores them for you. If you have this nibbling feeling, that you heard about it, maybe as a “Pingback” - I had this feeling too. They’re similar, but different solutions.
To enable it, you just add a couple of links to your site’s head:
+ {{- with site.Params.WebmentionsHost }}
+ <link rel="webmention" href="https://webmention.io/{{ . }}/webmention" />
+ <link rel="pingback" href="https://webmention.io/{{ . }}/xmlrpc" />
+ {{- end }}
But catching them is only half the battle. I wanted to display these mentions on my blog, similar to the Giscus comments I mentioned earlier, but without the “scary login” barrier. If someone likes my post on Mastodon, I want that heart to show up here.
I didn’t want a heavy dependency, so I built a custom, vanilla JavaScript renderer. It uses the IntersectionObserver to lazy-load interactions only when you scroll to the bottom. It handles likes, reposts, and replies, sanitizing the content and making sure everything looks native to my theme. It even handles URL aliases - so if I change a post slug for SEO reasons, the script is smart enough to query the API for all historical URLs associated with that post. No mention gets left behind.
This script still grows, so I would not put it here - you can find it on my PaperMod theme fork .
To make this work, I added a <div> container to my post template where the script could inject the mentions, placing it just above the existing comments section:
+ {{- partial "webmentions.html" . }}
{{- if (.Param "comments") }}
{{- partial "comments.html" . }}
{{- end }}
The code is simple-just a <div> container with a unique id for easy targeting:
{{- if site.Params.ShowWebmentions }}
{{- $targets := slice .Permalink }}
{{- range .Aliases }}
{{- $targets = $targets | append (. | absURL) }}
{{- end }}
{{- with .Params.canonicalURL }}
{{- $targets = $targets | append (trim . " " | absURL) }}
{{- end }}
{{- $targets = $targets | uniq }}
<div id="webmentions-container" data-targets="{{ delimit $targets " ," }}"></div>
{{- $webmentionJs := resources.Get "js/webmention-renderer.js" | resources.Minify | resources.Fingerprint }}
<script src="{{ $webmentionJs.RelPermalink }}" defer></script>
{{- end }}
The key part here is the data-targets attribute. It must list every possible URL for a post (including the main one and any aliases from redirects) so services like Brid.gy can recognize them.
This brings us to the “Syndicate Elsewhere” part of POSSE. Brid.gy is the magic glue. It watches my site, sees the hidden u-syndication links I’ve added to each post, and cross-posts them to Mastodon and BlueSky.
{{- if (.Param "comments") }}
{{- partial "comments.html" . }}
{{- end }}
+ {{- if and site.Params.Syndication site.Params.Syndication.links $typeConfig }}
+ {{- range site.Params.Syndication.links }}
+ <a href="{{ . }}" class="u-syndication" hidden></a>
+ {{- end }}
+ {{- end }}
+ <a class="u-url" href="{{ if .Params.canonicalURL -}} {{ trim .Params.canonicalURL " " }} {{- else -}} {{ .Permalink }} {{- end }}" hidden></a>
The process is bi-directional: Brid.gy sends the post out, and then it watches for replies on those platforms and sends them back to me as Webmentions.
To keep things clean, I refactored the configuration into a dedicated Syndication map in my hugo.yaml. This allows me to define custom messages for different content types (like “Just finished a book…” for my book reviews) and specific hashtags for different types of content. I also added a “ping” step to my GitHub Actions deployment. Now, when my site deployment finishes on Cloudflare, my GitHub Action pings Brid.gy to say, “Hey, I’m updated!” This makes the cross-posting almost instantaneous.
# IndieWeb configs
WebmentionsHost: gagor.pro
ShowWebmentions: true
Syndication:
links:
- https://brid.gy/publish/mastodon
- https://brid.gy/publish/bluesky
types:
posts:
extra_tags: ["blog"]
books:
message: "Just finished a book, check my review:"
extra_tags: ["blog", "books"]
While this wasn’t overly complex, it didn’t make sense to follow the traditional Hugo method of overriding the theme by copying individual files to my local layouts directory. The number of modified files was just too large. I decided to fork the theme instead.
The full diff of my changes is available on GitHub .
Was It Worth It?
Implementing this felt a bit like going back to the basics of HTML. It’s a lot of manual work compared to just slapping a comment widget on a page. On the one hand, it’s not rocket science. The IndieWeb standards mostly involve adding some classes and a few extra metadata tags. On the other hand, my site is now bloated with metadata: JSON-LD, OpenGraph, Twitter Cards, and now Microformats. It feels like a lot of overhead just to make automation work. It doesn’t feel quite right.
I remember “pingbacks” from my WordPress days, which were similar but notorious for delivering spam. Webmentions feel like an improvement, but I question myself if this level of integration is necessary when simpler solutions exist, like the developer who built a comment section using the raw Bluesky API (I’ve read it after I went for IndieWeb trip 😉).
Another concern is my new dependency on two external services: webmention.io and Brid.gy . They’re nice and community-driven, but the community feels small. There aren’t many alternatives, and the services look dated, with aging UIs and missing features. It looks like Brid.gy supported more networks in the past but is now limited mostly to Mastodon and Bluesky. While I’m no fan of Big Tech social networks, they undeniably drive traffic.
In the case of Webmentions, they are not widely supported - you have to manually add them to your blog. Plugins exist, but it’s not an out-of-the-box experience. I can’t shake the feeling that I’ve cluttered my website with metadata to achieve something I could have done with my existing crosspost action and a few direct API calls. Some IndieWeb authors even add forms to their sites asking readers to “manually submit a Webmention.” Why would anyone do that? It’s just more friction, and most people have no idea what IndieWeb or Webmentions are.
There’s also the question of ownership. The community is great, but what if the services shut down? I don’t own my GitHub Discussions comments either, but GitHub is a corporate giant that will likely survive for a while. If they were going to close or monetize the service, there would likely be enough warning to export my data. The major social networks will also survive, making it possible to retrieve content if needed. I’m not yet convinced I can trust the IndieWeb ecosystem in the long run. I don’t want to involve in holding my own infra either, so I will take this risk.
For now, I’ve taken a deep dive into the IndieWeb to give it a fair shot. It was a relatively low-cost experiment. My blog is now a true citizen of the social web without being beholden to any single platform. If this doesn’t work out, I’ll move on, perhaps to build a custom solution.
So, if you’re reading this on Mastodon or BlueSky , try replying. Your comment might just appear right here. Pretty neat, right?
