Together with Marcel Pociot and our colleagues at Beyond Code and Spatie, I'm currently building Flare, a paid service which will be revealed at Laracon EU. Together with the service we'll release a package that will change the way you will work with Laravel. To stay in the loop subscribe to our mailinglist at https://flareapp.io

How to add webmentions to a Laravel powered blog

Original – by Freek Van der Herten – 8 minute read

The comment section of this blog used to be powered by Disqus. At its core, Disqus works pretty well. But I don't like the fact that it pulls in a lot of JavaScript to make it work. It's also not the prettiest UI.

I've recently replaced Disqus comments with webmentions. If you reply to, like or retweet this tweet, that interaction will, after a few minutes, appear in the comment section below.

Try it!

In this blog post, I'd like to explain why I moved to webmentions and how they are implemented on this blog.

What are webmentions?

Recently my colleague Seb wrote a very interesting blog post on webmentions. Reading that post got me interested in webmentions. In the post, Seb already explains what webmentions are.

Webmentions are a protocol for websites to communicate across each other. What makes the webmention standard interesting is that it’s not tied to a single service — it’s a protocol. Webmentions can be aggregated from a range of different services from Twitter, to other blogs or even direct comments.

Sending webmentions

Whenever I publish a blog post, a tweet with a link is automatically published. Ideally, I'd like to be notified by Twitter whenever that tweet (or any tweet that contains a link to my blog) is replied to, liked or retweeted.

Unfortunately, Twitter itself has no support for webmentions. But luckily some services can add this.

One of them is Bridgy. After it has been set up, Bridgy will scan Twitter for any tweets that contain a link to https://freek.dev. It'll also scan for new interactions (replies, retweets, likes) to those tweets.

Whenever it finds a link to my blog, it will look for a link tag on that page. If Bridgy finds this tag, it will send a webmention to the specified URL in href. More on that later.

<link rel="webmention" href="...">

Here's how Bridgy UI looks like.

The Bridgy UI

It might not be the prettiest screen, but here you can see when it'll scan Twitter for new mentions and crawl my blog for new or updated webmention targets. Using the UI, you can manually start these actions. You can also resend a webmention. These options came in very handy when developing webmention support for this blog.

Receiving webmentions

So Bridgy can send out webmentions, but we should also take care of receiving webmentions. Sure, you could code up a server yourself that can receive webmentions, but there are also specialized services for this. By using a service, you're sure that even when your server might be down for some reason, webmentions will still be recorded.

I'm using Webmention.io. It can filter out spammy webmentions performed by bots. To have Bridgy send all webmentions to webmention.io, I need to specify this href on each post page of my blog.

<link rel="webmention" href="https://webmention.io/freek.dev/webmention" />

On the webmention.io UI, you can see each webmention that Bridgy sent.

The Webmention.io UI

Webmention.io has support for sending out webhooks. We can configure it so that a soon as they get a webmention (and they deem it non-spammy), they send it to a specified URL. Webhook settings on Webmention.io

With that out of the way, let's take a look at how we handle the incoming webhooks from webmention.ui in our Laravel app.

Processing webmentions

A few weeks ago, my team at Spatie released a package called laravel-webhooks-client. This package makes it easy to handle any incoming webhook in a Laravel app.

After installing the package, the first thing that needs to be taken care of is making sure there's a URL that can accept the call made to our app by webmention.io. Using the package, you need to use the webhooks macro on Route. I've put this line in my routes file:

Route::webhooks('webhook-webmentions', 'webmentions');

The first parameter is the URL. The second parameter is the key used in the config file. Go over this section in the readme of the package on GitHub to know more about that.

In the screenshot above you've probably noticed that callback secret field. Webmention.io will add the value of that field in the payload it sends. I've also created a webmentions.webhook_secret entry in the config/services.php file and put that secret there.

When using the laravel-webhook-client package, you can specify a class that is responsible for determining if the webhook call is valid.

namespace App\Services\Webmentions;

use Illuminate\Http\Request;
use Spatie\WebhookClient\SignatureValidator\SignatureValidator;
use Spatie\WebhookClient\WebhookConfig;

class WebmentionWebhookSignatureValidator implements SignatureValidator
{
    public function isValid(Request $request, WebhookConfig $config): bool
    {
        if (! $request->has('secret')) {
            return false;
        }

        return $request->secret === config('services.webmentions.webhook_secret');
    }
}

Whenever webmention.io calls our app via the webhook, I'd like to transform the payload, associate it with the blog post it concerns, and add it to the database in the webmentions table.

All this is done in ProcessWebhookJob. Here is the code:

namespace App\Services\Webmentions

use App\Models\Post;
use App\Models\Webmention;
use Illuminate\Support\Arr;
use Spatie\Url\Url;
use Spatie\WebhookClient\ProcessWebhookJob as SpatieProcessWebhookJob;

class ProcessWebhookJob extends SpatieProcessWebhookJob
{
    public function handle()
    {
        $payload = $this->webhookCall->payload;

        if ($this->payloadHasBeenReceivedBefore($payload)) {
            return;
        }

        if (!$type = $this->getType($payload)) {
            return;
        }

        if (!$post = $this->getPost($payload)) {
            return;
        }

        Webmention::create([
            'post_id' => $post->id,
            'type' => $type,
            'webmention_id' => Arr::get($payload, 'post.wm-id'),
            'author_name' => Arr::get($payload, 'post.author.name'),
            'author_photo_url' => Arr::get($payload, 'post.author.photo'),
            'author_url' => Arr::get($payload, 'post.author.url'),
            'interaction_url' => Arr::get($payload, 'post.url'),
            'text' => Arr::get($payload, 'post.content.text'),
        ]);
    }
  
