Coding with Jesse

Add Mastodon replies to your blog

You can now comment on blog posts on Coding with Jesse! I turned off comments years ago, because I was getting tons of spam. But recently, with my return to social media, I decided to integrate Mastodon to give people a way to comment on and interact with my articles.

Initially, I wasn't sure how I would accomplish this. Mastodon has a ton of servers, and Mastodon search can only search for hashtags, so how would I know whether someone commented on my article? And how would I integrate it into my website?

I looked around at how some other blogs were handling this, and came across Webmentions, and particularly, Webmention.io. It's a web standard for communicating interactions across web servers! Perfect!

I naively assumed that Mastodon would automatically hit my server with notifications any time someone favourited, boosted or replied to a post that contained a link to my site. But alas, Mastodon doesn't do that for privacy reasons (understandably).

Fortunately, I'm not the first one to run into this problem, and so there's a free service available that solves this problem. If you sign up for brid.gy, you can link your social media accounts, including Mastodon, and brid.gy will automatically send webmentions to your site whenever one of your posts contains a link to your site, and people reply to, boost or favourite your post. Essentially, your Mastodon posts become an anchor for all the interactions on your blog posts.

With these two services in hand, here's how you can integrate Mastodon into your website the way I did:

1. Sign in to webmention.io.

You need to sign in with your website URL, and your GitHub account. Also, your blog needs to link to that GitHub profile with either <a href="https://github.com/jesseskinner" rel="me"> or <link href="https://github.com/jesseskinner" rel="me">, to prove that you own the site.

2. Add webmention tags to your blog

When you sign in, go to https://webmention.io/settings. Under Setup, you'll see these two link tags:

<link rel="webmention" href="https://webmention.io/username/webmention" />
<link rel="pingback" href="https://webmention.io/username/xmlrpc" />

Copy these and paste them into the <head> on your blog. These will tell other services (like brid.gy) where they need to send webmentions for your posts.

Go to fed.brid.gy and, if you're like me, you'll want to click on "Cross-post to a Mastodon account", so that it'll integrate with your existing Mastodon account.

4. Post a link to a blog post on Mastodon

Try linking to your most recent blog post on Mastodon. If you already did this some time ago, brid.gy will scan your posts looking for links. You can also feed it a URL to a specific Mastodon post so that it will discover it.

Brid.gy will periodically poll your account looking for new interactions on these posts, and will send any new favourites, boosts or replies to webmention.io.

Note that your post doesn't count as a webmention - only the interactions on that post do. But you can reply to your own post as a way to trigger a webmention.

When I was setting this up, I was logged into both brid.gy and webmention.io, clicking the "Poll now" button on brid.gy and eagerly looking for interactions to show up. You have to have some patience here as well, as both services have a bit of a delay.

Once you see some mentions show up on webmention.io, you're ready to render them onto your blog.

5. Add the webmentions onto your website

Here's the trickier part. You'll need to hit the webmention.io API and fetch the mentions for your blog post. You can do this server-side, if you want. My blog is static, so I needed to do this client side.

Since the results are paginated, you can only get back 100 at a time. I wrote this function to help me retrieve all the pages at once, and sort the results into chronological order:

async function getMentions(url) {
    let mentions = [];
    let page = 0;
    let perPage = 100;

    while (true) {
        const results = await fetch(
            `https://webmention.io/api/mentions.jf2?target=${url}&per-page=${perPage}&page=${page}`
        ).then((r) => r.json());

        mentions = mentions.concat(results.children);

        if (results.children.length < perPage) {
            break;
        }

        page++;
    }

    return mentions.sort((a, b) => ((a.published || a['wm-received']) < (b.published || b['wm-received']) ? -1 : 1));
}

Then, I used the results of this to pull out four things: the favourites, boosts, replies, and also a link to the original post where I can send other visitors to my blog if they want to "Discuss this article on Mastodon". Here's how that looks:

let link;
let favourites;
let boosts;
let replies;

const mentions = await getMentions(url);

if (mentions.length) {
    link = mentions
        // find mentions that contain my Mastodon URL
        .filter((m) => m.url.startsWith('https://toot.cafe/@JesseSkinner/'))
        // take the part before the hash
        .map(({ url }) => url.split('#')[0])
        // take the first one
        .shift();

    // use the wm-property to make lists of favourites, boosts & replies
    favourites = mentions.filter((m) => m['wm-property'] === 'like-of');
    boosts = mentions.filter((m) => m['wm-property'] === 'repost-of');
    replies = mentions.filter((m) => m['wm-property'] === 'in-reply-to');
}

