Blade-Component: Webmentions

Photo by NordWood Themes on Unsplash

Blade-Component: Webmentions

While developing my new website including this blog I've decided to go with a static site and don't want to handle comments and so on. But I still wanted to include user contributed content below the articles.

So I searched for solutions and found some articles by my friends at Spatie how they've implemented webmention.io.

As I've created this site with Laravel 7 I wanted to create a simple to use Blade Component that will do everything for me. The result is a PHP file that loads the webmentions and maps them into three collections - likes, reposts and comments - and a blade file that renders them.

The approach is pretty simple as webmention does the hard job for us. At first I load all webmentions for the given or current URL. As the API is paginated I'm using a do-while loop to load all pages. After that I'm splitting the results in three collections.

php
app/View/Components/Webmentions.php
namespace App\View\Components;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Illuminate\View\Component;

class Webmentions extends Component
{
    public Collection $likes;
    public Collection $reposts;
    public Collection $comments;

    public function __construct(?string $url = null)
    {
        $url ??= request()->url();

        $webmentions = collect();
        $page = 0;
        do {
            $entries = Http::get('https://webmention.io/api/mentions.jf2', [
                'token' => config('services.webmention.token'),
                'domain' => parse_url($url, PHP_URL_HOST),
                'per-page' => 100,
                'page' => $page,
            ])->json()['children'] ?? [];
            $webmentions->push(...$entries);

            $page++;
        } while (count($entries) >= 100);

        $webmentions = $webmentions
            ->filter(fn (array $entry): bool => trim(parse_url($entry['wm-target'], PHP_URL_PATH), '/') === trim(parse_url($url, PHP_URL_PATH), '/'));

        $this->likes = $webmentions
            ->filter(fn (array $entry): bool => $entry['wm-property'] === 'like-of')
            ->mapInto(Like::class)
            ->sortByDesc('date');

        $this->reposts = $webmentions
            ->filter(function (array $entry): bool {
                if ($entry['wm-property'] === 'repost-of') {
                    return true;
                }

                if ($entry['wm-property'] === 'mention-of') {
                    return empty($entry['content']['text']);
                }

                return false;
            })
            ->mapInto(Repost::class)
            ->sortByDesc('date');

        $this->comments = $webmentions
            ->filter(fn (array $entry): bool => in_array($entry['wm-property'], ['mention-of', 'in-reply-to']))
            ->reject(fn (array $entry): bool => empty($entry['content']['text']))
            ->mapInto(Comment::class)
            ->sortBy('date');
    }

    public function render()
    {
        return view('components.webmentions');
    }
}

And the Blade view is super opinionated so I will reduce it to the important part. You automatically have access to all public properties of the component class. To use autocompletion in my blade files I'm always adding the variables to the top of the blade file as a doc-comment.

php
resources/views/components/webmentions.blade.php
<?php /** @var \Illuminate\View\ComponentAttributeBag $attributes */ ?>
<?php /** @var \Illuminate\Support\Collection|\App\Webmentions\Like[] $likes */ ?>
<?php /** @var \Illuminate\Support\Collection|\App\Webmentions\Repost[] $reposts */ ?>
<?php /** @var \Illuminate\Support\Collection|\App\Webmentions\Comment[] $comments */ ?>

With this you can loop over the items and render them however you want. And can use the new blade component wherever you need it.

html
<x-webmentions :url="$url" />