    private function payloadHasBeenReceivedBefore(array $payload): bool
    {
        $webmentionId = Arr::get($payload, 'post.wm-id');

        return Webmention::where('webmention_id', $webmentionId)->exists();
    }

    private function getType(array $payload): ?string
    {
        $types = [
            'in-reply-to' => Webmention::TYPE_REPLY,
            'like-of' => Webmention::TYPE_LIKE,
            'repost-of' => Webmention::TYPE_RETWEET,
        ];

        $wmProperty = Arr::get($payload, 'post.wm-property');

        if (!array_key_exists($wmProperty, $types)) {
            return null;
        }

        return $types[$wmProperty];
    }

    private function getPost(array $payload): ?Post
    {
        $url = Arr::get($payload, 'post.wm-target');

        if (!$url) {
            return null;
        }

        $postIdSlug = Url::fromString($url)->getSegment(1);

        [$id] = explode('-', $postIdSlug);

        return Post::find($id);
    }
}

I'm not going to go through this code step by step. Most of it should be self-explanatory.

Now that we have stored the webmentions in the database and they are associated with a post, we can loop through them in a Blade view.

Using webmentions as a commenting system vs Disqus

I have a love-hate relationship with Disqus. The core features work very well, there's support for nested comments, logging in possible to post comments is possible via a variety of systems.

On the downside, the UI Disqus adds to a site is ugly (imho) and can't be customized too much. Because of how it is rendered on the page, Google won't index the comments. Disqus can also inject ads into the comments.

Using Twitter webmentions as a comment system is also a mixed bag. It's nice to that you're in full control of how the comments are rendered. You'll also store the comments somewhere yourself, so you're not dependent on a service that keeps the content for you.

On the flip side, you must keep in mind that webmentions don't appear immediately. Commenters need an account of another service (in my case they need to have a Twitter account). Comments can't be long, because there's a low limit on the number of characters a tweet can have (you can of course still use multiple tweets. There's no support for nested tweets whatsoever.

Right now I think I like the trade-offs of webmentions more. I don't have too many comments on the posts on this blog, but most of my audience seems to be active on Twitter. Maybe my thoughts on this will change in the future. We'll see...

Closing thoughts

I hope you've enjoyed this tour of how webmentions are implemented on this blog. Here are some resources to check out if you want to know more:

What do you think of this? Let me know in the comments below 😀.

Stay up to date with all things Laravel, PHP, and JavaScript.

Follow me on Twitter. I regularly tweet out programming tips, and what I myself have learned in ongoing projects.

Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Comments

You can comment on this post by replying to this tweet.
Neil Mather replied on 24th July 2019
One of the nice things about webmentions is that I can like or reply to your post from my own site, too.  No Twitter required, and no character limit 🙂
Neil Mather liked on 24th July 2019
Simpledev liked on 18th July 2019
José Cage replied on 17th July 2019
Thanks for posting. Really interesting.
José Cage liked on 17th July 2019
Erhan Kayar liked on 17th July 2019
Luca Andrea Rossi replied on 17th July 2019
Awesome! But I think that likes and retweets add too much noise. I think that showing those in a smaller font, maybe at the end, would make comments easier to read.
Nemanja Ivankovic liked on 16th July 2019
Mark Myers liked on 16th July 2019
Ahmad Ripaldi liked on 16th July 2019
Linka Softwares liked on 16th July 2019
ArielSalvadorDev liked on 15th July 2019
Paulund liked on 15th July 2019
Só Marmota mesmo liked on 15th July 2019
Mazedul Islam Khan liked on 15th July 2019
Ilya Sakovich liked on 15th July 2019
Ihor Vorotnov • 25% liked on 15th July 2019
Oliver Kaufmann liked on 15th July 2019
Brian liked on 15th July 2019
Andrés Herrera García liked on 15th July 2019
Cohan Robinson liked on 15th July 2019
Spatie retweeted on 15th July 2019
Edwin I Arellano liked on 15th July 2019
Sam Snelling liked on 15th July 2019
Jordan Kniest liked on 15th July 2019
Volkan Metin liked on 15th July 2019
Mateus Junges retweeted on 15th July 2019
Mateus Junges liked on 15th July 2019
Sidrit Trandafili replied on 15th July 2019
That's awesome man. Thank you for sharing this.
Hamed liked on 15th July 2019
ダビッド トレス liked on 15th July 2019
ダビッド トレス retweeted on 15th July 2019
Tobias van Beek liked on 15th July 2019
Gökhan Çelebi liked on 15th July 2019
Michaël De Boey liked on 15th July 2019
Hu.go liked on 15th July 2019
Robert Clancy replied on 15th July 2019
test comment
MadeWithLaravel liked on 15th July 2019
MadeWithLaravel retweeted on 15th July 2019
Daksh H. Mehta liked on 15th July 2019
Rias Van der Veken liked on 15th July 2019
Armin Ulrich liked on 15th July 2019
Tim liked on 15th July 2019
Duncan McClean liked on 15th July 2019
Teun de Kleijne liked on 15th July 2019
Florian Voutzinos ⚡ liked on 15th July 2019
PHP Synopsis retweeted on 15th July 2019
Riff Point liked on 15th July 2019
Riff Point retweeted on 15th July 2019
frank lucas liked on 15th July 2019
Sander de Vos liked on 15th July 2019
Pepe García liked on 15th July 2019
Frederick Vanbrabant liked on 15th July 2019
Frederick Vanbrabant retweeted on 15th July 2019
Robin Dirksen liked on 15th July 2019
Mark Topper liked on 15th July 2019