I spent a bit of time over the past week and weekend learning a bit more about webmentions. I finally learned enough to get them enabled on this site. You can see the first mention displayed on my ai naming post. I thought I would write up a post showing how I got mentions working.

Receiving

Initially, I had plans of writing my own webmention receiver, but after some consideration I decided that using an open source tool would be faster. I did not find a lot of options, and I settled on installing webmentiond. This seems to be a solid implementation that is written in Go and can be fairly easily hosted using Docker which met my requirements. I have yet to run into any missing features I care about, but knowing I can learn some Go while extending it is a plus for me.

After getting this up and running, I was able to successfully start receiving webmentions.

Displaying

Webmentiond provides a small Javascript snippet that can fetch webmentions from your server based on a page’s url. This is basically a JS widget. Since one of my goals for this site is not running any required javascript, this approach did not work for me. I ended up adding functionaity to my Astro site to include the mentions at build time.

CI Authentication

Happily webmentiond provides a simple api that we can use to fetch all mentions for your site with 2 requests. The first request uses what is technically a preshared key to generate a access token. This key should be generated by you, stored security, and added to the startup uption for webmentiond. This looks like:

--auth-admin-access-keys YOUR_GENERATED_KEY=ci

Note that the =ci portion is just an identifier, it can be anything, but ci works well since the main use is to integrate this into a Drone build process.

Fetching your mentions

Now that we have a preshared key, you will want to add two new ENV variables. For development purposes I just added these to a .env file in the root of my astro projects.

PUBLIC_WEBMENTIOND_TOKEN = The url that is used to access your webmentiond instance 
PUBLIC_WEBMENTIOND_URL = YOUR_GENERATED_KEY

Then we can fetch the mentions. The code for this looks like*.


let promise: Promise<Response>;
let fetching = false;
import type { Response } from "./webmentions";
export default async function(approved=true) {
    if(!fetching) {
        fetching = true;
        
        promise = new Promise(async (resolve) => {
            const mentions = await fetchMentions()
            if(approved) {
                resolve({items: mentions.items.filter((mention) => {
                    return mention.status === "approved"
                })});
                return
            }
            
            resolve(mentions);
        })
    }
    return promise;
}

async function fetchJWT(){
    return await (await fetch('https://wm.connermccall.com/authenticate/access-key', {
        method: "POST", // Using POST method
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: `key=${import.meta.env.PUBLIC_WEBMENTIOND_TOKEN}`,
    })).text();
}

async function fetchMentions(): Promise<Response>{
    const jwt = await fetchJWT();
    return await (await fetch('https://wm.connermccall.com/manage/mentions', {
        headers: {'Authorization': `Bearer ${jwt}`},
    })).json()
}

In this code you’ll notice I wrap the fetch in a Promise. This prevents the Fetch from running for every single post. This approach caches the first request and reuses the response for all future posts. The chain of requests is to first use the long lived token to fetch a JWT token, then use that JWT token to fetch the mentions.

One thing you may want to do from a security standpoint on your webmentiond service is set --auth-admin-access-key-jwt-ttl to something like 10s or 30s. Since we do not reuse this key it should expire almost immediatly after we use it.

*Technically I should have made these a data content type in Astro

Displaying mentions

Now that we have a way to fetch our mentions, we need to actually display them. In this case I created a new .astro file that exports a React component to fetch and display the mentions based on a slug.


import fetch_webmentions from "../lib/fetch_webmentions";
import Like from './mentions/Like.astro';
import Reply from './mentions/Reply.astro';
import Repost from './mentions/Repost.astro';
import Mention from './mentions/Mention.astro';
import Rsvp from './mentions/Rsvp.astro';
import Comment from './mentions/Comment.astro'
interface Props {
	slug: string;
}

const { slug } = Astro.props;

const mentions = (await fetch_webmentions()).items.filter((mention) => {
    return mention.target.indexOf(slug) > -1
});

---
<div class="webmentions mx-auto mb-2 text-base">
    {mentions.length > 0 &&  <h2 class="text-lg">Mentions</h2>
 <ul class="mb-4 text-base ">
    {mentions.map((mention)=> ( 
        <li class="mb-4 border-b">
            {mention.type && mention.type === 'like' && <Like mention={mention} />}
            {mention.type && mention.type === 'reply' && <Reply mention={mention} />}
            {mention.type && mention.type === 'repost' && <Repost mention={mention} />}
            {mention.type && mention.type === 'rsvp' && <Rsvp mention={mention} />}
            {mention.type && mention.type === 'comment' && <Comment mention={mention} />}
            {mention.type && mention.type === 'mention' && <Mention mention={mention} />}
            {mention.type === undefined && <Mention mention={mention} />}
        </li>
    
    ))}
    </ul>}
    <p><a class="text-sm text-gray-600" href="/replying">How to reply to this post</a></p>
    </div>

Now in my BLogPost.astro layout, I can just add <Mentions slug={slug} />

This component fetches the mentions, filters for the mentions with a target that matches the given slug, then displays the mention using a specific component based on the type. This makes it extremely easy to work with a single mention type, add a new type, or even remove a type all together. Based on my reading the six types I chose should be the most common types you would encounter.

A lot of folks are using Webmention.io to handle receiving webmentions. If you go that route, you would need to modify fetch_webmentions to use their endpoint for fetching all links on your domain. You’ll likely need to modify the rest of the code to match what the JSON format for the mentions looks like.

You can see the code for my site, including the webmention pieces (on github)[https://github.com/sloped/astro-site].

How to reply to this post