Of course, you should replace the link to my profile with the link to your own. I'm taking the first mention (after sorting chronologically) that is interacting with one of my posts, and linking to that post URL.

With those in hand, you'll have everything you need to render the replies, boosts and favourites to your blog. My approach was to render just the avatars of everyone who boosted or favourited my post, and all the replies.

One thing to watch out for is that the content of each reply is HTML. To be safe (paranoid), I'm running the HTML through sanitize-html to make sure nobody can inject sketchy HTML into my site.

6. Allow people to share posts without mentions

For any posts that don't have any mentions, I added a different button, "Share this on Mastodon". When you click it, it runs this code, which prompts you for your Mastodon server (inspired by Advent of Code's share functionality):

const server = prompt('Mastodon Instance / Server Name?');

if (server) {
    // test if server looks like a domain
    if (!server.match(/^[^\s]+\.[^\s]+$/)) {
        alert('Invalid server name');
        return;
    }

    const text = `"${post.title}" by @[email protected]\n\n${url}`;

    window.open(`https://${server}/share?text=${encodeURIComponent(text)}`);
}

Yay for Mastodon comments!

I'm really happy with how this turned out. To add some placeholders on my old blog posts, I posted some links to some of the more recent posts, so they interactions would have a place to live. For the older posts, I'm just relying on the share functionality.

I'm considering implementing some server-side functionality to replace either webmention.io or brid.gy in the future, so that the mentions live in my database instead of relying on a third-party service that may disappear one day. I think I could also skip the webmentions process by associating a Mastodon post URL with each blog post, and then using the Mastodon API of my server to periodically check for interactions and replies. Or maybe it could log in to my server and listen for notifications. But for now, this works really well.

So from now on, whenever I write a new blog post, like this one, I'm sure to share in on Mastodon and give a place for readers to ask questions or discuss the article. Check the end of this blog post to see how it all looks, and be sure to favourite, boost or reply to my Mastodon post so that you show up on the page as well!

Published on December 27th, 2022. © Jesse Skinner

Advent of Code 2022

I've been really enjoying working on this year's Advent of Code. If you haven't heard of it, it's a series of coding puzzles, two a day for 25 days, from December 1st to December 25th every year. It only started a few days ago, so it's not too late to catch up. Or, if you're reading this later, you can always go back and try it out at your leisure. But, it is a lot of fun to wait until midnight each day to see what the next puzzle is.

What's cool about it, is that you can use any programming language you want. You just need to take an input file, run a calculation based on the puzzle instructions, and come up with an "answer", which is usually a number of some kind.

You can use your favourite language to try to come up with an answer as fast as possible, or you can use it as an opportunity to strengthen your skills in another language, even a language you've never used before and want to try out!

You need to log in with GitHub, Google, Reddit or Twitter, and then you can download an input file for each day. You'll need to read the input file in to your language of choice, and parse and process each line of the file.

If you're really fast, you can even get on the leaderboard. But doing that requires completing the puzzles in just a few minutes at midnight EST so that you're one of the first 100 people to do so. I'm definitely not fast enough to even bother trying!

So far, I've been using JavaScript with Node.js this year. My approach is to pipe in the input into my puzzle solution like this:

node solution.js < input.txt

To do this, I'm using an npm library called split that simply splits a stream into lines, to make it easier to work with. Here's a simple example that just counts the number of lines in a stream:

import split from 'split';

// keep some variables in the module scope to keep track of things
let count = 0;

// read the input file as a stream from stdin (Standard Input)
process.stdin

    // pipe it to the split library, to split it into lines
    .pipe(split())

    // receive a callback for each line in the stream
    .on('data', (line) => {
        // do something with each line, in this case just counting
        count++;
    })

    // receive a single callback when the file is done
    .on('end', () => {
        // do something at the end, eg. console.log the output
        console.log(count);
    });

If you're interested, there is an online community on Reddit where you can share your solution and join in the discussion to see what others have done each day.

I'll be sharing my progress on Mastodon at @[email protected], so you can follow me on there for updates and commentary.

I've also been pushing my solutions to Advent of Code up to GitHub, so feel free to see how I've approached it, if you're interested. But no cheating! 😉

Published on December 4th, 2022. © Jesse Skinner