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
```
+### 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
+
+```
+
## 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 @@
+ 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');
+});