★ How to add webmentions to a Laravel powered blog
Tags: laravel, php, webmentions
Published: 2020-01-02 23:32:40

---
title: ★ How to add webmentions to a Laravel powered blog
published: true
date: 2019-07-15 07:49:19 UTC
tags: laravel, php, webmentions
canonical_url: https://freek.dev/1406-how-to-add-webmentions-to-a-laravel-powered-blog
---

The comment section of this blog used to be powered by [Disqus](https://disqus.com/). 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.

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](https://sebastiandedeyne.com/) wrote [a very interesting blog post](https://sebastiandedeyne.com/adding-webmentions-to-my-blog/) 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](https://github.com/spatie/freek.dev/blob/031f52f616111005da8f21d705a31c91f190e61e/app/Actions/PublishPostAction.php#L34) 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](https://brid.gy/). 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.

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

Here's how Bridgy UI looks like.

![The Bridgy UI](https://freek.dev/uploads/media/webmentions/bridgy.png)

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](https://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.

```html
<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](https://freek.dev/uploads/media/webmentions/webmention.png)

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](https://freek.dev/uploads/media/webmentions/webmention-webhook.png)

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](https://spatie.be) released a package called [laravel-webhooks-client](https://github.com/spatie/laravel-webhook-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](https://github.com/spatie/freek.dev/blob/e1ec197e0183d8799caafe17af833bacf78b90cb/routes/web.php#L34) in my routes file:

```php
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](https://github.com/spatie/laravel-webhook-client#handling-incoming-webhook-request-for-multiple-apps) 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.

```php
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`](https://github.com/spatie/freek.dev/blob/4f3b582fa5201ef7a512336f426bd924b1580c6b/app/Services/Webmentions/ProcessWebhookJob.php). Here is the code:

```php
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](https://github.com/spatie/freek.dev/blob/8d978aee6565ed71d1f7ba0ca85bccf530d5b06c/app/Models/Post.php#L55), we can loop through them [in a Blade view](https://github.com/spatie/freek.dev/blob/0b1e992b8b7ffde9c27bb1fe98fda95f95fdbb14/resources/views/front/posts/partials/webmentions.blade.php#L18-L38).

## Using webmentions as a commenting system vs Disqus #

I have a love-hate relationship with [Disqus](https://disqus.com/). 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:

- [Seb's blogpost on webmentions](https://sebastiandedeyne.com/adding-webmentions-to-my-blog/)
- [A deep dive on webmentions by Chris Aldrich](https://alistapart.com/article/webmentions-enabling-better-communication-on-the-internet/)
- [Brid.gy](https://brid.gy/)
- [Webmention.io](https://webmention.io/)
- [Our webhooks package](https://github.com/spatie/laravel-webhook-client)

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