Webmentions on my blog, Part 1: Incoming
5 min read
I'm unsure if everyone is really talking about webmentions or if I'm just noticing them more because I'm working on mine, but it certainly feels like the former.[1]
A few weeks ago, I mentioned that my site now supports webmentions. Since then, I've continued to iterate on my implementation, and I think I've finally gotten to the point where I'm happy with everything.
My site and requirements #
I build my site with 11ty, and it's hosted on a VPS over at Capsul. Many people host their static sites on something like Netlify or GitHub Pages, but I do not. Instead, I have a homelab with a Proxmox LXC host. This runs a cron job that checks my git repo, which is hosted locally only, for new commits, which then triggers a build and rsync pushes to the VPS.
My goal is to reduce and eliminate third-party dependencies, so while many people use Webmention.io and Bridgy for their blog webmentions, I wanted to try to avoid that.
In the interest of full disclosure, I did use Claude Code to help me develop this. I'm not a web developer by trade. I can hack around and get shit on the page, but webmentions were too complex and important for me to get wrong. I could have never implemented this on my own using a search engine. I think something many people dismiss too quickly is how agents can empower normal people to participate fully in the IndieWeb.
Native webmentions #
Since I knew webmention.io was out for me, I decided pretty quickly to just roll my own receiver.
Here is the technology stack I landed on:
- Node.js runtime
- Express 5 (HTTP server)
- microformats-parser (
mf2) for Post Type Discovery - Node built-ins:
dns,net(SSRF guard),crypto,fs - flat JSON file store (
interactions.json), atomic writes, no database - systemd unit, listening
127.0.0.1:3210 - Caddy reverse proxy (public TLS)
I'm already using and am pretty familiar with Node.js, so that seemed a natural reach. I wanted to try to avoid a real database because that's just another thing to maintain. I already use Caddy, so that one was easy too.
As I was building this, I tried very hard to build something that was secure. I did not want to introduce vulnerabilities or opportunities for bots and spammers to spam my webmentions or compromise my server.
Here's the overview of how webmentions come in.
Step 1: Receive a new webmention #
When a website sends my site a webmention, I do not just take the sender's word for it. The first thing my receiver does is verify that the page being sent along with the webmention actually does contain a link to my site/post/whatever. If it does not, then the webmention gets dropped. This is one of the main mechanisms used to fight spam.
This is probably the riskiest part of webmentions because the sender picks the URL. A bad actor could send a link that's specifically designed to poke around on my server. This is a well-known class of attack called server-side request forgery, or SSRF.
The defense is to refuse to fetch anything that isn't safely on the public internet. Before my server connects, it looks up the URL's hostname, finds the actual IP address behind it, and rejects the request if that address is internal: localhost or a private home-network range. And because someone could try to sneak past that with a redirect, the check runs again on every hop, not just the first URL.
Step 2: Classify #
Once a mention is verified, the receiver parses the source page's microformats to figure out what kind it is: a like, a repost, a reply, or just a plain mention.
Microformats are small bits of standardized, machine-readable markup that a page adds to its own HTML to declare what something is. So a like isn't just a link; the sender's page carries hidden markup that effectively says "this link is a like of that URL."
Step 3: Store #
The verified, classified webmention gets stored into a JSON file on my server's disk to await the build server's pickup of it.
I chose a JSON file because I thought that a database would be overkill for a small blog like mine. A file is one fewer service to run, secure, back up, and keep alive.
The storage step is also where deduplication happens to ensure we're not recording the same webmention multiple times.
Step 4: Build server pickup and render #
My homelab server runs hourly to check for new webmentions in the JSON file. If any are found, it picks them up, rebuilds the site, and publishes.
Social interactions #
Bridgy is the service that people use to bridge social interactions on syndicated posts back to webmentions. Social sites like Mastodon, Bluesky, and Flickr do not natively support webmentions, so this is where services like Bridgy come in.
Since I'm choosing not to depend on third-party services, I rolled my own version of Bridgy. I went with Node.js scripts for this step, one for each of the social networks I currently collect reactions from. At the time of this writing, I'm polling the following networks:
- Mastodon
- Bluesky
- Pixelfed (not supported by Bridgy!)
- Flickr
They all work using the same pattern insofar as they use the service's API to access my account and collect any reactions for my syndicated posts. They then transform the reactions into the right shape and store them into the JSON store file in step 3 above. From there, the normal pipeline ingests and renders the reactions alongside standard webmentions.
Conclusion #
And that's how my site processes and renders incoming webmentions. Part 2 is in the works to explain the outgoing workflow for webmentions.
A couple of recent examples: how webmentions work on brennan.day and enriching webmentions from third-party platforms. ↩︎
Leave a comment