mirror of
https://github.com/archtechx/laravel-seo.git
synced 2025-12-12 01:44:03 +00:00
finished
This commit is contained in:
parent
051a32575c
commit
b12c9ecb55
21 changed files with 750 additions and 86 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -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
190
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
5
assets/views/components/extensions/twitter.blade.php
Normal file
5
assets/views/components/extensions/twitter.blade.php
Normal 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
|
||||||
10
assets/views/components/meta.blade.php
Normal file
10
assets/views/components/meta.blade.php
Normal 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
2
check
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
|
||||||
|
|
@ -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
222
src/SEOManager.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/SEOServiceProvider.php
Normal file
40
src/SEOServiceProvider.php
Normal 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
16
src/helpers.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
38
tests/Pest/BladeTest.php
Normal 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');
|
||||||
|
});
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
it('succeeds', function () {
|
|
||||||
expect(true)->toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails', function () {
|
|
||||||
expect(false)->toBeTrue();
|
|
||||||
});
|
|
||||||
72
tests/Pest/ExtensionTest.php
Normal file
72
tests/Pest/ExtensionTest.php
Normal 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
42
tests/Pest/FlippTest.php
Normal 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
20
tests/Pest/HelperTest.php
Normal 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');
|
||||||
|
});
|
||||||
68
tests/Pest/ManagerTest.php
Normal file
68
tests/Pest/ManagerTest.php
Normal 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');
|
||||||
|
});
|
||||||
21
tests/Pest/ModifierTest.php
Normal file
21
tests/Pest/ModifierTest.php
Normal 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');
|
||||||
|
});
|
||||||
|
|
@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
tests/views/components/facebook.blade.php
Normal file
1
tests/views/components/facebook.blade.php
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<meta name="facebook:title" content="{{ strtoupper(seo()->facebookTitle) }}" />
|
||||||
Loading…
Add table
Add a link
Reference in a new issue