├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Client.php ├── Collections └── WebmentionsCollection.php ├── Components └── WebmentionLinks.php ├── Facades └── Webmentions.php ├── Models ├── Author.php ├── Entry.php ├── Like.php ├── Mention.php ├── Model.php ├── Reply.php └── Repost.php └── WebmentionsServiceProvider.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-webmentions` will be documented in this file 4 | 5 | ## v0.6.0 - 2024-06-25 6 | 7 | - add Laravel 11 support 8 | - drop PHP7.4 & PHP8.0 9 | - drop Laravel 8 & Laravel 9 10 | 11 | ## v0.5.0 - 2023-08-10 12 | 13 | - add Laravel 10 support 14 | 15 | ## v0.4.0 - 2022-09-22 16 | 17 | - add Laravel 9 support 18 | 19 | ## v0.3.0 - 2021-03-01 20 | 21 | - add `` Blade component 22 | 23 | ## v0.2.0 - 2021-03-01 24 | 25 | - add `\Astrotomic\Webmentions\Client::count($url)` method 26 | 27 | ## v0.1.0 - 2021-02-27 28 | 29 | - initial release 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Astrotomic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Webmentions 2 | 3 | [![Latest Version](http://img.shields.io/packagist/v/astrotomic/laravel-webmentions.svg?label=Release&style=for-the-badge)](https://packagist.org/packages/astrotomic/laravel-webmentions) 4 | [![MIT License](https://img.shields.io/github/license/Astrotomic/laravel-webmentions.svg?label=License&color=blue&style=for-the-badge)](https://github.com/Astrotomic/laravel-webmentions/blob/master/LICENSE) 5 | [![Offset Earth](https://img.shields.io/badge/Treeware-%F0%9F%8C%B3-green?style=for-the-badge)](https://plant.treeware.earth/Astrotomic/laravel-webmentions) 6 | [![Larabelles](https://img.shields.io/badge/Larabelles-%F0%9F%A6%84-lightpink?style=for-the-badge)](https://www.larabelles.com/) 7 | 8 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/Astrotomic/laravel-webmentions/run-tests?style=flat-square&logoColor=white&logo=github&label=Tests)](https://github.com/Astrotomic/laravel-webmentions/actions?query=workflow%3Arun-tests) 9 | [![StyleCI](https://styleci.io/repos/322693045/shield)](https://styleci.io/repos/322693045) 10 | [![Total Downloads](https://img.shields.io/packagist/dt/astrotomic/laravel-webmentions.svg?label=Downloads&style=flat-square)](https://packagist.org/packages/astrotomic/laravel-webmentions) 11 | 12 | A simple client to retrieve [webmentions](https://webmention.io) for your pages. 13 | 14 | ## Installation 15 | 16 | You can install the package via composer: 17 | 18 | ```bash 19 | composer require astrotomic/laravel-webmentions 20 | ``` 21 | 22 | ## Configuration 23 | 24 | At firsts you will have to add your [webmention.io](https://webmention.io) API access token to the `services.php` config file. 25 | 26 | ```php 27 | return [ 28 | // ... 29 | 'webmention' => [ 30 | 'token' => env('WEBMENTION_TOKEN'), 31 | ], 32 | // ... 33 | ]; 34 | ``` 35 | 36 | ## Usage 37 | 38 | You can retrieve all webmentions for a given URL by calling the `get()` method on the packages client. 39 | 40 | ```php 41 | use Astrotomic\Webmentions\Facades\Webmentions; 42 | 43 | $records = Webmentions::get('https://gummibeer.dev/blog/2020/human-readable-intervals'); 44 | ``` 45 | 46 | If you omit the url as argument it will automatically use `\Illuminate\Http\Request::url()` as default. 47 | The return value will be an instance of `\Astrotomic\Webmentions\Collections\WebmentionsCollection` which provides you with predefined filter methods. 48 | You can also use the shorthand methods on the client to retrieve a collection of likes, mentions, replies or reposts. 49 | 50 | ```php 51 | use Astrotomic\Webmentions\Facades\Webmentions; 52 | 53 | $likes = Webmentions::likes('https://gummibeer.dev/blog/2020/human-readable-intervals'); 54 | $mentions = Webmentions::mentions('https://gummibeer.dev/blog/2020/human-readable-intervals'); 55 | $replies = Webmentions::replies('https://gummibeer.dev/blog/2020/human-readable-intervals'); 56 | $reposts = Webmentions::reposts('https://gummibeer.dev/blog/2020/human-readable-intervals'); 57 | ``` 58 | 59 | All items will be a corresponding instance of `\Astrotomic\Webmentions\Models\Like`, `\Astrotomic\Webmentions\Models\Mention`, `\Astrotomic\Webmentions\Models\Reply` or `\Astrotomic\Webmentions\Models\Repost`. 60 | 61 | If you only need the count of items you can use the `Webmentions::count()` method. 62 | 63 | ```php 64 | use Astrotomic\Webmentions\Facades\Webmentions; 65 | 66 | $counts = Webmentions::count('https://gummibeer.dev/blog/2020/human-readable-intervals'); 67 | [ 68 | 'count' => 52, 69 | 'type' => [ 70 | 'like' => 23, 71 | 'mention' => 8, 72 | 'reply' => 16, 73 | 'repost' => 5, 74 | ], 75 | ]; 76 | ``` 77 | 78 | ### Caching 79 | 80 | The client uses a poor man cache by default - so per runtime every domain is only requested once. 81 | If you want extended caching behavior you should wrap the calls in a `Cache::remember()` for example. 82 | 83 | ### Blade Component 84 | 85 | To receive webmentions for your page you have to add two `` tags to your head. 86 | This package provides a `` Blade component that makes it easier for you. 87 | 88 | ```html 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | ``` 98 | 99 | ## Testing 100 | 101 | ```bash 102 | composer test 103 | ``` 104 | 105 | ## Changelog 106 | 107 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 108 | 109 | ## Contributing 110 | 111 | Please see [CONTRIBUTING](https://github.com/Astrotomic/.github/blob/master/CONTRIBUTING.md) for details. You could also be interested in [CODE OF CONDUCT](https://github.com/Astrotomic/.github/blob/master/CODE_OF_CONDUCT.md). 112 | 113 | ### Security 114 | 115 | If you discover any security related issues, please check [SECURITY](https://github.com/Astrotomic/.github/blob/master/SECURITY.md) for steps to report it. 116 | 117 | ## Credits 118 | 119 | - [Tom Witkowski](https://github.com/Gummibeer) 120 | - [All Contributors](../../contributors) 121 | 122 | ## License 123 | 124 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 125 | 126 | ## Treeware 127 | 128 | You're free to use this package, but if it makes it to your production environment I would highly appreciate you buying the world a tree. 129 | 130 | It’s now common knowledge that one of the best tools to tackle the climate crisis and keep our temperatures from rising above 1.5C is to [plant trees](https://www.bbc.co.uk/news/science-environment-48870920). If you contribute to my forest you’ll be creating employment for local families and restoring wildlife habitats. 131 | 132 | You can buy trees at [offset.earth/treeware](https://plant.treeware.earth/Astrotomic/laravel-webmentions) 133 | 134 | Read more about Treeware at [treeware.earth](https://treeware.earth) 135 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astrotomic/laravel-webmentions", 3 | "description": "", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "astrotomic", 8 | "laravel-webmentions", 9 | "laravel", 10 | "webmentions", 11 | "webmention" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Tom Witkowski", 16 | "email": "gummibeer@astrotomic.info", 17 | "homepage": "https://gummibeer.dev", 18 | "role": "Developer" 19 | } 20 | ], 21 | "homepage": "https://github.com/astrotomic/laravel-webmentions", 22 | "require": { 23 | "php": "^8.1", 24 | "ext-json": "*", 25 | "guzzlehttp/guzzle": "^7.0.1", 26 | "illuminate/collections": "^10.0 || ^11.0", 27 | "illuminate/http": "^10.0 || ^11.0", 28 | "illuminate/support": "^10.0 || ^11.0" 29 | }, 30 | "require-dev": { 31 | "gajus/dindent": "^2.0", 32 | "orchestra/testbench": "^8.0 || ^9.0", 33 | "phpunit/phpunit": "^9.3 || ^10.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Astrotomic\\Webmentions\\": "src" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Astrotomic\\Webmentions\\Tests\\": "tests" 43 | } 44 | }, 45 | "config": { 46 | "sort-packages": true 47 | }, 48 | "extra": { 49 | "laravel": { 50 | "providers": [ 51 | "Astrotomic\\Webmentions\\WebmentionsServiceProvider" 52 | ] 53 | } 54 | }, 55 | "scripts": { 56 | "test": "vendor/bin/phpunit" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | byDomain($domain) 26 | ->filter(fn (Entry $entry): bool => $this->extractPath($entry->target) === $this->extractPath($url)) 27 | ->values(); 28 | } 29 | 30 | public function likes(?string $url = null): Collection 31 | { 32 | return $this->get($url)->likes(); 33 | } 34 | 35 | public function mentions(?string $url = null): Collection 36 | { 37 | return $this->get($url)->mentions(); 38 | } 39 | 40 | public function replies(?string $url = null): Collection 41 | { 42 | return $this->get($url)->replies(); 43 | } 44 | 45 | public function reposts(?string $url = null): Collection 46 | { 47 | return $this->get($url)->reposts(); 48 | } 49 | 50 | /** 51 | * @param string|null $url 52 | * @return array 53 | * 54 | * @see https://webmention.io/api/count?target={$url} 55 | */ 56 | public function count(?string $url = null): array 57 | { 58 | $items = $this->get($url); 59 | 60 | return [ 61 | 'count' => $items->count(), 62 | 'type' => [ 63 | 'like' => $items->likes()->count(), 64 | 'mention' => $items->mentions()->count(), 65 | 'reply' => $items->replies()->count(), 66 | 'repost' => $items->reposts()->count(), 67 | ], 68 | ]; 69 | } 70 | 71 | protected function byDomain(string $domain): WebmentionsCollection 72 | { 73 | if (! isset(static::$webmentions[$domain])) { 74 | $webmentions = new WebmentionsCollection(); 75 | 76 | $page = 0; 77 | do { 78 | $entries = Http::get(self::BASE_URL, [ 79 | 'token' => config('services.webmention.token'), 80 | 'domain' => $domain, 81 | 'per-page' => self::PER_PAGE, 82 | 'page' => $page, 83 | ])->json()['children'] ?? []; 84 | 85 | $webmentions->push(...$entries); 86 | 87 | $page++; 88 | } while (count($entries) >= self::PER_PAGE); 89 | 90 | static::$webmentions[$domain] = $webmentions 91 | ->map(fn (array $entry): ?Entry => Entry::make($entry)) 92 | ->filter() 93 | ->values(); 94 | } 95 | 96 | return static::$webmentions[$domain]; 97 | } 98 | 99 | protected function extractPath(string $url): ?string 100 | { 101 | return trim(parse_url($url, PHP_URL_PATH), '/'); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Collections/WebmentionsCollection.php: -------------------------------------------------------------------------------- 1 | filter(fn (Entry $entry): bool => $entry instanceof Like)->toBase(); 20 | } 21 | 22 | /** 23 | * @return \Illuminate\Support\Collection|\Astrotomic\Webmentions\Models\Mention[] 24 | */ 25 | public function mentions(): Collection 26 | { 27 | return $this->filter(fn (Entry $entry): bool => $entry instanceof Mention)->toBase(); 28 | } 29 | 30 | /** 31 | * @return \Illuminate\Support\Collection|\Astrotomic\Webmentions\Models\Reply[] 32 | */ 33 | public function replies(): Collection 34 | { 35 | return $this->filter(fn (Entry $entry): bool => $entry instanceof Reply)->toBase(); 36 | } 37 | 38 | /** 39 | * @return \Illuminate\Support\Collection|\Astrotomic\Webmentions\Models\Mention[] 40 | */ 41 | public function reposts(): Collection 42 | { 43 | return $this->filter(fn (Entry $entry): bool => $entry instanceof Repost)->toBase(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Components/WebmentionLinks.php: -------------------------------------------------------------------------------- 1 | domain = $domain ?? parse_url(Request::url(), PHP_URL_HOST); 15 | } 16 | 17 | public function render(): string 18 | { 19 | return << 21 | 22 | HTML; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Facades/Webmentions.php: -------------------------------------------------------------------------------- 1 | $author['name'], 15 | 'avatar' => $author['photo'] ?: null, 16 | 'url' => $author['url'] ?: null, 17 | ]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Models/Entry.php: -------------------------------------------------------------------------------- 1 | $entry['wm-id'], 41 | 'url' => $entry['url'], 42 | 'source' => $entry['wm-source'], 43 | 'target' => $entry['wm-target'], 44 | 'published_at' => $entry['published'] 45 | ? Carbon::parse($entry['published']) 46 | : null, 47 | 'created_at' => $entry['published'] 48 | ? Carbon::parse($entry['published']) 49 | : Carbon::parse($entry['wm-received']), 50 | 'author' => Author::fromWebmention($entry['author']), 51 | 'text' => $entry['content']['text'] ?? null, 52 | 'html' => isset($entry['content']['html']) 53 | ? new HtmlString($entry['content']['html']) 54 | : null, 55 | 'raw' => $entry, 56 | ]); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Models/Like.php: -------------------------------------------------------------------------------- 1 | $value) { 10 | $this->{$field} = $value; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Models/Reply.php: -------------------------------------------------------------------------------- 1 | app->singleton(Client::class); 14 | } 15 | 16 | public function boot(): void 17 | { 18 | $this->callAfterResolving(BladeCompiler::class, function (BladeCompiler $blade) { 19 | $blade->component(WebmentionLinks::class, 'webmention-links'); 20 | }); 21 | } 22 | } 23 | --------------------------------------------------------------------------------