Last update 12/21/19

  • removed reply count buttons HTML and JS
  • adding a note about switching to native micro.blog webmentions in the future

Past updates

  • added <ul> for shares back in

Background

As part of getting back into using micro.blog I've been working to customize my hosted site. One of the things I wanted to do was add webmentions rather then comments like Disqus. Webmentions are coming to MB at some point and most themes already contain references to them:

<link rel="webmention" href="https://micro.blog/webmention" />

I didn't want to wait though and we don't need to thanks to Webmention.io! I'm going to try and document exactly what I did through this post. Parts of it will be specific to the theme I'm using, Hello in this case but, hopefully will have enough detail that anyone can follow.

To be clear, if you use this method you may lose some mentions sent to the external endpoint if you switch from webmentions.io back to the native micro.blog webmention endpoint once it's live

This isn't new territory but I thought it'd be nice to record exactly what I did to get this working on micro.blog. In fact, I hacked most of this in place based on code released by Keith Grant. Thanks Keith!

Pre-requisites

To get started you really only need a few things:

  1. a micro.blog hosted blog or a static website you want to add webmentions to
  2. a custom theme
  3. basic web development skills (some HTML/JavaScript)

As I mentioned in the lightbox post, @manton has a great YouTube video which covers the basics of getting up and running with a theme. I will be referring to the web based editor but you don't really need to use that if you want to make changes locally and then push to Git.

Webmention

First thing to do is to visit Webmention.io. Here we're going to paste in our Micro.blog URL.

webmention-01

After signing in we'll be brought to the dash board. We are currently only concerned about the to <link>'s in the setup section.

webmention-02

HeadSpace

Step one out of the way we'll need to edit our theme header to include these links. In this example I'm using the Hyde theme. From here on this will vary from theme to theme but hopefully not too much.

We need to, at this point make sure we've read up on custom themes. I can wait, take your time. Go head and switch over to a custom theme if you aren't already using one.

Alight lets continue.

To get to the micro.blog theme editor goto your “Posts” page, then click on “Design”. Scroll down and click on “Edit Custom Themes”. Now choose the theme you want to edit.

You should now see something similar to the following.

webmentions-03

We'll first edit layouts/partials/head.html. Again this may be slightly different for the theme you are using. Our first change is to pull out the micro.blog webmentions placeholder. So find:

webmentions-04

<link rel="webmention" href="https://micro.blog/webmention" />

Found it! Now paste our webmentions.io links in it's place. For me that would be:

<link rel="webmention" href="https://webmention.io/shindakun-test.micro.blog/webmention" />
<link rel="pingback" href="https://webmention.io/shindakun-test.micro.blog/xmlrpc" />

Click “Update Template” button to save changes. One down.

HTMLing

Alright, now we should get some stuff into the template in order to actually show the mentions. This will be kind of barebones and may not be very pretty, I'll leave the CSSing to the reader - that comes later though. For now we need to edit our single post HTML. For Hyde that is layouts/post/single.html.

webmentions-05

If you look at the screenshot above you'll see about where we are going to add our HTML. For now we'll put it after the <div> that is just under {{ .Content }}. And below you can find that HTML - I decided it was easiest to just include it here, even though its quite a big block.

<!-- First we have out reply and like template HTML -->
<template id="reply-template">
  <li class="h-entry">
    <div class="comment-note">
      <div class="comment-note__avatar">
        <a class="js-author" href="">
          <img class="comment-avatar u-photo js-avatar" src=""/>
        </a>
      </div>
      <div class="comment-note__body">
        <div class="p-author">
          <a class="comment-author u-author js-author-name" href=""></a>
          <a class="comment-timestamp u-url js-date js-source" href=""></a>
        </div>
        <div class="comment-body js-content e-entry"></div>
      </div>
    </div>
  </li>
</template>
<template id="like-template">
  <li class="h-entry">
    <div class="reply h-card p-author">
      <a class="reply__bar u-url js-source" href="">
        <div class="reply__author p-name js-sentence js-author-name" href=""></div>
        <div class="reply__date js-date" href=""></div>
      </a>
      <a class="reply__avatar u-author js-author" href="">
        <img class="u-photo js-avatar" src=""/>
      </a>
    </div>
  </li>
</template>

<!-- The webmentions.js file we look at in a bit will fill out our lists -->
<ul class="replies" id="replies"></ul>
<ul class="likes" id="likes"></ul>
<ul class="shares" id="shares"></ul>

<div id="comments"></div>

