--- date: '2023-02-09' title: 'Adding client-side rendered webmentions to my blog' description: 'My blog is currently hosted on weblog.lol which allows for a simple and configurable weblog managed in git with posts formatted in markdown.' draft: false tags: ['webmentions', 'development', 'JavaScript', 'API'] --- My blog is currently hosted on weblog.lol which allows for a simple and configurable weblog managed in git with posts formatted in markdown. I wanted to add webmentions to my blog which, as of now, doesn't include a build step. To accomplish this, I've added an intermediary api endpoint to the same Next.js app that powers my [/now](https://coryd.dev/now) page. Robb has [a handy write-up on adding webmentions to your website](https://rknight.me/adding-webmentions-to-your-site/), which I followed — first adding the appropriate Mastodon link to my blog template, registering for webmentions.io and Bridgy, then adding the appropriate tags to my template document's `` to record mentions. Next it was simply a question of rendering the output from the webmentions endpoint. My Next.js api looks like this: ```typescript export default async function handler(req: any, res: any) { const KEY_CORYD = process.env.API_KEY_WEBMENTIONS_CORYD_DEV const KEY_BLOG = process.env.API_KEY_WEBMENTIONS_BLOG_CORYD_DEV const DOMAIN = req.query.domain const TARGET = req.query.target const data = await fetch( `https://webmention.io/api/mentions.jf2?token=${DOMAIN === 'coryd.dev' ? KEY_CORYD : KEY_BLOG}${ TARGET ? `&target=${TARGET}` : '' }&per-page=1000` ).then((response) => response.json()) res.json(data) } ``` I have a pair of keys depending on the mentioned and provides domain, though this is only used on my blog at present. I also support passing through the `target` parameter but don't leverage it at the moment. This is called on the client side as follows: ```javascript document.addEventListener('DOMContentLoaded', (event) => { ;(function () { const formatDate = (date) => { var d = new Date(date), month = '' + (d.getMonth() + 1), day = '' + d.getDate(), year = d.getFullYear() if (month.length < 2) month = '0' + month if (day.length < 2) day = '0' + day return [month, day, year].join('-') } const webmentionsWrapper = document.getElementById('webmentions') const webmentionsLikesWrapper = document.getElementById('webmentions-likes-wrapper') const webmentionsBoostsWrapper = document.getElementById('webmentions-boosts-wrapper') const webmentionsCommentsWrapper = document.getElementById('webmentions-comments-wrapper') if (webmentionsWrapper && window) { try { fetch('https://utils.coryd.dev/api/webmentions?domain=blog.coryd.dev') .then((response) => response.json()) .then((data) => { const mentions = data.children if (mentions.length === 0 || window.location.pathname === '/') { webmentionsWrapper.remove() return } let likes = '' let boosts = '' let comments = '' mentions.map((mention) => { if ( mention['wm-property'] === 'like-of' && mention['wm-target'].includes(window.location.href) ) { likes += `${mention.author.name}` } if ( mention['wm-property'] === 'repost-of' && mention['wm-target'].includes(window.location.href) ) { boosts += `${mention.author.name}` } if ( mention['wm-property'] === 'in-reply-to' && mention['wm-target'].includes(window.location.href) ) { comments += `
${mention.author.name}
${formatDate( mention.published )}
${mention.content.text}
` } }) webmentionsLikesWrapper.innerHTML = '' webmentionsLikesWrapper.insertAdjacentHTML('beforeEnd', likes) webmentionsBoostsWrapper.innerHTML = '' webmentionsBoostsWrapper.insertAdjacentHTML('beforeEnd', boosts) webmentionsCommentsWrapper.innerHTML = '' webmentionsCommentsWrapper.insertAdjacentHTML('beforeEnd', comments) webmentionsWrapper.style.opacity = 1 if (likes === '') document.getElementById('webmentions-likes').innerHTML === '' if (boosts === '') document.getElementById('webmentions-boosts').innerHTML === '' if (comments === '') document.getElementById('webmentions-comments').innerHTML === '' if (likes === '' && boosts === '' && comments === '') webmentionsWrapper.remove() }) } catch (e) { webmentionsWrapper.remove() } } })() }) ``` This JavaScript is all quite imperative — it verifies the existence of the appropriate DOM nodes, concatenates templated HTML strings and then injects them into the targeted DOM elements. If there aren't mentions of a supported type, the container node is removed. If there are no mentions, the whole node is removed. The webmentions HTML shell is as follows: ```html

Likes

Boosts

Comments

``` And there you have it — webmentions loaded client side and updated as they occur. There's an example visible on my post [Automating (and probably over-engineering) my /now page](https://blog.coryd.dev/2023/02/automatingandprobablyoverengineeringmy-nowpage#webmentions).