Implementing Webmentions on this blog

I never got around to implementing a comment system on this blog. After migrating from Wordpress to Jekyll and Github Pages (a static site) it was never someting that really interested me enough to spend time on. I briefly looked into a client side comment systems like Disqus but never actually took it any further than the research phase. I’m glad about that considering the hit Disqus has on web performance and issues data ownership. Things like:

  • loading 450KB of data, most of which is JavaScript
  • loading takes place from multiple different domains
  • poor cache optimisation for loaded content
  • you don’t own the comments, they are stored on a third-party server

There’s an excellent post here by Nicolas Hoizey who goes into these issues and others in much more detail if you’d like to know more. So no Disqus comments. What’s the alternative?

Introducing Webmentions

I must admit I hadn’t heard of Webmentions until fairly recently, but once I did I thought they sounded very interesting. So I’ve spent the past few weeks looking into implementing them on this blog. So what are they you may ask? Well I’ll let the W3C specification explain that:

Webmention is a simple way to notify any URL when you mention it on your site. From the receiver’s perspective, it’s a way to request notifications when other sites mention it.

Essentially they are quite similar to the WordPress Pingback and Trackback systems, which allowed WordPress blogs to talk to each other. For example, a blog post written on one WordPress blog could tell another that it had linked to it using these systems. Unfortunately the systems were hijacked by spammers resulting in its decline. Great idea, ruined by a certain demographic of users on the web. This is why we can’t have nice things…

The first W3C Public Working Draft was published on 12 January 2016 by Aaron Parecki, and it has grown in popularity ever since. Webmention offers a number of advantages over Pingbacks and Trackbacks:

  • it uses HTTP and x-www-form-urlencoded content, a much more widely accepted format compared to XMLRPC used by PingBacks and Trackbacks
  • faster and easier to integrate due to use of these accepted formats
  • W3C standard, rather than a proprietary system (bundled with WordPress)
  • spam protection using the Vouch protocol
  • human readable format rather than a simple title and an ellipsed summary as was the case with Pingbacks

If you want to know more about Webmentions, checkout this post by Jeremy Keith and this ALA article by Chris Aldrich.

My implementation

As mentioned before I use Jekyll via GitHub pages to compile a set of posts and templates into static HTML pages. It works well, but a major issue with GH pages is that you can’t use custom plugins. You can only use those listed on this page. That means jekyll-webmention_io isn’t an option, unless I plan to completely change my workflow. This isn’t something I’m willing to do at the moment as I plan to migrate to 11ty at some point in the future. I will leave that large change until then. So I decided to look into a client-side only alternative.

I stumbled across the article “Adding Webmention Support to a Static Site” by Keith Grant, which was incredibly useful. His code wasn’t quite what I wanted, so I decided to adapt it a little more for my needs. Also, I don’t get to write code as much as I used to, so it was a good opportunity to write a little JavaScript again!

This is what I wanted from my implementation:

  • progressive enhancement
  • minimal impact on web performance
  • no external dependencies and minimal impact on the build pipeline

I’ll go into each of these points individually.

Progressive enhancement

Working at Government Digital Service (GDS) I’m often involved in a lot of discussions about progressive enhancement and why it is the approach we should (and are) using. At it’s heart, progressive enhancement is focused on building the most resilient website you can through the use of layering technologies on top of each other. Build your core functionality using only HTML, layer on the CSS, then enhance further using JavaScript. If a user has a device that supports the latest features, they get the full experience. A user on an older device, or if something fails on the page (e.g. the JavaScript), these users won’t get the full experience, but they will get an experience. If built correctly they should still be able to complete the core task they came to the website for.

The core experience of this blog is for a user to be able to visit the site and read the content. A user can do this if the JavaScript fails (or is disabled). They can even read the content should the CSS fail (or be disabled). Give it a try some time. It is the ultimate minimalist responsive layout. Without JavaScript, a user gets to submit a Webmention via the form provided on each post page. If JavaScript loads and executes they get the enhanced experience. All Webmentions and comments are loaded, and the Webmention form submits via Ajax rather than a standard form post.

Minimal impact on web performance

I wanted to keep the script as small as possible as this is one way to minimise the performance impact on devices. But I also acknowledge that not all users are going to read the whole post. They may just be skimming, or land on the incorrect page. So why burden these users with JavaScript for content they aren’t going to see. This is where the use of the IntersectionObserver API comes in handy. It’s an API that allows you to read the position of a DOM element relative to the top-level viewport. It is often used to lazy-load images and scripts. Only once a user reaches a particular point in the page will an image load or script execute. For this blog I’ve set the bottom of a post to be the point where the script is loaded and executed.

There’s a small app.js file (1.1KB minified, 686 bytes gzipped) that does a few things. It performs a very simplistic version of “cutting the mustard”, by testing the availability of the IntersectionObserver API. If the browser passes, there is then a test to see if certain ES6 (ES2015) features are supported (more on that later). Assuming the browser passes both tests, the loading of the webmentions.js script is primed (6.4KB minified, 2.1KB gzipped). If a user now scrolls to the bottom of the page, the webmentions.js script is injected in to the page and it executes. This in turn loads all the Webmentions that the post has (if any).

The webmentions.js script has a couple of performance considerations too. All the templates for the replies, likes, and reposts are contained within this file, rather than using a <template> tag. This means that these templates are only loaded if a users browser passes both tests, and the user scrolls to the bottom of the page. This gives a slight saving in initial HTML size for all users, no matter what browser they use.

The second consideration is related to the profile images that are retrieved for each Webmention. By default, each of these profile images is large in terms of physical dimensions and page weight. Now considering we could be loading quite a few of these profile images, and they are only displayed at thumbnail size, that’s a lot of pixels and data that will be loaded that isn’t needed. To solve this I’ve made use of the Cloudinary free tier. The Cloudinary API allows you to send an image URL to their servers, which then responds back with an optimised (and resized) image for use on a page. These optimised images are cached on their server once generated, so image manipulation / optimisation only occurs once.

No external dependencies and minimal impact on build pipeline

It was very important to me not to require any external dependencies when creating this implementation. DOM manipulation would have been so much easier if I’d loaded a version of jQuery, but do I really need to load 30KB of minified and gzipped JavaScript just for DOM manipulation and Ajax? Not at all. So all code was written in plain old vanilla JavaScript. If this is something you are considering I recommend checking out You might not need jQuery and You Don’t Need jQuery!.

And lastly I didn’t want to have to modify my build process to be able to use new ES6 (ES2015) features. The usual method for using modern JavaScript syntax is to use Babel. You input JavaScript with the latest features and get ES5 browser compatible JavaScript out the other end. Now since I’m only using a few modern features (const, let, template literals), and I plan to completely overhaul the build pipeline when migrating to 11ty in the future, this is something I decided to avoid for the moment. Hence the inclusion of the test for ES6 (ES2015) features mentioned earlier. A browser will only get to the “modern” JavaScript features if it passes both the IntersectionObserver test and the ES6 (ES2015) test.

Show me the code already!

I’m sure you’re bored of my ramblings by now and all you want is the code. So here it is:

The code contains lots of comments and links to sources where I have copy & pasted others code (isn’t open source great!). You can see it in action if you scroll to the bottom of this page on WebPageTest waterfall charts. If you spot any improvements or idiotic errors in the code (or this post), either leave a comment on the gist or send me a Webmention! I’m using Bridgy, so a mention on Twitter should work too.

Loading

Webmentions