<!-- I'm including this here to make sure it's only used on the single post page. We'll get into this a bit later. -->
<script type="text/javascript">
  document.addEventListener('DOMContentLoaded', function () {
    fetchWebmentions({{ .Permalink }}, {{ .Aliases }});
  });
</script>

Once more, click on “Update Template” to save our changes. Saving brings us back to the template file list - which works out quite nicely.

JavaScripty

On the file list screen click the “New Template” button. This should really say “New File” since we're not creating a whole new template but… We should now be looking at an empty text-box. This time we need to note where we want this file to be, in my case to stick with the theme convention I'll use static/js/webmentions.js. This path may change slightly depending on how the theme or you want it to be - for instance in the Hello theme it might be static/assets/webmentions.js.

webmentions-06

Into the actual code text-box we'll need to paste the following JavaScript. There's quite a bit. I've tested this on two micro.blog sites and it seems to work OK, your milage may vary though. I'm going to put a few comments in just to try and describe a bit of what's happening in places. I'm not going to go to crazy though so a little JavaScript to general programming knowledge is probably helpful here.

You'll need to supply you're own anonymous avatar image for any mentions that don't have an image.

// You will need to supply an image, for MB hosted blogs I
// recommend uploading  to the image gallery and then pasting
// the link in here
const ANON_AVATAR = '/images/anon-avatar.png';

// fetchWebmentions retireves the actual messages for the URL
// passed in. First we check to see if we're on a page that
// has a comment `div` and then if no URL is passed in we'll
// put one together from the current page and fetch webmentions.
function fetchWebmentions(url, aliases) {
  if (!document.getElementById('comments')) {
    return;
  }
  if (!url) {
    url = document.location.origin + document.location.pathname;
  }
  const targets = getUrlPermutations(url, aliases);

  var script = document.createElement('script');
  var src =
    'https://webmention.io/api/mentions?perPage=500&jsonp=parseWebmentions';
  targets.forEach(function(targetUrl) {
    src += `&target[]=${encodeURIComponent(targetUrl)}`;
  });
  src += `&_=${Math.random()}`;
  script.src = src;
  script.async = true;
  document.getElementsByTagName('head')[0].appendChild(script);
}

// getUrlPermutations builds up a list of potential URLs to
// check for mentions on. You will need to update the URLs
// to point to your own MB instance. The `localhost:1313` one
// can probably be removed unless your going to be testing
// the theme locally with Hugo.
function getUrlPermutations(url, aliases) {
  const urls = [];
  url = url.replace('http://localhost:1313', 'https://shindakun-test.micro.blog/');
  urls.push(url);
  urls.push(url.replace('https://', 'http://'));
  if (url.substr(-1) === '/') {
    var noslash = url.substr(0, url.length - 1);
    urls.push(noslash);
    urls.push(noslash.replace('https://', 'http://'));
  }
  if (aliases) {
    aliases.forEach(function(alias) {
      urls.push(`https://shindakun-test.micro.blog/${alias}`);
    });
  }
  return urls;
}

function parseWebmentions(data) {
  var links = data.links.sort(wmSort);
  var likes = [];
  var reposts = [];
  var replies = [];
  links.map(function(l) {
    if (!l.activity || !l.activity.type) {
      console.warning('unknown link type', l);
      return;
    }
    if (!l.verified) {
      return;
    }
    switch (l.activity.type) {
      case 'like':
        likes.push(l);
        break;
      case 'repost':
      case 'link':
        reposts.push(l);
        break;
      default:
        replies.push(l);
        break;
    }
  });
  renderLikes(likes);
  renderReposts(reposts);
  renderReplies(replies);
  showInteractions();
}
window.parseWebmentions = parseWebmentions;

function wmSort(a, b) {
  const dateA = getWmDate(a);
  const dateB = getWmDate(b);
  if (dateA < dateB) {
    return -1;
  } else if (dateB < dateA) {
    return 1;
  }
  return 0;
}

function getWmDate(webmention) {
  if (webmention.data.published) {
    return new Date(webmention.data.published);
  }
  return new Date(webmention.verified_date);
}

var months = [
  'Jan',
  'Feb',
  'Mar',
  'Apr',
  'May',
  'Jun',
  'Jul',
  'Aug',
  'Sep',
  'Oct',
  'Nov',
  'Dec',
];

function renderLikes(likes) {
  var t = document.getElementById('like-template').content;
  var list = document.getElementById('likes');
  likes.map(function(l) {
    console.log(l)
    fillTemplate(t, {
      photo: l.data.author.photo || ANON_AVATAR,
      name: l.data.author.name,
      authorUrl: l.data.author.url,
      url: l.data.url,
      sentence: l.activity.sentence,
      date: new Date(l.data.published || l.verified_date),
    });
    var clone = document.importNode(t, true);
    list.appendChild(clone);
  });
}

