Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Previewify image provider #20

Merged
merged 2 commits into from
Jun 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ By default, it uses `<title>` 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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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\.$#'
26 changes: 26 additions & 0 deletions src/SEOManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
17 changes: 12 additions & 5 deletions src/SEOServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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')
Expand All @@ -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;
}
Expand Down
7 changes: 7 additions & 0 deletions tests/Pest/FlippTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
77 changes: 77 additions & 0 deletions tests/Pest/PreviewifyTest.php
Original file line number Diff line number Diff line change
@@ -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');
});