diff --git a/README.md b/README.md index cc600fd..b78a2eb 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ By default, it uses `` and OpenGraph tags. It also ships with a Twitter e **Features**: - Setting SEO tags from PHP - Setting SEO tags from Blade -- Integration with [Flipp](https://useflipp.com), to automatically generate cover images +- Integration with [Flipp](https://useflipp.com) and [Previewify](https://previewify.app), to automatically generate cover images - Custom extension support - Expressive & simple API - Customizable views @@ -215,6 +215,43 @@ The `flipp()` method also returns a signed URL to the image, which lets you use <img alt="@seo('title')" src="@seo('flipp', 'blog')"> ``` +### Previewify integration + +First, you need to add your Previewify API keys: +1. Add your API key to the `PREVIEWIFY_KEY` environment variable. You can get the key [here](https://previewify.app/app/account). +2. Go to `config/services.php` and add: + ```php + 'previewify' => [ + 'key' => env('PREVIEWIFY_KEY'), + ], + ``` + +Then, register your templates, for example in `AppServiceProvider`: +```php +seo()->previewify('blog', 24); +seo()->previewify('page', 83); +``` + +After that, you can use the templates by calling `seo()->previewify()` like this: +```php +seo()->previewify('blog', ['title' => 'Foo', 'content' => 'bar'])` +``` + +The call will set the generated image as the OpenGraph and Twitter card images. The generated URLs are signed. + +If no data array is provided, the method will use the `title` and `description` from the current SEO config: + +```php +seo()->title($post->title); +seo()->description($post->excerpt); +seo()->previewify('blog'); +``` + +The `previewify()` method also returns a signed URL to the image, which lets you use it in other places, such as blog cover images. +```php +<img alt="@seo('title')" src="@seo('previewify', 'blog')"> +``` + ## Examples ### Service Provider diff --git a/phpstan.neon b/phpstan.neon index a04c137..2351e0b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -14,5 +14,6 @@ parameters: ignoreErrors: # Waiting for https://github.com/phpstan/phpstan/issues/5706 - - '#^Cannot call method (flipp|get|set)\(\) on ArchTech\\SEO\\SEOManager\|array\|string\|null\.$#' + - '#^Cannot call method (flipp|previewify|get|set)\(\) on ArchTech\\SEO\\SEOManager\|array\|string\|null\.$#' - '#^Method ArchTech\\SEO\\SEOManager::flipp\(\) should return static\(ArchTech\\SEO\\SEOManager\)\|string but returns array\|string\|null\.$#' + - '#^Method ArchTech\\SEO\\SEOManager::previewify\(\) should return static\(ArchTech\\SEO\\SEOManager\)\|string but returns array\|string\|null\.$#' diff --git a/src/SEOManager.php b/src/SEOManager.php index 68d6d68..1709552 100644 --- a/src/SEOManager.php +++ b/src/SEOManager.php @@ -180,6 +180,32 @@ public function flipp(string $alias, string|array $data = null): string|static return $this->set('image', "https://s.useflipp.com/{$template}.png?s={$signature}&v={$query}"); } + /** Configure or use Previewify. */ + public function previewify(string $alias, int|string|array $data = null): string|static + { + if (is_string($data) || is_int($data)) { + $this->meta("previewify.templates.$alias", (string) $data); + + return $this; + } + + if ($data === null) { + $data = [ + 'title' => $this->raw('title'), + 'description' => $this->raw('description'), + ]; + } + + $query = base64_encode(json_encode($data, JSON_THROW_ON_ERROR)); + + /** @var string $template */ + $template = $this->meta("previewify.templates.$alias"); + + $signature = hash_hmac('sha256', $query, config('services.previewify.key')); + + return $this->set('image', "https://previewify.app/generate/templates/{$template}/signed?signature={$signature}&fields={$query}"); + } + /** Enable favicon extension. */ public function favicon(): static { diff --git a/src/SEOServiceProvider.php b/src/SEOServiceProvider.php index 7e063ca..8a2ed89 100644 --- a/src/SEOServiceProvider.php +++ b/src/SEOServiceProvider.php @@ -5,6 +5,7 @@ namespace ArchTech\SEO; use ArchTech\SEO\Commands\GenerateFaviconsCommand; +use Illuminate\Support\Arr; use Illuminate\Support\ServiceProvider; use ImLiam\BladeHelper\BladeHelperServiceProvider; use ImLiam\BladeHelper\Facades\BladeHelper; @@ -32,11 +33,11 @@ public function boot(): void ], 'seo-views'); BladeHelper::directive('seo', function (...$args) { - // Flipp supports more arguments - if ($args[0] === 'flipp') { - array_shift($args); + // Flipp and Previewify support more arguments + if (in_array($args[0], ['flipp', 'previewify'], true)) { + $method = array_shift($args); - return seo()->flipp(...$args); + return seo()->{$method}(...$args); } // Two arguments indicate that we're setting a value, e.g. `@seo('title', 'foo') @@ -46,7 +47,13 @@ public function boot(): void // An array means we don't return anything, e.g. `@seo(['title' => 'foo']) if (is_array($args[0])) { - seo($args[0]); + foreach ($args[0] as $type => $value) { + if (in_array($type, ['flipp', 'previewify'], true)) { + seo()->{$type}(...Arr::wrap($value)); + } else { + seo()->set($type, $value); + } + } return null; } diff --git a/tests/Pest/FlippTest.php b/tests/Pest/FlippTest.php index b440540..5874ee4 100644 --- a/tests/Pest/FlippTest.php +++ b/tests/Pest/FlippTest.php @@ -68,3 +68,10 @@ ->toContain('s.useflipp.com/abcdefg') ->toContain(base64_encode(json_encode(['title' => 'foo', 'description' => 'bar']))); }); + +test('the @seo helper can be used for setting a flipp image', function () { + seo()->flipp('blog', 'abcdefg'); + blade("@seo(['flipp' => ['blog', ['title' => 'abc', 'excerpt' => 'def']]])"); + + expect(seo('image'))->toContain('s.useflipp.com/abcdefg'); +}); diff --git a/tests/Pest/PreviewifyTest.php b/tests/Pest/PreviewifyTest.php new file mode 100644 index 0000000..ebc4030 --- /dev/null +++ b/tests/Pest/PreviewifyTest.php @@ -0,0 +1,77 @@ +<?php + +beforeEach(fn () => config(['services.previewify.key' => 'abc'])); + +test('previewify templates can be set', function () { + seo()->previewify('blog', 1); + + expect(seo()->meta('previewify.templates')) + ->toHaveCount(1) + ->toHaveKey('blog', '1'); +}); + +test('previewify makes a request to the template not the alias', function () { + seo()->previewify('blog', 1); + expect(seo()->previewify('blog')) + ->toContain('previewify.app/generate/templates/1'); +}); + +test('previewify templates can be given data', function () { + seo()->previewify('blog', 1); + expect(seo()->previewify('blog', ['title' => 'abc', 'excerpt' => 'def'])) + ->toContain('previewify.app/generate/templates/1') + ->toContain(base64_encode(json_encode(['title' => 'abc', 'excerpt' => 'def']))); +}); + +test('the previewify method returns a link to a signed url', function () { + seo()->previewify('blog', 1); + + expect(seo()->previewify('blog', ['title' => 'abc'])) + ->toContain('?signature=' . hash_hmac('sha256', base64_encode(json_encode(['title' => 'abc'])), config('services.previewify.key'))); +}); + +test("previewify templates use default data when they're not passed any data explicitly", function () { + seo()->previewify('blog', 1); + + seo()->title('foo')->description('bar'); + + expect(seo()->previewify('blog')) + ->toContain('previewify.app/generate/templates/1') + ->toContain(base64_encode(json_encode(['title' => 'foo', 'description' => 'bar']))); +}); + +test('previewify images are used as the cover images', function () { + seo()->previewify('blog', 1); + + seo()->title('foo')->description('bar'); + + expect(seo()->previewify('blog')) + ->toBe(seo('image')); +}); + +test('the blade directive can be used with previewify', function () { + seo()->previewify('blog', 1); + + seo()->title('foo')->description('bar'); + + expect(blade("@seo('previewify', 'blog')"))->toBe(seo()->previewify('blog')); + expect(blade("@seo('previewify', 'blog', ['title' => 'abc'])"))->toBe(seo()->previewify('blog', ['title' => 'abc'])); +}); + +test('previewify uses the raw title and description', function () { + seo()->previewify('blog', 1); + + seo()->title(modify: fn (string $title) => $title . ' - modified'); + seo()->title('foo')->description('bar'); + + expect(seo()->previewify('blog')) + ->toContain('previewify.app/generate/templates/1') + ->toContain(base64_encode(json_encode(['title' => 'foo', 'description' => 'bar']))); +}); + +test('the @seo helper can be used for setting a previewify image', function () { + seo()->previewify('blog', 1); + blade("@seo(['previewify' => ['blog', ['title' => 'abc', 'excerpt' => 'def']]])"); + + expect(seo('image'))->toContain('previewify.app/generate/templates/1'); +});