function getHostName(url) {
  var a = document.createElement('a');
  a.href = url;
  return (a.hostname || '').replace('www.', '');
}

function renderReposts(reposts) {
  var t = document.getElementById('like-template').content;
  var list = document.getElementById('shares');
  reposts.map(function(l) {
    let data;
    if (l.data.author) {
      data = {
        photo: l.data.author.photo || ANON_AVATAR,
        name: l.data.author.name,
        authorUrl: l.data.author.url,
        url: l.data.url,
        date: new Date(l.data.published || l.verified_date),
      };
    } else {
      data = {
        photo: ANON_AVATAR,
        name: getHostName(l.data.url) || 'inbound link',
        authorUrl: l.data.url,
        url: l.data.url,
        date: new Date(l.data.published || l.verified_date),
      };
    }
    fillTemplate(t, data);
    var clone = document.importNode(t, true);
    list.appendChild(clone);
  });
}

function renderReplies(replies) {
  var t = document.getElementById('reply-template').content;
  var list = document.getElementById('replies');
  replies.map(function(l) {
    let data;
    if (l.data.author) {
      data = {
        photo: l.data.author.photo || ANON_AVATAR,
        name: l.data.author.name,
        authorUrl: l.data.author.url,
        url: l.data.url,
        date: new Date(l.data.published || l.verified_date),
        content: l.data.content,
      };
    } else {
      data = {
        photo: ANON_AVATAR,
        name: getHostName(l.data.url) || 'inbound link',
        authorUrl: l.data.url,
        url: l.data.url,
        date: new Date(l.data.published || l.verified_date),
        content: l.data.content,
      };
    }

    fillTemplate(t, data);
    var clone = document.importNode(t, true);
    list.appendChild(clone);
  });
}

// fillTemplate marries a signle webmention to the
// template. The completed template is used in the
// render functions to actually display on the page.
function fillTemplate(template, vals) {
  template.querySelector('.js-avatar').src = vals.photo;
  template.querySelector('.js-author').href = vals.authorUrl;
  template.querySelector('.js-author-name').innerHTML = vals.name;
  template.querySelector('.js-author-name').href = vals.authorUrl;
  template.querySelector('.js-source').href = vals.url;
  if (vals.sentence) {
    template.querySelector('.js-sentence').innerText = vals.sentence;
  }
  const date = template.querySelector('.js-date');
  if (date) {
    date.innerHTML = formatDate(vals.date);
  }
  if (vals.content) {
    template.querySelector('.js-content').innerHTML = vals.content;
  }
}

// formatDate formats the date.
function formatDate(date) {
  if (!date) {
    return '';
  }
  return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`;
}

// This is leftover from the original implmentation,
// commented out since I don't use it in my version.
// Maybe I should just remove it for clarity sake.
// ¯\_(ツ)_/¯
function showInteractions() {
 // document.getElementById('comments-loader').classList.add('is-hidden');
 // document.getElementById('comments').classList.remove('is-hidden');
}

I recommend reading it through even if you don't understand all of it …

Now save the file which brings us back to the file list.

Back to the Head

JavaScript in place we now need to go back into the header file layouts/partials/head.html. Scroll all the way down and just before the </head> past in:

<script src="{{ "js/webmentions.js" | relURL }}"></script>

And save the file.

webmentions-07

Mission Accomplished

We did it! Technically we're all done at this point. If we were to look at a post on our side we should see something like the following from my test site

webmentions-08

Neat!

Alright but how can we tell if it's really working? Webmention Rocks will help us make sure everything is actually working as it should. The site has a number of tests to ensure your site is set up and ready to go. But for the purposes of this post we are mostly interested in the receiver tests. First one is first I suppose - visit https://webmention.rocks/receive/1 and enter the URL of your site to sign-in.

webmentions-09

Perfect! Now lets enter a URL from one of our MB posts.

webmentions-10

If all went well you should see something similar to the following image.

webmentions-11

Awesome! Now go over to the same page on MB and refresh…

webmentions-12

Yes the image Webmention Rocks uses is pretty big by default but, who cares we have one webmention! Alright, I care… For best results you'll want to use some CSS to style the replies. I have left that out since that's a but outside what we are trying to do and I don't really enjoy mucking about with CSS.


Enjoy this post?
How about buying me a coffee?