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!
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:
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" />
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 full component class looks like this (we'll break it down below):
1<?php23namespace App\Http\Livewire;45use Livewire\Component;6use Illuminate\Support\Facades\Http;78class Webmentions extends Component9{10 public $url;1112 public function mount($url)13 {14 $this->url = $url;15 }1617 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 }2526 public function getRepliesProperty()27 {28 return $this->mentions->only(['replies', 'mentions', 'reposts'])->flatten(1);29 }3031 public function getLikesProperty()32 {33 return $this->mentions->get('likes') ?? collect();34 }3536 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<?php23namespace App\Http\Livewire;45use Livewire\Component;6use Illuminate\Support\Facades\Http;78class Webmentions extends Component9{10 public $url;1112 public function mount($url)13 {14 $this->url = $url;15 }1617 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 }2526 public function getRepliesProperty()27 {28 return $this->mentions->only(['replies', 'mentions', 'reposts'])->flatten(1);29 }3031 public function getLikesProperty()32 {33 return $this->mentions->get('likes') ?? collect();34 }3536 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}56public 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}1415protected 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}56public 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}1415protected 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.
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 @endif78 @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 LIKES12 </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 @endforeach19 </div>20 </div>21 @endif2223 @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 MENTIONS27 </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 @endforeach34 </div>35 </div>36 @endif37</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 @endif78 @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 LIKES12 </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 @endforeach19 </div>20 </div>21 @endif2223 @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 MENTIONS27 </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 @endforeach34 </div>35 </div>36 @endif37</div>
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:
Perhaps I'll write part two of this post, which illustrates how we could do all of that. Until next week, thanks for reading!