Webmentions with Livewire on a Statamic Site

Published December 10th, 2020
18 minute read
Warning!
This was written over two years ago, so some information might be outdated. Frameworks and best practices change. The web moves fast! You may need to adjust a few things if you follow this article word for word.

Webmentions are a protocol that allows sites to track when links are made to them and to receive notifications of those links. They are a neat way to keep tabs on the kinds of interactions related to a website or article, such as likes, retweets, comments, reposts, etc...

This article will explain how I added them to articles on my site, so you can too!

Prerequisites

To integrate webmentions on your site, you first need to set them up to be sent and received. I followed this article by Sebastian De Deyne. Tracking webmentions requires two moving parts:

  • Scanning for new webmentions on your content and sending them out
  • Receiving webmentions from sources which send them

The quickest way to accomplish this is by using Bridgy (sending) and Webmention.io (receiving). I'm sure you could set up your own webmention services, but given how easy and free both of those are, why would you?

Sign up for both services. I linked them to Twitter because that's what I'm most interested in tracking, but you could assign Bridgy to look at other services too if you'd like. If your website isn't in your Twitter profile, you'll need to put it there.

Next, I had to put these <link> tags in my site's header.

1<link rel="webmention" href="https://webmention.io/austencam.com/webmention" />
2<link rel="pingback" href="https://webmention.io/austencam.com/xmlrpc" />
1<link rel="webmention" href="https://webmention.io/austencam.com/webmention" />
2<link rel="pingback" href="https://webmention.io/austencam.com/xmlrpc" />

Livewire + Statamic

Thanks to Jonas Siewertsen, this integration is already done for us. Install the Statamic Livewire addon via Composer:

1composer require jonassiewertsen/statamic-livewire
1composer require jonassiewertsen/statamic-livewire

Then add {{ livewire:styles }} to the <head></head> tags, and {{ livewire:scripts }} just before the closing </body> tag in your layout. Now you can use Livewire components within Antlers templates and it "just works!" Thanks, Jonas. 🙌

Webmention.io provides a straightforward API to get mention data for a given URL as JSON. My first attempt at displaying this data was using fetch with javascript. While there's nothing wrong with that, I found I needed to do a lot of templating and post-processing of the data to get it looking how I wanted, so I started over.

Using a Livewire component is a nice alternative. Having the full power of blade templating is much easier than wrangling elements around with JS. It also makes it a cinch to add things later on, such as loading states, or deferred loading on page load.

Getting the data is also simple from PHP, since Laravel has a nice built-in HTTP client nowadays.

Make a new livewire component with php artisan make:livewire webmentions.

The Component

