Supporting webmentions on a Quarto blog

Add Webmention support to your Quarto blog to enable indieweb interactivity
quarto
indieweb
Author

Alan Schussman

Published

January 15, 2024

Quarto blogs have built-in support for comments via several mechanisms, but not (yet) for Webmentions. Supporting Webmentions allows for additional interactivity from around the web by aggregating comments, replies and likes on a post from other sources. With Webmention support, Quarto blogs can interoperate with the broader indieweb! Fortunately, it’s not too complex to add this support yourself.

Robb Knight has a really clear and useful writeup of webmentions at his (beautiful, impeccably designed) website, highly recommended for further reading.

Webmentions for Quarto require a few components:

  • A webmention endpoint: This is the service that receives requests to post a Webmention to a specific URL. I use webmention.io.
  • A designation within given posts that identify their content as possible Webmention targets
  • Code that displays Webmentions on a given page

The bits below in my site’s _quarto.yml assemble some of the pieces:

format:
  html:
    include-in-header:
      - text: <link rel="stylesheet" href="https://alanschussman.com/datablog/webmention-io.css">
      - text: <span class="syndications"></a><link rel="webmention" href="https://webmention.io/alanschussman.com/webmention" /></span>
      - text: <link rel="me" href="https://github.com/ats">
    include-before-body:
      - text: <div class="h-entry e-content entry-content">
    include-after-body:
      - text: </div> <!-- end h-entry and e-content -->
      - file: webmention-insert.html

format:
  html:
    include-in-header:
      - text: <link rel="stylesheet" href="https://alanschussman.com/datablog/webmention-io.css">
      - text: <span class="syndications"></a><link rel="webmention" href="https://webmention.io/alanschussman.com/webmention" /></span>
      - text: <link rel="me" href="https://github.com/ats">
    include-before-body:
      - text: <div class="h-entry e-content entry-content">
    include-after-body:
      - text: </div> <!-- end h-entry and e-content -->
      - file: webmention-insert.html

In the include-in-header section, I reference a new stylesheet for the webmentions, and I identify the webmention.io service as my endpoint service.

webmention.io handles receiving requests and then serves them back to me when a page requests them.

In order to authorize webmention.io, you need the last statement in that include section: The rel="me" statement identifies the authorization endpoint that tells webmention.io that you are who you say you are. When you activate webmention.io, you’ll be redirected over to authorize yourself just as you would when setting up an API authorization.

The include-before-body and include-after-body statements serve to wrap each post on the blog in a new HTML div with a specific microformats h-entry. This is necessary for webmention services to be able to identify content that can receive a mention. (Note that this is just about the most minimal possible enablement for webmentions: There’s a whole range of microformats that enhance the way indieweb tools parse and display content, and this is a just barely-viable implementation.)

Finally, I include a file, webmention-insert.html, after the body of each post. This file contains a bit of Javascript to retrieve and parse mentions for the given URL, and then format that output at the bottom of the post. I’m including that code here. I wish I could remember the lineage of this code: I’ve been tending it along a couple-three blog generations and know that some of it comes from authors around the Webmention or Micro.blog communities.

<div class="mentionsio">
  <ul id="facepile">
  </ul>
  <ul id="reposts">
  </ul>
  <ul id="mentions">
  </ul>
</div>


<script type="text/javascript">
var url_string = window.location.href;

const facepile = document.getElementById('facepile');
const ul = document.getElementById('mentions');
const reposts = document.getElementById('reposts');
const wm = 'https://webmention.io/api/mentions?target='
const fetchargs = '&per-page=200&sort-dir=up';
let fetchurl = `${wm}${url_string}${fetchargs}`;
let facelist = "";
let item = "";
let content = "";
let repostlist = "";

