1
0
Fork 0
mirror of https://github.com/archtechx/laravel-seo.git synced 2025-12-12 01:44:03 +00:00
This commit is contained in:
Samuel Štancl 2021-05-24 10:53:00 +02:00
parent 051a32575c
commit b12c9ecb55
21 changed files with 750 additions and 86 deletions

View file

@ -18,8 +18,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Start docker containers
run: docker-compose up -d
- name: Install composer dependencies - name: Install composer dependencies
run: composer install run: composer install
- name: Run tests - name: Run tests

190
README.md
View file

@ -1,41 +1,195 @@
# REPLACE # laravel-seo
Simple and flexible package template. This is a simple package for improving SEO via OpenGraph and Twitter meta tags.
# Usage It's semi-opinionated, as it's what we use for all of our websites. At the same time, it's not overengineered like many other SEO packages.
- Replace all occurances of `REPLACE` (case sensitive) with the name of the package namespace. E.g. the `Foo` in `ArchTech\Foo`. **Features**:
- Replace all occurances of `replace2` with the name of the package on composer, e.g. the `bar` in `archtechx/bar`. - Setting SEO tags from PHP
- If MySQL is not needed, remove `docker-compose.yml`, remove the line that runs docker from `./check`, and set `DB_CONNECTION` in `phpunit.xml` to `sqlite`, and `DB_DATABASE` to `:memory:`. - Setting SEO tags from Blade
- Integration with [Flipp](https://useflipp.com), to automatically generate cover images
- Custom extension support
- Expressive & simple API
--- Example usage:
```php
seo()
->title($post->title)
->description($post->excerpt)
->flipp('blog')
// Adds OpenGraph tags
// Adds Twitter card tags
// Generates social image using Flipp and sets it as the cover photo
```
## Installation ## Installation
```sh ```sh
composer require stancl/replace2 composer require archtechx/laravel-seo
```
And add the following line to your layout file's `<head>` tag:
```html
<x-seo::meta />
``` ```
## Usage ## Usage
The package can be used from any PHP code, or specifically from Blade using the `@seo` directive.
### PHP
Use the `seo()` helper to retrieve the SeoManager instance, on which you can call the following methods:
Available methods:
```js
site(string $site)
title(string $title)
description(string $description)
image(string $url)
twitterUser(string $username)
twitterTitle(string $title)
twitterDescription(string $description)
twitterImage(string $url)
```
Example usage:
```php ```php
// ... seo()->title('foo')->description('bar')
```
### Blade views
You can use the `@seo` directive to call the methods from Blade:
```html
@seo('title') // Echoes the title
@seo('title', 'foo') // Sets the title & echoes it
@seo(['title' => 'foo']) // Sets the title without echoing it
```
In general, you'll want to use `@seo(['title' => 'foo'])` at the start of a view — to set the values — and `@seo('title')` inside the view if you wish to fetch the value.
That is, if you'll use the helpers in Blade at all. Some apps will only use the PHP helper.
For Twitter, use the `twitter.author` format, e.g. `@seo('twitter.author')`.
### Defaults
To configure default values, call the methods with the `default` argument:
```php
seo()
->title(default: 'ArchTech — Meticulously architected web applications')
->description(default: 'We are a web development agency that ...');
```
### Modifiers
You may want to modify certain values before they get inserted into the template. For example, you may want to suffix the meta `<title>` with `| ArchTech` when it has a non-default value.
To do that, simply add the `modify` argument to the method calls like this:
```php
seo()->title(modify: fn (string $title) => $title . ' | ArchTech');
```
You can, of course, combine these with the defaults:
```php
seo()->title(
default: 'ArchTech — Meticulously architected web applications',
modify: fn (string $title) => $title . ' | ArchTech'
);
```
Which will make the package use the default if no title is provided, and if a title is provided using e.g. `seo()->title('Blog')`, it will be modified **right before being inserted into the template**.
### Flipp integration
First, you need to add your Flipp API keys:
1. Add your API key to the `FLIPP_KEY` environment variable. You can get the key [here](https://useflipp.com/settings/profile/api).
2. Go to `config/services.php` and add:
```php
'flipp' => [
'key' => env('FLIPP_KEY'),
],
```
Then, register your templates:
```php
seo()->flipp('blog', 'v8ywdwho3bso');
seo()->flipp('page', 'egssigeabtm7');
```
After that, you can use the templates by calling `seo()->flipp()` like this:
```php
seo()->flipp('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()->flipp('blog');
```
The `flipp()` 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('flipp', 'blog')">
```
## Customization
### Views
You can publish views running `php artisan vendor:publish --tag=seo-views`.
### Extensions
To use a custom extension, create a Blade *component* with the desired meta tags. The component should read data using `{{ seo()->get('foo') }}` or `@seo('foo')`.
For example:
```php
<meta name="facebook-title" content="@seo('facebook.foo')">
```
Once your view is created, register the extension:
```php
seo()->extension('facebook', view: 'my-component')
// The extension will use <x-my-component>
```
To set data for an extension (in our case `facebook`), simply prefix calls with the extension name in camelCase, or use the `->set()` method:
```php
seo()->facebookFoo('bar')
seo()->facebookTitle('About us')
seo()->set('facebook.description', 'We are a web development agency that ...')
seo(['facebook.description' => 'We are a web development agency that ...'])
```
To disable an extension, set the second argument in the `extension()` call to false:
```php
seo()->extension('facebook', false);
``` ```
## Development ## Development
Running all checks locally: Run all checks locally:
```sh ```sh
./check ./check
``` ```
Running tests:
```sh
MYSQL_PORT=3307 docker-compose up -d
phpunit
```
Code style will be automatically fixed by php-cs-fixer. Code style will be automatically fixed by php-cs-fixer.

View file

@ -0,0 +1,5 @@
<meta name="twitter:card" content="summary_large_image">
@if(seo('twitter.user')) <meta name="twitter:site" content="@seo('twitter.user')"> @endif
@if(seo('twitter.title')) <meta name="twitter:title" content="@seo('twitter.title')"> @endif
@if(seo('twitter.description')) <meta name="twitter:description" content="@seo('twitter.description')"> @endif
@if(seo('twitter.image')) <meta name="twitter:image" content="@seo('twitter.image')" /> @endif

View file

@ -0,0 +1,10 @@
<title>@seo('title')</title>
<meta property="og:type" content="website" />
@if(seo('site')) <meta property="og:site_name" content="@seo('site')"> @endif
@if(seo('title')) <meta property="og:title" content="@seo('title')" /> @endif
@if(seo('description')) <meta property="og:description" content="@seo('description')" /> @endif
@if(seo('image')) <meta property="og:image" content="@seo('image')" /> @endif
@foreach(seo()->extensions() as $extension)
<x-dynamic-component :component="$extension" />
@endforeach

2
check
View file

@ -43,8 +43,6 @@ else
offer_run './vendor/bin/phpstan analyse' offer_run './vendor/bin/phpstan analyse'
fi fi
(MYSQL_PORT=3307 docker-compose up -d > /dev/null 2>/dev/null) || true
if (./vendor/bin/pest > /dev/null 2>/dev/null); then if (./vendor/bin/pest > /dev/null 2>/dev/null); then
echo '✅ PEST OK' echo '✅ PEST OK'
else else

View file

@ -1,5 +1,5 @@
{ {
"name": "archtechx/replace", "name": "archtechx/laravel-seo",
"description": "", "description": "",
"type": "library", "type": "library",
"license": "MIT", "license": "MIT",
@ -10,17 +10,22 @@
} }
], ],
"autoload": { "autoload": {
"files": [
"src/helpers.php"
],
"psr-4": { "psr-4": {
"ArchTech\\REPLACE\\": "src/" "ArchTech\\SEO\\": "src/"
} }
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"ArchTech\\REPLACE\\Tests\\": "tests/" "ArchTech\\SEO\\Tests\\": "tests/"
} }
}, },
"require": { "require": {
"illuminate/support": "^8.24" "php": "^8.0",
"illuminate/support": "^8.24",
"imliam/laravel-blade-helper": "^1.0"
}, },
"require-dev": { "require-dev": {
"orchestra/testbench": "^6.9", "orchestra/testbench": "^6.9",
@ -32,7 +37,7 @@
"extra": { "extra": {
"laravel": { "laravel": {
"providers": [ "providers": [
"ArchTech\\REPLACE\\PackageServiceProvider" "ArchTech\\SEO\\PackageServiceProvider"
] ]
} }
} }

View file

@ -1,12 +0,0 @@
version: '3'
services:
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: main
MYSQL_USER: user
MYSQL_PASSWORD: password
MYSQL_TCP_PORT: ${MYSQL_PORT}
ports:
- "${MYSQL_PORT}:${MYSQL_PORT}"

View file

@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace ArchTech\REPLACE;
use Illuminate\Support\ServiceProvider;
class REPLACEServiceProvider extends ServiceProvider
{
public function register(): void
{
}
public function boot(): void
{
// $this->loadViewsFrom(__DIR__ . '/../assets/views', 'package');
// $this->publishes([
// __DIR__ . '/../assets/views' => resource_path('views/vendor/package'),
// ], 'package-views');
// $this->mergeConfigFrom(
// __DIR__ . '/../assets/package.php',
// 'package'
// );
// $this->publishes([
// __DIR__ . '/../assets/package.php' => config_path('package.php'),
// ], 'package-config');
}
}

222
src/SEOManager.php Normal file
View file

@ -0,0 +1,222 @@
<?php
namespace ArchTech\SEO;
use Illuminate\Support\Str;
/**
* @method $this title(string $title) Set the title.
* @method $this description(string $description) Set the description.
* @method $this site(string $site) Set the site name.
* @method $this image(string $url) Set the cover image.
* @method $this twitter(enabled $bool = true) Enable the Twitter extension.
* @method $this twitterUser(string $username) Set the Twitter author.
* @method $this twitterTitle(string $title) Set the Twitter title.
* @method $this twitterDescription(string $description) Set the Twitter description.
* @method $this twitterImage(string $url) Set the Twitter cover image.
*/
class SEOManager
{
/** Value modifiers. */
protected array $modifiers = [];
/** Default values. */
protected array $defaults = [];
/** User-configured values. */
protected array $values = [];
/** List of extensions. */
protected array $extensions = [
'twitter' => false,
];
/** Metadata for additional features. */
protected array $meta = [];
/** Get all used values. */
public function all(): array
{
return collect($this->getKeys())
->mapWithKeys(fn (string $key) => [$key => $this->get($key)])
->toArray();
}
/** Get a list of used keys. */
protected function getKeys(): array
{
return collect(['site', 'title', 'image', 'description', 'twitter.user', 'twitter.title', 'twitter.image', 'twitter.description'])
->merge(array_keys($this->defaults))
->merge(array_keys($this->values))
->unique()
->filter(function (string $key) {
if (count($parts = explode('.', $key)) > 1) {
if (isset($this->extensions[$parts[0]])) {
// Is the extension allowed?
return $this->extensions[$parts[0]];
}
return false;
}
return true;
})
->toArray();
}
/** Get a modified value. */
protected function modify(string $key): string|null
{
return isset($this->modifiers[$key])
? $this->modifiers[$key]($this->values[$key])
: $this->values[$key];
}
/** Set one or more values. */
public function set(string|array $key, string|null $value = null): string|array
{
if (is_array($key)) {
foreach ($key as $k => $v) {
$this->set($k, $v);
}
return collect($key)
->keys()
->mapWithKeys(fn (string $key) => [$key => $this->get($key)])
->toArray();
} else {
$this->values[$key] = $value;
if (Str::contains($key, '.')) {
$this->extension(Str::before($key, '.'), enabled: true);
}
return $this->get($key);
}
}
/** Resolve a value. */
public function get(string $key): string|null
{
return isset($this->values[$key])
? $this->modify($key)
: $this->defaults[$key] ?? (
Str::contains($key, '.') ? $this->get(Str::after($key, '.')) : null
);
}
/** Configure an extension. */
public function extension(string $name, bool $enabled = true, string $view = null): static
{
$this->extensions[$name] = $enabled;
if ($view) {
$this->meta("extensions.$name.view", $view);
}
return $this;
}
/** Get a list of enabled extensions. */
public function extensions(): array
{
return collect($this->extensions)
->filter(fn (bool $enabled) => $enabled)
->keys()
->mapWithKeys(fn (string $extension) => [
$extension => $this->meta("extensions.$extension.view") ?? ("seo::extensions." . $extension)
])
->toArray();
}
/** Configure or use Flipp. */
public function flipp(string $template, string|array $data = null): string|static
{
if (is_string($data)) {
$this->meta("flipp.templates.$template", $data);
return $this;
}
if ($data === null) {
$data = [
'title' => $this->title,
'description' => $this->description,
];
}
$query = base64_encode(json_encode($data));
$signature = hash_hmac('sha256', $template . $query, config('services.flipp.key'));
return $this->set('image', "https://s.useflipp.com/{$template}.png?s={$signature}&v={$query}");
}
/**
* Get or set metadata.
* @param string|array $key The key or key-value pair being set.
* @param string|array|mixed $value The value (if a single key is provided).
* @return mixed
*/
public function meta(string|array $key, mixed $value = null): mixed
{
if (is_array($key)) {
foreach ($key as $k => $v) {
$this->meta($k, $v);
}
return $this;
}
if ($value === null) {
return data_get($this->meta, $key);
}
data_set($this->meta, $key, $value);
return $this;
}
/** Handle magic method calls. */
public function __call($name, $arguments)
{
if (isset($this->extensions[$name])) {
return $this->extension($name, $arguments[0] ?? true);
}
$key = Str::snake($name, '.');
if (isset($arguments['default'])) {
$this->defaults[$key] = $arguments['default'];
}
if (isset($arguments['modifier'])) {
$this->modifiers[$key] = $arguments['modifier'];
}
// modify: ... is an alias for modifier: ...
if (isset($arguments['modify'])) {
$this->modifiers[$key] = $arguments['modify'];
}
if (isset($arguments[0])) {
$this->set($key, $arguments[0]);
return $this;
}
return $this->get($key);
}
/** Handle magic get. */
public function __get(string $key)
{
return $this->get(Str::snake($key, '.'));
}
/** Handle magic set. */
public function __set(string $key, mixed $value)
{
return $this->set(Str::snake($key, '.'), $value);
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace ArchTech\SEO;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use ImLiam\BladeHelper\Facades\BladeHelper;
class SEOServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton('seo', SEOManager::class);
}
public function boot(): void
{
$this->loadViewsFrom(__DIR__ . '/../assets/views', 'seo');
$this->publishes([
__DIR__ . '/../assets/views' => resource_path('views/vendor/seo'),
], 'seo-views');
BladeHelper::directive('seo', function (...$args) {
if (count($args) === 2) {
return seo()->set($args[0], $args[1]);
}
if (is_array($args[0])) {
seo($args[0]);
return null;
}
return seo()->get($args[0]);
});
}
}

16
src/helpers.php Normal file
View file

@ -0,0 +1,16 @@
<?php
use ArchTech\SEO\SEOManager;
if (! function_exists('seo')) {
function seo(string|array $key = null): SEOManager|string|array|null
{
if (! $key) {
return app('seo');
} else if (is_array($key)) {
return app('seo')->set($key);
} else {
return app('seo')->get($key);
}
}
}

View file

@ -11,7 +11,7 @@
| |
*/ */
uses(ArchTech\REPLACE\Tests\TestCase::class)->in('Pest'); uses(ArchTech\SEO\Tests\TestCase::class)->in('Pest');
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -39,7 +39,12 @@ expect()->extend('toBeOne', function () {
| |
*/ */
function something() function blade(string $blade): string
{ {
// .. return eval('ob_start(); ?>' . app('blade.compiler')->compileString($blade) . ' <?php return trim(ob_get_clean());');
}
function meta(): string
{
return view('seo::components.meta')->render();
} }

38
tests/Pest/BladeTest.php Normal file
View file

@ -0,0 +1,38 @@
<?php
test('the @seo helper can be used for fetching values', function () {
seo(['image' => 'foo']);
expect(blade('<img src="@seo(\'image\')">'))
->toBe('<img src="foo">');
});
test('the @seo helper can be used for setting & fetching values', function () {
expect(blade('<img src="@seo(\'image\', \'bar\')">'))
->toBe('<img src="bar">');
});
test('the @seo helper can be used for setting values with no output', function () {
expect(blade('<img src="@seo([\'image\' => \'foo\'])">'))
->toBe('<img src="">');
expect(seo('image'))->toBe('foo');
});
test("opengraph tags are rendered only if they're set", function () {
seo()->title('foo');
expect(meta())
->toContain('og:title')
->not()->toContain('og:description');
});
test('twitter tags are rendered only if the extension is enabled', function () {
seo()->title('foo');
expect(meta())->not()->toContain('twitter');
seo()->twitter()->twitterTitle('bar');
expect(meta())->toContain('twitter');
});

View file

@ -1,9 +0,0 @@
<?php
it('succeeds', function () {
expect(true)->toBeTrue();
});
it('fails', function () {
expect(false)->toBeTrue();
});

View file

@ -0,0 +1,72 @@
<?php
use ArchTech\SEO\Tests\Etc\FacebookExtension;
use Illuminate\Support\Facades\Blade;
use Illuminate\View\Component;
test('the twitter extension is disabled by default', function () {
expect(seo()->all())
->not()->toBeEmpty()
->not()->toHaveKey('twitter.title');
});
test('the twitter extension can be enabled by calling twitter', function () {
expect(seo()->twitter()->all())
->not()->toBeEmpty()
->toHaveKey('twitter.title');
});
test('the twitter extension can be disabled by calling twitter with false', function () {
expect(seo()->twitter()->twitter(false)->all())
->not()->toBeEmpty()
->not()->toHaveKey('twitter.title');
});
test('when an extension is enabled, all of its keys are included in the resolved values', function () {
expect(seo()->twitter()->all())
->not()->toBeEmpty()
->toHaveKeys(['twitter.title', 'twitter.description', 'twitter.user', 'twitter.image']);
});
test('extension keys can be set by prefixing the call with the extension name and using camelcase', function () {
seo()->extension('foo');
seo()->fooTitle('bar');
expect(seo()->all())
->toHaveKey('foo.title', 'bar');
});
test('extensions can use custom blade paths', function () {
view()->addNamespace('test', __DIR__ . '/../views');
seo()->extension('facebook', view: 'test::facebook');
seo()->facebookTitle('abc');
expect(meta())->toContain('<meta name="facebook:title" content="ABC" />');
});
test('twitter falls back to the default values', function () {
seo()->twitter();
seo()->title('foo');
seo()->twitterDescription('bar');
seo()->description('baz');
expect(seo('twitter.title'))->toBe('foo');
expect(seo('twitter.description'))->toBe('bar');
expect(seo('description'))->toBe('baz');
expect(meta())->toContain('<meta name="twitter:title" content="foo">');
});
test('extensions are automatically enabled when values for them are set', function () {
expect(seo()->extensions())->not()->toHaveKey('twitter');
seo()->twitterTitle('foo');
expect(seo()->extensions())->toHaveKey('twitter');
});

42
tests/Pest/FlippTest.php Normal file
View file

@ -0,0 +1,42 @@
<?php
test('flipp templates can be set', function () {
seo()->flipp('blog', 'abcdefg');
expect(seo()->meta('flipp.templates'))
->toHaveCount(1)
->toHaveKey('blog', 'abcdefg');
});
test('flipp templates can be given data', function () {
seo()->flipp('blog', 'abcdefg');
expect(seo()->flipp('blog', ['title' => 'abc', 'excerpt' => 'def']))
->toContain('s.useflipp.com/blog')
->toContain(base64_encode(json_encode(['title' => 'abc', 'excerpt' => 'def'])));
});
test('the flipp method returns a link to a signed url', function () {
seo()->flipp('blog', 'abcdefg');
expect(seo()->flipp('blog', ['title' => 'abc']))
->toContain('?s=' . hash_hmac('sha256', 'blog' . base64_encode(json_encode(['title' => 'abc'])), config('services.flipp.key')));
});
test("flipp templates use default data when they're not passed any data explicitly", function () {
seo()->flipp('blog', 'abcdefg');
seo()->title('foo')->description('bar');
expect(seo()->flipp('blog'))
->toContain('s.useflipp.com/blog')
->toContain(base64_encode(json_encode(['title' => 'foo', 'description' => 'bar'])));
});
test('flipp images are used as the cover images', function () {
seo()->flipp('blog', 'abcdefg');
seo()->title('foo')->description('bar');
expect(seo()->flipp('blog'))
->toBe(seo('image'));
});

20
tests/Pest/HelperTest.php Normal file
View file

@ -0,0 +1,20 @@
<?php
use ArchTech\SEO\SEOManager;
test('the seo helper returns a SEOManager instance when no arguments are passed', function () {
expect(seo())->toBeInstanceOf(SEOManager::class);
});
test('the seo helper returns a value when an argument is passed', function () {
seo()->title('foo');
expect(seo('title'))->toBe('foo');
});
test('the seo helper accepts an array of key-value pairs', function () {
seo(['foo' => 'bar', 'abc' => 'xyz']);
expect(seo('foo'))->toBe('bar');
expect(seo('abc'))->toBe('xyz');
});

View file

@ -0,0 +1,68 @@
<?php
use ArchTech\SEO\SEOManager;
test('set returns the set value', function () {
expect(seo()->set('foo', 'bar'))->toBe('bar');
});
test('the __call proxy is chainable', function () {
expect(seo()->foo('bar'))->toBeInstanceOf(SEOManager::class);
});
test('default values can be set in the proxy call', function () {
seo()->title(default: 'foo');
expect(seo('title'))->toBe('foo');
seo()->title('bar');
expect(seo('title'))->toBe('bar');
});
test('default values can be set in the proxy call alongside the value', function () {
seo()->description('bar', default: 'foo');
expect(seo('description'))->toBe('bar');
});
test('metadata can be used as strings', function () {
seo()->meta('foo', 'bar');
expect(seo()->meta('foo'))->toBe('bar');
});
test('metadata can be used as arrays', function () {
seo()->meta('abc', ['def' => 'xyz']);
expect(seo()->meta('abc.def'))->toBe('xyz');
seo()->meta('abc.def', 'xxx');
expect(seo()->meta('abc.def'))->toBe('xxx');
seo()->meta(['abc.def' => 'yyy']);
expect(seo()->meta('abc.def'))->toBe('yyy');
});
test('values can be set magically', function () {
seo()->foo = 'bar';
expect(seo('foo'))->toBe('bar');
expect(seo()->foo)->toBe('bar');
});
test('magic access respects modifiers', function () {
seo()->foo(modify: 'strtoupper');
seo()->foo = 'bar';
expect(seo('foo'))->toBe('BAR');
expect(seo()->foo)->toBe('BAR');
});
test('magic access gets converted to dot syntax', function () {
seo()->fooBar('baz');
expect(seo('foo.bar'))->toBe('baz');
expect(seo()->fooBar)->toBe('baz');
seo()->abcDef = 'xyz';
expect(seo('abc.def'))->toBe('xyz');
expect(seo()->abcDef)->toBe('xyz');
});

View file

@ -0,0 +1,21 @@
<?php
test('values can be modified using modifiers', function () {
seo()->title(modify: fn (string $title) => $title . ' | ArchTech');
seo()->title('About us');
expect(seo('title'))->toBe('About us | ArchTech');
});
test('modifiers are applied on values returned from set', function () {
seo()->title(modify: fn (string $title) => $title . ' | ArchTech');
expect(seo(['title' => 'Blog']))->toHaveKey('title', 'Blog | ArchTech');
});
test('modifiers are not applied on default values', function () {
seo()->title(modify: fn (string $title) => $title . ' | ArchTech', default: 'ArchTech — Web development agency');
expect(seo('title'))->toBe('ArchTech — Web development agency');
});

View file

@ -1,16 +1,18 @@
<?php <?php
namespace ArchTech\REPLACE\Tests; namespace ArchTech\SEO\Tests;
use Orchestra\Testbench\TestCase as TestbenchTestCase; use Orchestra\Testbench\TestCase as TestbenchTestCase;
use ArchTech\REPLACE\REPLACEServiceProvider; use ArchTech\SEO\SEOServiceProvider;
use ImLiam\BladeHelper\BladeHelperServiceProvider;
class TestCase extends TestbenchTestCase class TestCase extends TestbenchTestCase
{ {
protected function getPackageProviders($app) protected function getPackageProviders($app)
{ {
return [ return [
REPLACEServiceProvider::class, SEOServiceProvider::class,
BladeHelperServiceProvider::class,
]; ];
} }
} }

View file

@ -0,0 +1 @@
<meta name="facebook:title" content="{{ strtoupper(seo()->facebookTitle) }}" />