The full component class looks like this (we'll break it down below):

1<?php
2 
3namespace App\Http\Livewire;
4 
5use Livewire\Component;
6use Illuminate\Support\Facades\Http;
7 
8class Webmentions extends Component
9{
10 public $url;
11 
12 public function mount($url)
13 {
14 $this->url = $url;
15 }
16 
17 public function getMentionsProperty()
18 {
19 return collect(Http::get('https://webmention.io/api/links.jf2?target=' . urlencode($this->url))['children'])
20 ->groupBy('wm-property')
21 ->mapWithKeys(function ($collection, $key) {
22 return [$this->readableAction($key) => $collection];
23 });
24 }
25 
26 public function getRepliesProperty()
27 {
28 return $this->mentions->only(['replies', 'mentions', 'reposts'])->flatten(1);
29 }
30 
31 public function getLikesProperty()
32 {
33 return $this->mentions->get('likes') ?? collect();
34 }
35 
36 protected function readableAction($action)
37 {
38 return [
39 'in-reply-to' => 'replies',
40 'like-of' => 'likes',
41 'repost-of' => 'reposts',
42 'mention-of' => 'mentions'
43 ][$action] ?? '';
44 }
45}
1<?php
2 
3namespace App\Http\Livewire;
4 
5use Livewire\Component;
6use Illuminate\Support\Facades\Http;
7 
8class Webmentions extends Component
9{
10 public $url;
11 
12 public function mount($url)
13 {
14 $this->url = $url;
15 }
16 
17 public function getMentionsProperty()
18 {
19 return collect(Http::get('https://webmention.io/api/links.jf2?target=' . urlencode($this->url))['children'])
20 ->groupBy('wm-property')
21 ->mapWithKeys(function ($collection, $key) {
22 return [$this->readableAction($key) => $collection];
23 });
24 }
25 
26 public function getRepliesProperty()
27 {
28 return $this->mentions->only(['replies', 'mentions', 'reposts'])->flatten(1);
29 }
30 
31 public function getLikesProperty()
32 {
33 return $this->mentions->get('likes') ?? collect();
34 }
35 
36 protected function readableAction($action)
37 {
38 return [
39 'in-reply-to' => 'replies',
40 'like-of' => 'likes',
41 'repost-of' => 'reposts',
42 'mention-of' => 'mentions'
43 ][$action] ?? '';
44 }
45}

The most important parts are the mount, getMentionsProperty , and readableAction methods. We need the current page's URL when initializing the Livewire component so we can get the mentions for that article.

All the work of hitting the Webmention.io API is done in the getMentionsProperty function. First, it calls the endpoint (with the url we passed into mount) using the HTTP client. It puts the results into a collection, then groups the mentions by type and remaps those to more "readable" type names. Using a computed property here ensures that we only make that expensive (external API) call once per request, regardless of how many times the $this->mentions variable gets used.

The readableAction function maps the webmention types (wm-property) to more readable words. For example, instead of in-reply-to, it will now be replies.

1public function mount($url)
2{
3 $this->url = $url;
4}
5 
6public function getMentionsProperty()
7{
8 return collect(Http::get('https://webmention.io/api/links.jf2?target=' . urlencode($this->url))['children'])
9 ->groupBy('wm-property')
10 ->mapWithKeys(function ($collection, $key) {
11 return [$this->readableAction($key) => $collection];
12 });
13}
14 
15protected function readableAction($action)
16{
17 return [
18 'in-reply-to' => 'replies',
19 'like-of' => 'likes',
20 'repost-of' => 'reposts',
21 'mention-of' => 'mentions'
22 ][$action] ?? '';
23}
1public function mount($url)
2{
3 $this->url = $url;
4}
5 
6public function getMentionsProperty()
7{
8 return collect(Http::get('https://webmention.io/api/links.jf2?target=' . urlencode($this->url))['children'])
9 ->groupBy('wm-property')
10 ->mapWithKeys(function ($collection, $key) {
11 return [$this->readableAction($key) => $collection];
12 });
13}
14 
15protected function readableAction($action)
16{
17 return [
18 'in-reply-to' => 'replies',
19 'like-of' => 'likes',
20 'repost-of' => 'reposts',
21 'mention-of' => 'mentions'
22 ][$action] ?? '';
23}

The other two methods filter the $this->mentions property to only get mentions of a specific type. This gives the template a couple convenience variables $this->likes and $this->replies to access each type of interaction for a given article.

The Template

Now that we've covered the meat of the component class, let's talk about the template. It's really just a couple of if and foreach statements. If there's no mentions, I display a sad emoji and a message about having no mentions on the article. Otherwise, I display a "face pile" of user avatars who liked or mentioned it. Something to note is that the webmention.io API returns data about the mention and its context. Although I chose not to display this information, you could use it to display what was actually said if you want. This would be similar to a comment feed you'd see on other websites. Here's the template:

1<div>
2 @if ($this->mentions->isEmpty())
3 <div class="mt-6 text-gray-400">
4 😢 <span class="italic text-sm">Awww, nobody has liked or mentioned this on Twitter yet.</span>
5 </div>
6 @endif
7 
8 @if ($this->likes->isNotEmpty())
9 <div class="mt-4">
10 <div class="uppercase text-sm font-medium tracking-wide text-gray-400 pr-6">
11 LIKES
12 </div>
13 <div class="flex mt-2">
14 @foreach ($this->likes as $like)
15 <a href="{{ $like['author']['url'] }}" class="block border-4 border-white hover:border-orange-300 rounded-full w-12 h-12 shadow-sm -ml-2 overflow-auto transition duration-300">
16 <img src="{{ $like['author']['photo'] }}" alt="{{ $like['author']['name'] }}" class="max-w-full">
17 </a>
18 @endforeach
19 </div>
20 </div>
21 @endif
22 
23 @if ($this->replies->isNotEmpty())
24 <div class="mt-4">
25 <div class="uppercase text-sm font-medium tracking-wide text-gray-400 pr-6">
26 MENTIONS
27 </div>
28 <div class="flex mt-2">
29 @foreach ($this->replies as $reply)
30 <a href="{{ $reply['url'] }}" title="{{ $reply['content']['text'] }}" class="block border-4 border-white hover:border-orange-300 rounded-full w-12 h-12 shadow-sm -ml-2 overflow-auto transition duration-300">
31 <img src="{{ $reply['author']['photo'] }}" alt="{{ $reply['author']['name'] }}" class="max-w-full">
32 </a>
33 @endforeach
34 </div>
35 </div>
36 @endif
37</div>
1<div>
2 @if ($this->mentions->isEmpty())
3 <div class="mt-6 text-gray-400">
4 😢 <span class="italic text-sm">Awww, nobody has liked or mentioned this on Twitter yet.</span>
5 </div>
6 @endif
7 
8 @if ($this->likes->isNotEmpty())
9 <div class="mt-4">
10 <div class="uppercase text-sm font-medium tracking-wide text-gray-400 pr-6">
11 LIKES
12 </div>
13 <div class="flex mt-2">
14 @foreach ($this->likes as $like)
15 <a href="{{ $like['author']['url'] }}" class="block border-4 border-white hover:border-orange-300 rounded-full w-12 h-12 shadow-sm -ml-2 overflow-auto transition duration-300">
16 <img src="{{ $like['author']['photo'] }}" alt="{{ $like['author']['name'] }}" class="max-w-full">
17 </a>
18 @endforeach
19 </div>
20 </div>
21 @endif
22 
23 @if ($this->replies->isNotEmpty())
24 <div class="mt-4">
25 <div class="uppercase text-sm font-medium tracking-wide text-gray-400 pr-6">
26 MENTIONS
27 </div>
28 <div class="flex mt-2">
29 @foreach ($this->replies as $reply)
30 <a href="{{ $reply['url'] }}" title="{{ $reply['content']['text'] }}" class="block border-4 border-white hover:border-orange-300 rounded-full w-12 h-12 shadow-sm -ml-2 overflow-auto transition duration-300">
31 <img src="{{ $reply['author']['photo'] }}" alt="{{ $reply['author']['name'] }}" class="max-w-full">
32 </a>
33 @endforeach
34 </div>
35 </div>
36 @endif
37</div>

Room for Improvement

This only covers the basics. Sending and receiving mentions, getting them from the API, and displaying them on the page. However, there's always room for improvement!

There are a couple of problems with our implementation:

  • Technically it holds up the page load — we need to defer loading of the Livewire component
  • Duplicate avatars can be displayed. They should be grouped by person.
  • There's no loading indicator, it'd be nice to see one if the API for getting mentions is slow

Perhaps I'll write part two of this post, which illustrates how we could do all of that. Until next week, thanks for reading!

Enjoy this article? Follow me on Twitter for more tips, articles and links.
LIKES
MENTIONS

Want Updates?

Sign up here if you want to stay in the loop about new articles or products I'm making.
I'll never spam you. Unsubscribe at any time.
Copyright ©2024 Austen Cameron