fetch(fetchurl)
    .then((response) => response.json()) 
    .then(function(data) {
        let links = data.links;
        return links.map(function(link) {
            // validate some json
            let pubdate = "&infin;";
            let content = "";

            if (link.data.published) { pubdate += new Date(link.data.published) } else if (link.verified_date) { pubdate += new Date(link.verified_date) }
            if (link.data.name) { content = link.data.name } else {content = link.data.content }

            // do reply/link/bookmark
            if ((link.activity.type == "reply") || (link.activity.type == "link") ||  (link.activity.type == "bookmark") || (link.activity.type == null)) {
if (item == "") { ul.innerHTML = "<li id=\"rotated\">COMMENTS &<br /> MENTIONS"; }
                item = `<li class="p-comment h-cite comment"><span class="p-author h-card"><a class="p-name u-url" href="${link.data.author.url}"><img class="img-mention u-photo" style="margin-right: 10px;" src="${link.data.author.photo}" alt="${link.data.author.name} - avatar picture" > ${link.data.author.name}</a></a> <a href="${link.data.url}" class="pubdate">${pubdate}</a><div class="mentionscontent"><p>${content}</p> </div><br clear=left /> `;
            ul.innerHTML += item;
            }
            // do repost
            if (link.activity.type == "repost") {
                if (repostlist == "") {
                    reposts.innerHTML = "<li id=\"rotated\">REPOSTS";
                }
            repostlist = `<li class="p-repost h-cite"><a href="${link.data.url}" class="u-url"><span class="p-author h-card"><img class="img-mention u-photo" src="${link.data.author.photo}" alt="avatar image for ${link.data.author.name}"></span></a>\n`;
            reposts.innerHTML += repostlist;
            }
            // do likes
            if (link.activity.type == "like") {
                if (facelist == "") { facepile.innerHTML = "<li id=\"rotated\">LIKES &<br /> FAVES"; }
                facelist = `<li class="p-repost h-cite"><a href="${link.data.url}" class="u-url"><span class="p-author h-card"><img class="img-mention u-photo" src="${link.data.author.photo}" alt="avatar image for ${link.data.author.name}"></span></a>\n`;
                facepile.innerHTML += facelist;
            }
        })
        document.write(facelist);
    });
</script>

Receiving Webmentions

With the above code, a Quarto blog is wired to display Webmentions, and so now I need to establish a way for my blog to receive them. Put differently, your blog posts need to exist somewhere that Webmentions can be created. There are lots of ways to do this. Here are four. I’m stopping short of a full tutorial here, but am happy to elaborate if someone starts to walk through this and gets stuck:

  1. Syndicate your blog using Micro.blog: If you use Micro.blog, you can add your Quarto blog’s RSS feed to your account, and then Micro.blog will syndicate your posts automatically. Any reply to your post on the Micro.blog timeline will be treated as a Webmention. Micro.blog can even automatically cross-post to other services like Mastodon; and if you use brid.gy, discussion from those services can be treated like Webmentions![^bridgy]

You really should check out Micro.blog. It’s great!

  1. Use brid.gy to get Webmentions from social media and other platforms: When Brid.gy recognizes a reply to a post that it is tracking as syndicated from your own site, it handles the Webmention activity that the host site doesn’t innately perform, pinging your Webmention endpoint on your behalf. How does it know? You tell Brid.gy about your social media and other platform accounts1, and it periodically polls them for updates; when you post a link from the former to something on your blog, Brid.gy detects it and treats it as a syndicated post, and then sends a mention when it sees like and reply activity. This allows for creating Webmentions from Mastodon, GitHub, Flickr, Bluesky, and more.

  2. Add syndication links to specific posts. This is a little more fiddly, and also relies on brid.gy: Imagine you don’t want to syndicate via a link back to your site, but still want to enable mentions (this is a more common scenario with micro blogging posts). You just make your post to the third-party site, and then place the permalink of that post into the header yaml of your Quarto blog post, like:

format:
  html:
    include-before-body: 
      - text: '<a class="u-syndication" style="display: none" href="https://social.lol/@alans/[..rest of my permalink..]">https://social.lol/@alans/[..rest of my permalink..]</a>'

Then you can tell brid.gy about the original post, and when it finds the u-syndication link in it, it will watch that URL for replies and other activity that it can treat as Webmentions.

  1. Other indieweb folks can craft posts that cite or respond to your blog. This is truly the mode of hand-crafted, bespoke web content, and the detail is a little beyond the scope of this already-unwieldy blog post, but the gist is: Any web page can be a Webmention! It just needs a link to a second URL and optionally some specific tags that identify it as being a reference type like a reply or like. You can send an http request to the host blog’s webmention endpoint (like an address at webmention.io) via a shortcut, bookmark, CLI app or shell command to ping target from mention like: curl -i -d source=[mention] -d target=[target] https://webmention.io/[webmention_endpoint], and if the webmention receiver detects a link to the specified target, it processes and stores the mention.

If you made it this far, you just might be interested in hooking up your own Quarto blog to the indieweb using Webmentions. If you do, consider sending a reply via Webmention! Happy Quarto-blogging, data friends.

Footnotes

  1. When you set up brid.gy, you essentially tell it what other blogs, web sites, or social media accounts are yours by establishing links between them. Placing the links demonstrates that you own them, so brid.gy can then begin to syndicate between them. Brid.gy can even post to things like Mastodon for you, fully performing the syndication both ways.↩︎