Adding IndieWeb support to my static Hugo blog

ig0r02 pts0 comments

Adding IndieWeb support to my static Hugo blog - Igor Kulman

I recently read Andros Fenollosa’s article I joined the IndieWeb, here’s what I learned and immediately went down the same rabbit hole.

I had seen the term IndieWeb before, but I always assumed it meant a vague preference for personal websites over social networks. Something nostalgic, maybe a little idealistic, but not necessarily technical.

It turns out there is a very practical side to it: small standards that let independent websites describe their content, receive links and reactions from other sites, and connect to social networks without giving up ownership of the original content.

That sounded exactly like something I wanted for this blog.

I was already halfway there

The first surprise was how much I had already done without explicitly thinking of it as IndieWeb.

This blog has its own domain. The content is Markdown stored in Git. Hugo generates plain HTML and Netlify hosts it. Every article has an explicit permanent URL. There is an RSS feed, canonical links, and rel="me" links to my GitHub and Mastodon profiles.

The most important IndieWeb decision probably happened back in 2015, when I moved this blog from WordPress to Hugo. The publishing interface is still my editor and Git, and the site does not need a database or an administration backend.

That meant I did not need to rebuild anything. I only needed to make the HTML more descriptive and add a way to receive interactions.

Adding microformats to Hugo

The first step was microformats2, a set of class names added to normal HTML. They do not change how the page looks, but they let parsers understand what the page contains.

I added an h-card to the profile on the homepage, an h-feed around the article list, and an h-entry to every article. The individual properties describe the title, content, publication date, author, URL, and tags:

article class="h-entry"><br>h1 class="p-name">Article titleh1><br>time class="dt-published" datetime="2026-07-29T07:00:00+02:00"><br>July 29, 2026<br>time><br>section class="e-content"><br>...<br>section><br>article>

My Hugo theme is vendored directly in the repository and I have already modified it many times. I could have copied the templates into the project-level layouts directory as overrides, but that would mean copying whole files. Updating the existing theme templates was simpler and consistent with how this blog is maintained.

I modified the list and article templates to emit:

h-card, p-name, p-note, u-photo, and u-url for the homepage profile

h-feed and lightweight h-entry items for article lists

h-entry, e-content, dt-published, p-author, u-url, u-uid, and p-category for articles

There was one amusing problem. The theme uses Tailwind CSS classes such as h-24 and h-full for element heights. A microformats parser sees classes beginning with h- as nested microformat roots, so my avatar stopped being parsed as the u-photo of the surrounding h-card.

CSS and microformats had accidentally chosen the same prefix for completely different things.

I replaced those two height utilities and validated the result with the Pin13 microformats parser. The parser then correctly found the profile, photo, feed, article titles, dates, URLs, author, content, and categories.

Receiving Webmentions

The next part was Webmention, the protocol that lets one page notify another page that it links to it.

A Webmention request contains only a source and a target URL. The receiver fetches the source, verifies that the link really exists, and then decides what kind of interaction it represents. Combined with microformats, that interaction can be a normal mention, a reply, a like, or a repost.

A static Hugo site cannot receive HTTP requests by itself, so I registered blog.kulman.sk with Webmention.io. It receives and stores Webmentions on behalf of static sites and exposes them through an API.

The blog now advertises the hosted endpoints in its :

link rel="webmention"<br>href="https://webmention.io/blog.kulman.sk/webmention"><br>link rel="pingback"<br>href="https://webmention.io/blog.kulman.sk/xmlrpc">

I tested the setup with Webmention Rocks. It discovered the endpoint, sent a Webmention, and Webmention.io returned HTTP 201 with a status URL. The test mention appeared in the dashboard and could be deleted afterwards.

At that point everything worked, but there was nothing useful to display.

Getting reactions back from Mastodon

What I actually wanted was backfeed: when I share an article on Mastodon, bring the replies, favorites, and boosts back to the original article.

For that I used Bridgy, specifically Bridgy classic rather than Bridgy Fed. The distinction is important.

Bridgy Fed would make the website its own Fediverse account. I already have @igorkulman@hachyderm.io, so I wanted to connect the blog to that existing account instead. Bridgy classic is designed for exactly this: connect an existing social account to a website, then translate social interactions into...

article blog webmention hugo content indieweb

Related Articles