Have you heard of webmentions? They’re similar to pingbacks—but modern—and allow websites to notify each other about different types of activity (like replies on social media). As of 2017, the protocol is a W3C recommendation.

Here’s an example:

When I post a link to this article on Mastodon, Mastodon sends a request back to my website every time someone likes, reposts, or replies to it. I can then display that activity here on my own website (see the bottom of this article). When I post an article here, I can also send webmentions to any websites I mention (link to).

In the case of Mastodon, it’s actually a tad more complicated than that; because Mastodon doesn’t send webmentions natively, there’s a service I use—called Bridgy—to watch Mastodon and send me webmentions on its behalf.

The problem with webmentions and static sites

I wanted to add webmentions to my blog but had a problem. I use a static site generator (Jekyll) to build my website. That means I can’t receive webmentions and process them dynamically without using a 3rd-party service, like Netlify Functions. I want my blog to run forever, though, and I’ve found serverless functions challenging to maintain (Node.js, gross). Also, I’m not an expert on the Webmention protocol and didn’t want to maintain my own implementation.

Fortunately, there is a 3rd-party service specifically for handling webmentions: Webmention.io. Webmention.io is an endpoint for receiving webmentions on your behalf, providing an API that returns well-formatted JSON. There are even plugins for various static site builders, including Jekyll. Most of them query the Webmention.io API at build time and bake in the webmentions for each URL, and/or use client-side JavaScript to grab new webmentions on every page view.


I like my websites to be fast. That’s why I use a static site generator to render plain HTML pages in the first place—servers are much quicker when requesting static pages. I also like the generator to be fast, and querying an API (like Webmention.io) to fetch the webmentions for each page at build time is slow, even with caching. Since Jekyll can render JSON files directly from the file system, I really want to store my webmentions with the rest of my static files and even push them to my git repository for permanent archiving.

Webmention.io can also send Webhooks. Instead of querying an API every time I build my blog, I could listen for incoming webhooks, save the JSON with the rest of my files, and then regenerate the blog (or, better yet, just the mentioned page). For that, I’d still need a dynamic endpoint, but a much simpler one—it just had to authenticate an HTTP POST request and write the JSON data in the request body to disk.

Adding webmentions to this blog

I still didn’t want to use a 3rd-party service to host an HTTP endpoint, and I didn’t want to manage a server process; I wanted a cgi-bin. When I started building websites over 20 years ago, I used Perl and CGI to run simple scripts, like a guestbook (I wrote my own). I prefer Ruby these days—and Perl has deprecated CGI—but could that approach still work? I thought it would be fun to try. It turns out it does work!

By this point, I was full-on coding like it was 1999, and I needed a web server—so obviously, I reached for Apache. Apache still supports mod_cgi, and although deprecated, Perl still supports CGI (and, as a one-time popular legacy system, will probably continue to do so for some time.)

Since I was hosting my blog at Netlify (which cannot run Apache directly), I created a $5/month cloud server at Hetzner. After some basic security hardening, I installed Apache and configured it to serve Jekyll’s _site directory (where it saves the static files). I then added a simple Perl script to Apache’s cgi-bin directory, and after fiddling with file permissions, I had a working endpoint for processing webhooks from Webmention.io.


Here’s how the whole thing works:

  1. Apache serves the static _site directory generated by Jekyll.
  2. When Webmention.io receives a new webmention, it sends a webhook to /cgi-bin/webmention at my website, invoking a process to run my Perl CGI script.
  3. The Perl script authenticates the request; if valid, it saves a new file in Jekyll’s _data directory.
  4. I also run jekyll build --watch on the server, which regenerates the site when the file system changes.
  5. I periodically commit those new files and push them to GitHub for archiving.

That’s it. Now that I had new webmentions saved in my Jekyll project, I could display them using Jekyll’s Liquid templates. You can see all the webmentions for this article below the closing paragraph—and if you like or repost it on Mastodon, Reddit, or Bluesky, you’ll also appear here.


What about monitoring and reliability? My Perl script had a bug when I first tested this, meaning incoming webmentions were lost. Webmention.io doesn’t retry webhooks; even if they do, it’s nice to know when things are failing. For that, I use a product we built at work—Hook Relay. Hook Relay sits between Webmention.io and my site and retries webhooks if my script fails or my site is down. I can also see all of the sent requests, which is handy!

A screenshot of a chart and list of webhook events in Hook Relay