Repository: romanzipp/Laravel-SEO
Branch: master
Commit: 27293c1583e5
Files: 90
Total size: 154.7 KB
Directory structure:
gitextract_b88zp0c2/
├── .editorconfig
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows/
│ ├── php-cs-fixer.yml
│ ├── phpstan.yml
│ └── tests.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── LICENSE.md
├── README.md
├── composer.json
├── config/
│ └── seo.php
├── deploy-docs.sh
├── docs/
│ ├── .vuepress/
│ │ └── config.js
│ ├── README.md
│ ├── example-app.md
│ ├── hooks.md
│ ├── laravel-mix.md
│ ├── schema-org.md
│ ├── structs.md
│ └── usage.md
├── package.json
├── phpstan.neon.dist
├── phpunit.xml
├── src/
│ ├── Builders/
│ │ └── StructBuilder.php
│ ├── Collections/
│ │ ├── Contracts/
│ │ │ └── CollectionContract.php
│ │ ├── SchemaCollection.php
│ │ └── StructCollection.php
│ ├── Conductors/
│ │ ├── ArrayFormatConductor.php
│ │ ├── ArrayStructures/
│ │ │ ├── AbstractArraySchema.php
│ │ │ ├── AttributeArraySchema.php
│ │ │ ├── NestedArraySchema.php
│ │ │ └── SingleArraySchema.php
│ │ ├── MixManifestConductor.php
│ │ ├── RenderConductor.php
│ │ └── Types/
│ │ └── ManifestAsset.php
│ ├── Enums/
│ │ └── HookTarget.php
│ ├── Exceptions/
│ │ └── ManifestNotFoundException.php
│ ├── Facades/
│ │ └── Seo.php
│ ├── Helpers/
│ │ └── Hook.php
│ ├── Providers/
│ │ └── SeoServiceProvider.php
│ ├── Schema/
│ │ └── Schema.php
│ ├── Services/
│ │ ├── SeoService.php
│ │ └── Traits/
│ │ ├── CollisionTrait.php
│ │ ├── SchemaOrgTrait.php
│ │ └── ShorthandSetterTrait.php
│ ├── Structs/
│ │ ├── Base.php
│ │ ├── Link/
│ │ │ └── Canonical.php
│ │ ├── Link.php
│ │ ├── Meta/
│ │ │ ├── AppLink.php
│ │ │ ├── Article.php
│ │ │ ├── Charset.php
│ │ │ ├── CsrfToken.php
│ │ │ ├── Description.php
│ │ │ ├── EmbedX.php
│ │ │ ├── OpenGraph.php
│ │ │ ├── Robots.php
│ │ │ ├── Twitter.php
│ │ │ └── Viewport.php
│ │ ├── Meta.php
│ │ ├── Noscript.php
│ │ ├── Script.php
│ │ ├── Struct.php
│ │ ├── Title.php
│ │ └── Traits/
│ │ └── HookableTrait.php
│ ├── Values/
│ │ ├── Attribute.php
│ │ ├── Body.php
│ │ └── Value.php
│ └── helpers.php
└── tests/
├── ArrayFormatTest.php
├── CollisionTest.php
├── EscapingTest.php
├── HooksTest.php
├── InstantiationTest.php
├── MixManifestAssetAttributesTest.php
├── MixManifestTest.php
├── RenderTest.php
├── SchemaOrgTest.php
├── SectionsTest.php
├── SetterTest.php
├── ShorthandSettersTest.php
├── Structs/
│ ├── UniqueMultiAttributeStruct.php
│ └── UniqueSingleAttributeStruct.php
├── Support/
│ ├── mix-manifest.empty.json
│ ├── mix-manifest.json
│ ├── mix-manifest.null.json
│ └── mix-manifest.versioned.json
├── TestCase.php
└── ValueTypesTest.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml,neon,neon.dist}]
indent_size = 2
================================================
FILE: .github/FUNDING.yml
================================================
github: romanzipp
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/workflows/php-cs-fixer.yml
================================================
name: PHP-CS-Fixer
on: [ push ]
jobs:
phpcs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.4
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
- name: Execute PHP-CS-Fixer
run: vendor/bin/php-cs-fixer fix
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Fix styling
================================================
FILE: .github/workflows/phpstan.yml
================================================
name: PHPStan
on: [ push ]
jobs:
phpstan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.4
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-scripts --no-progress --prefer-dist
- name: Execute PHPStan
run: vendor/bin/phpstan analyse
================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests
on: [ push, pull_request ]
jobs:
test:
strategy:
fail-fast: false
matrix:
php: [ "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3" ]
composer-dependency: [ prefer-stable, prefer-lowest ]
exclude:
- php: "8.1"
composer-dependency: prefer-lowest
- php: "8.2"
composer-dependency: prefer-lowest
- php: "8.3"
composer-dependency: prefer-lowest
name: "PHP ${{ matrix.php }} - ${{ matrix.composer-dependency }}"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:${{ matrix.php == '7.1' && 'v2.2' || 'v2' }}
coverage: none
- name: Install dependencies
run: |
composer global update
composer update --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist --${{ matrix.composer-dependency }}
- name: Execute tests
run: vendor/bin/phpunit
================================================
FILE: .gitignore
================================================
# Created by https://www.gitignore.io/api/composer
### Composer ###
composer.phar
composer.lock
/vendor/
# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
# composer.lock
# End of https://www.gitignore.io/api/composer
build/*
.phpunit.result.cache
.idea/
.php_cs.cache
.php-cs-fixer.cache
docs/.vuepress/dist
node_modules/
================================================
FILE: .php-cs-fixer.dist.php
================================================
in(__DIR__)
->preset(
new romanzipp\Fixer\Presets\PrettyLaravel()
)
->out();
================================================
FILE: LICENSE.md
================================================
The MIT License
Copyright (c) 2020 Roman Zipp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Laravel SEO
[](https://packagist.org/packages/romanzipp/laravel-seo)
[](https://packagist.org/packages/romanzipp/laravel-seo)
[](https://packagist.org/packages/romanzipp/laravel-seo)
[](https://github.com/romanzipp/Laravel-SEO/actions)
A SEO package made for maximum customization and flexibility.
## Documentation
The full package documentation can be found on [romanzipp.github.io/Laravel-SEO](https://romanzipp.github.io/Laravel-SEO/)

## Testing
```
./vendor/bin/phpunit
```
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
[](https://star-history.com/#romanzipp/laravel-seo&Date)
================================================
FILE: composer.json
================================================
{
"name": "romanzipp/laravel-seo",
"description": "Laravel SEO package",
"license": "MIT",
"type": "library",
"authors": [
{
"name": "romanzipp",
"email": "ich@ich.wtf",
"homepage": "https://ich.wtf"
}
],
"require": {
"php": "^7.1|^8.0",
"ext-json": "*",
"illuminate/console": ">=5.5",
"illuminate/support": ">=5.5",
"spatie/schema-org": "^2.1|^3.2"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",
"orchestra/testbench": ">=3.8",
"phpstan/phpstan": "^0.12.99|^1.0",
"phpunit/phpunit": ">=7.0",
"romanzipp/php-cs-fixer-config": "^3.0"
},
"autoload": {
"psr-4": {
"romanzipp\\Seo\\": "src"
},
"files": [
"src/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"romanzipp\\Seo\\Test\\": "tests"
}
},
"scripts": {
"test": "vendor/bin/phpunit"
},
"extra": {
"laravel": {
"providers": [
"romanzipp\\Seo\\Providers\\SeoServiceProvider"
],
"aliases": {
"Seo": "romanzipp\\Seo\\Facades\\Seo"
}
}
},
"config": {
"sort-packages": true
}
}
================================================
FILE: config/seo.php
================================================
[
/*
* Decide, which tags should be created when using the
* Seo Service shorthand methods like seo()->title(...)
*/
'title' => [
//
...
'tag' => true,
//
'opengraph' => true,
//
'twitter' => true,
//
'embedx' => true,
],
'description' => [
//
'meta' => true,
//
'opengraph' => true,
//
'twitter' => true,
//
'embedx' => true,
],
'image' => [
//
'meta' => true,
//
'opengraph' => true,
//
'twitter' => true,
//
'embedx' => true,
],
],
// Available options:
// - StructBuilder::TAG_SYNTAX_HTML5:
// - StructBuilder::TAG_SYNTAX_XHTML:
// - StructBuilder::TAG_SYNTAX_XHTML_STRICT:
'tag_syntax' => \romanzipp\Seo\Builders\StructBuilder::TAG_SYNTAX_XHTML,
];
================================================
FILE: deploy-docs.sh
================================================
#!/usr/bin/env sh
# abort on errors
set -e
# build
npm run docs:build
# navigate into the build output directory
cd docs/.vuepress/dist
git init
git add -A
git commit -m 'deploy'
git push -f git@github.com:romanzipp/Laravel-SEO.git master:gh-pages
cd -
================================================
FILE: docs/.vuepress/config.js
================================================
module.exports = {
base: '/Laravel-SEO/',
title: 'Laravel SEO',
description: 'SEO package made for maximum customization and flexibility ',
host: 'localhost',
port: 3001,
themeConfig: {
nav: [
{ text: 'Home', link: '/' },
{ text: 'GitHub', link: 'https://github.com/romanzipp/Laravel-SEO' },
{ text: 'Packagist', link: 'https://packagist.org/packages/romanzipp/laravel-seo' },
],
sidebar: [
'/',
'/usage',
'/structs',
'/hooks',
'/laravel-mix',
'/schema-org',
'/example-app',
],
displayAllHeaders: true,
sidebarDepth: 2
}
};
================================================
FILE: docs/README.md
================================================
# Introduction
## Installation
```
composer require romanzipp/laravel-seo
```
## Configuration
Copy configuration to config folder:
```
$ php artisan vendor:publish --provider="romanzipp\Seo\Providers\SeoServiceProvider"
```
## Integrations
### Laravel-Mix
This package can automatically preload all generated frontend assets via the Laravel Mix manifest.
See the [Laravel-Mix integration docs](/laravel-mix.html) for more information.
### Schema.org
We also feature a basic integration for [Spaties Schema.org](https://github.com/spatie/schema-org) package to generate ld+json scripts.
See the [Schema.org integration docs](/schema-org.html) for more information.
## Upgrading
- [Upgrading from 1.0 to **2.0**](https://github.com/romanzipp/Laravel-SEO/releases/tag/2.0.0)
## Cheat Sheet
| Code | Rendered HTML |
|----|----|
| **Shorthand Setters** | |
| `seo()->title('Laravel')` | `Laravel` |
| `seo()->description('Laravel')` | `` |
| `seo()->meta('author', 'Roman Zipp')` | `` |
| `seo()->twitter('card', 'summary')` | `` |
| `seo()->og('site_name', 'Laravel')` | `` |
| `seo()->charset()` | `` |
| `seo()->viewport()` | `` |
| `seo()->csrfToken()` | `` |
| **Adding Structs** | |
| `seo()->add(...)` | `` |
| `seo()->addMany([...])` | `` |
| `seo()->addIf(true, ...)` | `` |
| **Various** | |
| `seo()->mix()` | |
| `seo()->hook()` | |
| `seo()->render()` | |
================================================
FILE: docs/example-app.md
================================================
# Example App
## Service Provider
```
$ php artisan make:provider SeoServiceProvider
```
#### `Providers/SeoServiceProvider.php`
```php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use romanzipp\Seo\Builders\StructBuilder;
use romanzipp\Seo\Facades\Seo;
use romanzipp\Seo\Helpers\Hook;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Title;
class SeoServiceProvider extends ServiceProvider
{
public function boot()
{
StructBuilder::$indent = str_repeat(' ', 4);
// Add a getTitle method for obtaining the unmodified title
Seo::macro('getTitle', function () {
/** @var \romanzipp\Seo\Services\SeoService $this */
if ( ! $title = $this->getStruct(Title::class)) {
return null;
}
if ( ! $body = $title->getBody()) {
return null;
}
return $body->getOriginalData();
});
// Create a custom macro
Seo::macro('customTag', function (string $value) {
/** @var \romanzipp\Seo\Services\SeoService $this */
return $this->add(
Meta::make()->name('custom')->content($value)
);
});
// Add a hook to ensure the site name is always appended to the title
Title::hook(
Hook::make()
->onBody()
->callback(function ($body) {
return ($body ? $body . ' | ' : '') . 'Site-Name';
})
);
}
}
```
## Middleware
```
$ php artisan make:middleware AddSeoDefaults
```
#### `Http/Middleware/AddSeoDefaults.php`
```php
namespace App\Http\Middleware;
use Closure;
use romanzipp\Seo\Structs\Link;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta\OpenGraph;
use romanzipp\Seo\Structs\Meta\Twitter;
use romanzipp\Seo\Structs\Script;
class AddSeoDefaults
{
public function handle($request, Closure $next)
{
seo()->charset();
seo()->viewport();
seo()->title('Home');
seo()->description('My Description');
seo()->csrfToken();
seo()->addMany([
Meta::make()->name('copyright')->content('Roman Zipp'),
Meta::make()->name('mobile-web-app-capable')->content('yes'),
Meta::make()->name('theme-color')->content('#f03a17'),
Link::make()->rel('icon')->href('/assets/images/Logo.png'),
OpenGraph::make()->property('title')->content('Laravel'),
OpenGraph::make()->property('site_name')->content('Laravel'),
OpenGraph::make()->property('locale')->content('de_DE'),
Twitter::make()->name('card')->content('summary_large_image'),
Twitter::make()->name('site')->content('@romanzipp'),
Twitter::make()->name('creator')->content('@romanzipp'),
Twitter::make()->name('image')->content('/assets/images/Banner.jpg', false)
]);
seo('body')->add(
Script::make()->attr('src', '/js/app.js')
);
return $next($request);
}
}
```
## Controllers
```php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request)
{
seo()->title('All Posts');
$posts = Post::all();
return view('posts.index', compact('posts'));
}
public function show(Request $request, Post $post)
{
seo()->title($post->title ?: "Post No. {$post->id}");
seo()->description($post->intro);
seo()->image($post->thumbnail);
return view('posts.show', compact('post'));
}
}
```
## View
```blade
{{ seo()->render() }}
@yield('content')
{{ seo('body')->render() }}
```
================================================
FILE: docs/hooks.md
================================================
# Hooks
Hooks allow the modification of a Structs **body** or **attributes**.
### Adding hooks to Structs
```php
use romanzipp\Seo\Helpers\Hook;
$hook = Hook::make()
->onBody()
->callback(function ($body) {
return $body;
});
```
**Method 1**: Call the `SeoService::hook()` method to apply a given `$hook` to a Struct class.
```php
use romanzipp\Seo\Structs\Title;
seo()->hook(Title::class, $hook);
```
**Method 2**: Apply the `$hook` directly to the Struct.
```php
use romanzipp\Seo\Structs\Title;
Title::hook($hook);
```
Both methods are basically the same, choose which one you prefer.
## Examples
For example, you want to append a site name to the body of every `` tag:
#### Modify the `body` of all `Title` Structs.
```php
use romanzipp\Seo\Helpers\Hook;
use romanzipp\Seo\Structs\Title;
Title::hook(
Hook::make()
->onBody()
->callback(function ($body) {
return ($body ? $body . ' | ' : '') . 'Site-Name';
})
);
```
```php
use romanzipp\Seo\Structs\Title;
seo()->add(Title::make()->body('Home')); // Home | Site-Name
seo()->add(Title::make()->body(null)); // Site-Name
```
----
#### Modify any attribute of the `OpenGraph` Struct which has the attribute `property` with value `og:site_name`
```php
use romanzipp\Seo\Helpers\Hook;
use romanzipp\Seo\Structs\Meta\OpenGraph;
OpenGraph::hook(
Hook::make()
->whereAttribute('property', 'og:site_name')
->onAttributes()
->callback(function ($attributes) {
$attributes['new'] = 'This will be added to all meta tags with property="og:site_name"';
return $attributes;
})
);
```
----
#### Modify the `content` attribute of the `OpenGraph` Struct which has the attribute `property` with value `og:title`
```php
use romanzipp\Seo\Helpers\Hook;
use romanzipp\Seo\Structs\Meta\OpenGraph;
OpenGraph::hook(
Hook::make()
->whereAttribute('property', 'og:title')
->onAttribute('content')
->callback(function ($content) {
return ($content ? $content . ' | ' : '') . 'Site-Name';
})
);
```
```php
use romanzipp\Seo\Structs\Meta\OpenGraph;
$seo->add(OpenGraph::make()->property('title')->content('Home')); //
$seo->add(OpenGraph::make()->property('title')->content(null)); //
```
## Reference
### Hook Instance
```php
use romanzipp\Seo\Helpers\Hook;
$hook = Hook::make();
$hook = new Hook;
```
### Hook Targets
#### Target Struct Body
You will receive `$body` parameter of type `null|string` in the callback function
```php
$hook
->onBody()
->callback(function ($body) {
return $body;
});
```
#### Target any Struct Attribute
You will receive `$attributes` parameter of type `array` in the callback function
```php
$hook
->onAttributes('content')
->callback(function ($attributes) {
return $attributes;
});
```
#### Target a specific Struct Attribute
You will receive `$attribute` parameter of type `null|string` in the callback function
```php
$hook
->onAttribute('content')
->callback(function ($attribute) {
return $attribute;
});
```
### Hook Filters
Filter Structs by `$attribute` with value `$value`
```php
$hook->whereAttribute($attribute, $value);
```
================================================
FILE: docs/laravel-mix.md
================================================
# Laravel-Mix
You can include your `mix-manifest.json` file generated by [Laravel-Mix](https://laravel-mix.com) to automatically add preload/prefetch link elements to your document head.
## Basic example
```php
seo()
->mix()
->load();
```
**mix-manifest.json**
```json
{
"/js/app.js": "/js/app.js?id=123456789",
"/css/app.css": "/css/app.css?id=123456789"
}
```
**document ``**
```html
```
## Specify an alternate manifest path
```php
seo()
->mix()
->load(public_path('custom-manifest.json'));
```
## Ignore certain assets
By default, all assets are added to the document head. You can specify filters or rejections to hide certain assets like admin scripts. The callbacks are passed through the Laravel collection instance.
In this example, we will stop all **admin** frontend assets from prefetching by returning `null` within the provided map callback.
```php
use romanzipp\Seo\Conductors\Types\ManifestAsset;
seo()
->mix()
->map(static function(ManifestAsset $asset): ?ManifestAsset {
if (strpos($asset->path, 'admin') !== false) {
return null;
}
return $asset;
})
->load();
```
**mix-manifest.json**
```json
{
"/js/app.js": "/js/app.js?id=123456789",
"/js/admin.js": "/js/admin.js?id=123456789",
"/css/app.css": "/css/app.css?id=123456789",
"/css/admin.css": "/css/admin.css?id=123456789"
}
```
**document ``**
```html
```
## Provide an absolute URL
You can force your preloaded/prefetched assets to use an alternate URL by modifying the `url` attribute.
```php
use romanzipp\Seo\Conductors\Types\ManifestAsset;
seo()
->mix()
->map(static function(ManifestAsset $asset): ?ManifestAsset {
$asset->url = "http://localhost{$asset->url}";
return $asset;
})
->load();
```
**mix-manifest.json**
```json
{
"/js/app.js": "/js/app.js?id=123456789",
"/css/app.css": "/css/app.css?id=123456789"
}
```
**document ``**
```html
```
## Change mechanism
By default, all assets found in your mix file are inserted with the `prefetch` mechanism. You can read more about preloading and prefetching [in this article by css-tricks.com](https://css-tricks.com/prefetching-preloading-prebrowsing/).
You are also free to change the default `prefetch` value to `preload` using the map callback. The following code example will `preload` all assets containing "component" or otherwise fall back on `prefetch`.
```php
use romanzipp\Seo\Conductors\Types\ManifestAsset;
seo()
->mix()
->map(static function(ManifestAsset $asset): ?ManifestAsset {
$asset->rel = 'prefetch';
if (strpos($asset->path, 'component') !== false) {
$asset->rel = 'preload';
}
return $asset;
})
->load();
```
**mix-manifest.json**
```json
{
"/js/app.js": "/js/app.js?id=123456789",
"/js/app.routes.js": "/js/app.routes.js?id=123456789",
"/js/app.user-component.js": "/js/app.user-component.js?id=123456789",
"/js/app.news-component.js": "/js/app.news-component.js?id=123456789"
}
```
**document ``**
```html
```
## Asset resource type
Preloading content required a minimum of `href` and `as` attribute. This package will guess a [resource type](https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content) based on the provided file extension. Currently script, style, font, image and video are supported.
Feels free to change the resource type.
```php
use romanzipp\Seo\Conductors\Types\ManifestAsset;
seo()
->mix()
->map(static function(ManifestAsset $asset): ?ManifestAsset {
if (strpos($asset->path, 'virus') !== false) {
$asset->as = 'virus';
}
return $asset;
})
->load();
```
**mix-manifest.json**
```json
{
"/css/app.css": "/css/app.css?id=123456789",
"/js/app.js": "/js/app.routes.js?id=123456789",
"/data/totally-not-a-virus": "/data/totally-not-a-virus?id=123456789",
"/data/totally-not-a-virus": "/data/totally-not-a-virus?id=123456789"
}
```
**document ``**
```html
```
================================================
FILE: docs/schema-org.md
================================================
# Schema.org Integration
This package features a basic integration for [Spaties Schema.org](https://github.com/spatie/schema-org) package to generate ld+json scripts.
Added Schema types render with the packages structs.
```php
use Spatie\SchemaOrg\Schema;
seo()->addSchema(
Schema::localBusiness()->name('Spatie')
);
```
```php
use Spatie\SchemaOrg\Schema;
seo()->setSchemes([
Schema::localBusiness()->name('Spatie'),
Schema::airline()->name('Spatie'),
]);
```
================================================
FILE: docs/structs.md
================================================
# Structs
**Structs** are a code representation of **HTML head elements**.
## Available Shorthand Methods
Shorthand methods are **predefined shortcuts** to add commonly used Structs without the hassle of importing struct classes or chain many methods.
When using shorthand methods, you will skip the `seo()->add()` method.
You can configure which Structs should be added on shorthand calls in the `seo.php` config file under the `shorthand` key.
### Title
```php
seo()->title(string $title = null, bool $escape = true);
```
same as ...
```php
use romanzipp\Seo\Structs\Title;
use romanzipp\Seo\Structs\Meta;
seo()->addMany([
Title::make()
->body(string $title = null),
Meta\OpenGraph::make()
->property('title')
->content(string $title = null),
Meta\Twitter::make()
->name('title')
->content(string $title = null),
Meta\EmbedX::make()
->name('title')
->content(string $title = null),
]);
```
renders to ...
```html
{title}
```
### Description
```php
seo()->description(string $description = null, bool $escape = true);
```
same as ...
```php
use romanzipp\Seo\Structs\Meta;
seo()->addMany([
Meta\Description::make()
->name('description')
->content(string $description = null),
Meta\OpenGraph::make()
->property('description')
->content(string $description = null),
Meta\Twitter::make()
->name('description')
->content(string $description = null),
Meta\EmbedX::make()
->name('description')
->content(string $description = null),
]);
```
renders to ...
```html
```
### Image
```php
seo()->image(string $image = null, bool $escape = true);
```
same as ...
```php
use romanzipp\Seo\Structs\Meta;
seo()->addMany([
Meta::make()
->name('image')
->content($image, $escape),
Meta\OpenGraph::make()
->property('image')
->content($image, $escape),
Meta\Twitter::make()
->name('image')
->content($image, $escape),
Meta\EmbedX::make()
->name('image')
->content($image, $escape),
]);
```
renders to ...
```html
```
### Meta
```php
seo()->meta(string $name, $content = null, bool $escape = true);
```
same as ...
```php
use romanzipp\Seo\Structs\Meta;
seo()->add(
Meta::make()
->name(string $name, bool $escape = true)
->content($content = null, bool $escape = true)
);
```
renders to ...
```html
```
### OpenGraph
```php
seo()->og(string $property, $content = null, bool $escape = true);
```
same as ...
```php
use romanzipp\Seo\Structs\Meta\OpenGraph;
seo()->add(
OpenGraph::make()
->property(string $property, bool $escape = true)
->content($content = null, bool $escape = true)
);
```
renders to ...
```html
```
### Twitter
```php
seo()->twitter(string $name, $content = null, bool $escape = true);
```
same as ...
```php
use romanzipp\Seo\Structs\Meta\Twitter;
seo()->add(
Twitter::make()
->name(string $name, bool $escape = true)
->content($content = null, bool $escape = true)
);
```
renders to ...
```html
```
### [EmbedX](https://embedx.app)
[EmbedX](https://embedx.app) allows you to display rich embed thumbnails on X/Twitter and other social media platforms.
```php
seo()->embedx(string $name, $content = null, bool $escape = true);
```
same as ...
```php
use romanzipp\Seo\Structs\Meta\EmbedX;
seo()->add(
EmbedX::make()
->name(string $name, bool $escape = true)
->content($content = null, bool $escape = true)
);
```
renders to ...
```html
```
### Canonical
```php
seo()->canonical(string $canonical);
```
same as ...
```php
use romanzipp\Seo\Structs\Link\Canonical;
seo()->add(
Canonical::make()
->href($canonical = null)
);
```
renders to ...
```html
```
### CSRF Token
```php
seo()->csrfToken(string $token = null);
```
same as ...
```php
use romanzipp\Seo\Structs\Meta\CsrfToken;
seo()->add(
CsrfToken::make()
->token($token = null)
);
```
renders to ...
```html
```
## Adding single structs
If you need to use more advanced elements which are not covered with shorthand setters, you can easily add single structs to your SEO instance the following way.
*Remember: [There are many methods available for adding new structs](/usage.html#how-to-register-tags)*
### Titles
```php
use romanzipp\Seo\Structs\Title;
seo()->add(
Title::make()->body('This is a Title')
);
```
```html
This is a Title
```
### Meta Tags
Using the `attr(string $attribute, $value = null)` method, we can append attributes with given values.
```php
use romanzipp\Seo\Structs\Meta;
seo()->add(
Meta::make()
->attr('name', 'theme-color')
->attr('content', 'red')
);
```
```html
```
### OpenGraph
Because **OpenGraph** tags are `` elements, the `OpenGraph` Struct extends the `Meta` class.
All **OpenGraph** elements are defined by `property=""` and `content=""` attributes where the `property` value starts with a `og:` prefix.
Instead of using the `attr()` Struct method, we can use the shorthand `property()` and `content()` methods by the `OpenGraph` class.
```php
use romanzipp\Seo\Structs\Meta\OpenGraph;
seo()->add(
OpenGraph::make()
->attr('property', 'og:site_name')
->attr('content', 'Laravel')
);
```
```php
use romanzipp\Seo\Structs\Meta\OpenGraph;
seo()->add(
OpenGraph::make()
->property('site_name')
->content('Laravel')
);
```
... both render to ...
```html
```
### Twitter
**Twitter** meta tags share the same behavior as **OpenGraph** tags while the property prefix is `twitter:`.
```php
use romanzipp\Seo\Structs\Meta\Twitter;
seo()->add(
Twitter::make()
->attr('name', 'twitter:card')
->attr('content', 'summary')
);
```
```php
use romanzipp\Seo\Structs\Meta\Twitter;
seo()->add(
Twitter::make()
->name('card')
->content('summary')
);
```
... both render to ...
```html
```
## Available Structs
### Base
```php
romanzipp\Seo\Structs\Base::make();
```
### Link
```php
romanzipp\Seo\Structs\Link::make();
```
```php
romanzipp\Seo\Structs\Link\Canonical::make();
```
### Meta
```php
romanzipp\Seo\Structs\Meta::make();
```
```php
romanzipp\Seo\Structs\Meta\Article::make()
->property(string $value, bool $escape = true)
->content(string $value, bool $escape = true);
```
```php
romanzipp\Seo\Structs\Meta\AppLink::make()
->property(string $value, bool $escape = true)
->content(string $value, bool $escape = true);
```
```php
romanzipp\Seo\Structs\Meta\Charset::make()
->charset(string $charset, bool $escape = true);
```
```php
romanzipp\Seo\Structs\Meta\CsrfToken::make()
->token($token = null, bool $escape = true);
```
```php
romanzipp\Seo\Structs\Meta\Description::make();
```
```php
romanzipp\Seo\Structs\Meta\OpenGraph::make()
->property(string $value, bool $escape = true)
->content(string $value = null, bool $escape = true);
```
```php
romanzipp\Seo\Structs\Meta\Robots::make();
```
```php
romanzipp\Seo\Structs\Meta\Twitter::make()
->name(string $value, bool $escape = true)
->content(string $value, bool $escape = true);
```
```php
romanzipp\Seo\Structs\Meta\Viewport::make()
->content(string $content, bool $escape = true);
```
### Noscript
```php
romanzipp\Seo\Structs\Noscript::make();
```
### Script
```php
romanzipp\Seo\Structs\Script::make();
```
### Title
```php
romanzipp\Seo\Structs\Title::make();
```
## Escaping
By default, all body and attribute content is escaped via the Laravel [`e()`](https://github.com/illuminate/support/blob/5.8/helpers.php#L607) helper function. You can change this behavior by setting the `$escape` parameter on all attribute setters.
**Use this feature with caution!**
```php
use romanzipp\Seo\Structs\Title;
Title::make()->body('Dont \' escape me!', false);
```
```php
use romanzipp\Seo\Structs\Meta;
Meta::make()->attr('content', 'Dont \' escape me!', false);
```
## Creating custom Structs
You can create your own Structs simply by extending the `romanzipp\Seo\Structs\Struct` class.
```php
use romanzipp\Seo\Structs\Struct;
class MyStruct extends Struct
{
//
}
```
We differentiate between [**void elements**](https://www.w3.org/TR/html5/syntax.html#writing-html-documents-elements) and **normal elements**.
**Void elements**, like `` can not have a closing tag other than **normal elements** like ``.
### Tag
A struct **always** requires a **tag**. This can be set by implementing the abstract `tag()` method.
```php
protected function tag(): string
{
return 'script';
}
```
### Unique tags
Certain elements in a documents `` can only exist once, like the `` element.
By default, Structs are **not** unique. To change this behavior, apply the `unique` property.
```php
protected $unique = true;
```
Now, previously created Structs will be overwritten.
```php
use romanzipp\Seo\Structs\Struct;
class MyStruct extends Struct
{
protected $unique = false;
protected function tag(): string
{
return 'foo';
}
}
seo()->add(MyStruct::make()->attr('name', 'my-description')->attr('content', 'This is the FIRST description'));
seo()->add(MyStruct::make()->attr('name', 'my-description')->attr('content', 'This is the SECOND description'));
```
**Before**: (`$unique = false`)
```html
```
**After**: (`$unique = true`)
```html
```
### Unique attributes
You are also able to modify the unique attributes by setting the `uniqueAttributes` property. If empty, just the tag name will be considered as unique.
```php
use romanzipp\Seo\Structs\Struct;
class MyStruct extends Struct
{
protected $uniqueAttributes = ['name'];
protected function tag(): string
{
return 'foo';
}
}
seo()->add(MyStruct::make()->attr('name', 'my-description')->attr('content', 'This is the FIRST description'));
seo()->add(MyStruct::make()->attr('name', 'my-description')->attr('content', 'This is the SECOND description'));
seo()->add(MyStruct::make()->attr('name', 'my-title')->attr('content', 'This is the FIRST title'));
seo()->add(MyStruct::make()->attr('name', 'my-title')->attr('content', 'This is the SECOND title'));
```
**Before**: (`$uniqueAttributes = []`)
```html
```
**After**: (`$uniqueAttributes = ['name']`)
```html
```
### Defaults
After a Struct instance has been created, we call the static `defaults` method.
```php
use romanzipp\Seo\Structs\Struct;
class MyStruct extends Struct
{
public function __construct()
{
static::defaults($this);
}
public static function defaults(self $struct): void
{
//
}
}
```
By implementing the `defaults` method on your custom Struct, you can run any custom logic like adding default attributes.
This is used among others in the `romanzipp\Seo\Structs\Meta\Charset` Struct to set a default charset attribute.
```php
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Struct;
class Charset extends Meta
{
public static function defaults(Struct $struct): void
{
$struct->addAttribute('charset', 'utf-8');
}
}
```
================================================
FILE: docs/usage.md
================================================
# Usage
## Instantiation
You can access the SEO service in many different ways. Just use what you prefer! We will use the `seo()` function in this documentaiton.
```php
use romanzipp\Seo\Facades\Seo;
use romanzipp\Seo\Services\SeoService;
$seo = seo();
$seo = app(SeoService::class);
$seo = Seo::make();
```
## Render
Place this code snippet in your blade view.
```blade
{{ seo()->render() }}
```
## How to register tags
ℹ️ Going forward we will refer to head/meta elements as **Structs**.
This package offers many ways of adding new elements (**Structs**) to your ``.
1. Add commonly used structs via [shorthand setters](#shorthand-setters) like `seo()->title('...')`
2. Manually add single structs via the `seo()->add()` [methods](#add-structs)
3. Specify an [array of contents](#array-format) via `seo()->addFromArray()`
### Shorthand setters
Shorthand setters are **predefined shortcuts** to add commonly used Structs without the hassle of importing struct classes or chain many methods.
When using shorthand methods, you will skip the `seo()->add()` method.
You can configure which Structs should be added on shorthand calls in the `seo.php` config file under the `shorthand` key.
#### Title
```php
seo()->title('Laravel');
```
... renders to ...
```html
Laravel
```
#### Meta
```php
seo()->meta('copyright', 'Roman Zipp');
```
... renders to ...
```html
```
Take a look at the [shorthand setter docs](/structs.html#available-shorthand-methods) for all available methods.
### Add Structs
If you need to use more advanced elements which are not covered with shorthand setters, you can easily add single structs to your SEO instance the following way.
Further reading: [Adding single structs](/structs.html#adding-single-structs)
#### Single Structs
```php
use romanzipp\Seo\Structs\Title;
seo()->add(
Title::make()->body('My Title')
);
```
#### Multiple Structs
```php
use romanzipp\Seo\Structs\Title;
use romanzipp\Seo\Structs\Meta\Description;
seo()->addMany([
Title::make()->body('My Title'),
Description::make()->content('My Description'),
]);
```
#### Conditional additions
```php
use romanzipp\Seo\Structs\Title;
$boolean = random_int(0, 1) === 1;
seo()->addIf(
$boolean,
Title::make()->body('My Title')
);
```
### Array format
You can also register structs using the following format. This can be helpful if you are fetching SEO information from a database.
```php
seo()->addFromArray([
// The following items share the same behavior as the equally named shorthand setters.
'title' => 'Laravel',
'description' => 'Laravel',
'charset' => 'utf-8',
'viewport' => 'width=device-width, initial-scale=1',
// Twitter & Open Graph
'twitter' => [
//
//
'card' => 'summary',
'creator' => '@romanzipp',
],
'og' => [
//
//
'locale' => 'de',
'site_name' => 'Laravel',
],
// Custom meta & link structs. Each child array defines an attribute => value mapping.
'meta' => [
//
//
[
'name' => 'copyright',
'content' => 'Roman Zipp',
],
[
'name' => 'theme-color',
'content' => '#f03a17',
],
],
'link' => [
//
//
[
'rel' => 'icon',
'href' => '/favicon.ico',
],
[
'rel' => 'preload',
'href' => '/fonts/IBMPlexSans.woff2',
],
],
]);
```
## Sections
You can add structs to different **sections** by calling the `section('foo')` method on the `SeoService` instance or passing it as the first attribute to the `seo('foo')` helper method. By default all Structs will be added to the "default" section.
Sections allow you to create certain namespaces for Structs which can be used in many different ways: Distinct between "frontend" and "admin" page sections or "head" and "body" view sections.
### Using sections
```php
// This struct will be added to the "default" section
seo()->twitter('card', 'summary');
// This struct will be added to the "secondary" section
seo()->section('secondary')->twitter('card', 'image');
// This struct will be also added to the "default" section since the section() method changes are not persistent
seo()->twitter('card', 'summary');
```
You can also pass the section as parameter to the helper function.
```php
seo('secondary')->twitter('card', 'image');
```
### Rendering sections
This will render all structs added to the "default" section.
```blade
{{ seo()->render() }}
```
This will render all structs added to the "secondary" section.
```blade
{{ seo()->section('secondary')->render() }}
```
Of course, you can also pass the section as parameter to the helper function.
```blade
{{ seo('secondary')->render() }}
```
### Using sections with dependency resolving
```php
use romanzipp\Seo\Services\SeoService;
$seo = app(SeoService::class);
// will be applied to "default" section
$seo->twitter('card', 'summary');
// will be applied to "secondary" section
$seo->section('secondary')->twitter('card', 'summary');
// WARNING!
// This struct will be applied to the "secondary" section since the service instance has been resolved
// once and was set to "secondary" section in the previous step
$seo->twitter('card', 'summary');
```
## Macros
The `romanzipp\Seo\Services\SeoService` class uses the Laravel `Macroable` trait which allows creating short macros.
### Example
Let's say you want to display a page title in the document body but added a hook to append the site name.
In this case, we'll create a macro to retreive the original Title Struct body value.
```php
use romanzipp\Seo\Facades\Seo;
use romanzipp\Seo\Structs\Title;
Seo::macro('getTitle', function () {
if ( ! $title = $this->getStruct(Title::class)) {
return null;
}
if ( ! $body = $title->getBody()) {
return null;
}
return $body->getOriginalData();
});
```
## Recommended Minimum
For a full reference of what **could** go to your `` see [joshbuchea's HEAD](https://github.com/joshbuchea/HEAD)
```php
seo()->charset('utf-8');
seo()->viewport('width=device-width, initial-scale=1, viewport-fit=cover');
seo()->title('My Title');
```
```html
My Title
```
## Clear all added Structs
```php
seo()->clearStructs();
```
================================================
FILE: package.json
================================================
{
"scripts": {
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
},
"devDependencies": {
"vuepress": "^1.8.2"
}
}
================================================
FILE: phpstan.neon.dist
================================================
parameters:
phpVersion: 70100
level: 6
paths:
- src
================================================
FILE: phpunit.xml
================================================
tests
src
================================================
FILE: src/Builders/StructBuilder.php
================================================
struct = $struct;
}
/**
* Instantly build struct.
*
* @param \romanzipp\Seo\Structs\Struct $struct
*
* @return \Illuminate\Support\HtmlString
*/
public static function build(Struct $struct): HtmlString
{
return (new self($struct))->render();
}
/**
* Render element.
*
* @return \Illuminate\Support\HtmlString
*/
public function render(): HtmlString
{
$element = '';
if ($indent = self::$indent) {
$element .= $indent;
}
$element .= "<{$this->struct->getTag()}";
if ($attributes = $this->renderAttributes()) {
$element .= " {$attributes}";
}
$body = $this->struct->getBody();
$syntax = config('seo.tag_syntax') ?? self::TAG_SYNTAX_XHTML;
if ($body || ! $this->struct->isVoidElement()) {
$element .= ">{$body}{$this->struct->getTag()}>";
} else {
switch ($syntax) {
case self::TAG_SYNTAX_HTML5:
$element .= '>';
break;
case self::TAG_SYNTAX_XHTML:
$element .= ' />';
break;
case self::TAG_SYNTAX_XHTML_STRICT:
$element .= ">{$this->struct->getTag()}>";
break;
default:
$element .= ' />';
}
}
return new HtmlString($element);
}
/**
* Render struct attributes to string.
*
* @return string
*/
private function renderAttributes(): string
{
$attributes = [];
foreach ($this->struct->getComputedAttributes() as $attribute => $attributeValue) {
$attribute = trim($attribute);
if (null !== $attributeValue->data()) {
$attribute .= "=\"{$attributeValue}\"";
}
$attributes[] = $attribute;
}
return implode(' ', $attributes);
}
}
================================================
FILE: src/Collections/Contracts/CollectionContract.php
================================================
schemas;
}
public function add(SchemaContainer $schema): void
{
$this->schemas[] = $schema;
}
/**
* @param \romanzipp\Seo\Schema\Schema[] $schemas
*/
public function set(array $schemas): void
{
$this->schemas = $schemas;
}
}
================================================
FILE: src/Collections/StructCollection.php
================================================
structs;
}
public function add(Struct $struct): void
{
$this->structs[] = $struct;
}
/**
* @param \romanzipp\Seo\Structs\Struct[] $structs
*/
public function set(array $structs): void
{
$this->structs = $structs;
}
public function unset(int $index): void
{
unset($this->structs[$index]);
}
public function remove(Struct $struct): void
{
$this->structs = array_filter($this->structs, function (Struct $existing) use ($struct): bool {
return $existing !== $struct;
});
}
}
================================================
FILE: src/Conductors/ArrayFormatConductor.php
================================================
seo = $seo;
}
/**
* Get the predefined schemas for array formatting.
*
* @return array
*/
private function getSchemas(): array
{
return [
/*
* Single key-value pair.
*
* $data = [
* 'title' => 'Foo'
* ];
*/
'title' => SingleArraySchema::make()->callback(function (string $value) {
$this->seo->title($value);
}),
'description' => SingleArraySchema::make()->callback(function (string $value) {
$this->seo->description($value);
}),
'charset' => SingleArraySchema::make()->callback(function (string $value) {
$this->seo->charset($value);
}),
'viewport' => SingleArraySchema::make()->callback(function (string $value) {
$this->seo->viewport($value);
}),
'canonical' => SingleArraySchema::make()->callback(function (string $value) {
$this->seo->canonical($value);
}),
'image' => SingleArraySchema::make()->callback(function (string $value) {
$this->seo->image($value);
}),
/*
* Nested item with key-value pairs.
*
* $data = [
* 'twitter' => [
* 'card' => 'summary',
* 'creator' => '@romanzipp'
* ]
* ];
*/
'twitter' => NestedArraySchema::make()->callback(function (string $attribute, string $value) {
$this->seo->twitter($attribute, $value);
}),
'og' => NestedArraySchema::make()->callback(function (string $attribute, string $value) {
$this->seo->og($attribute, $value);
}),
/*
* Item with attribute schema.
*
* $data = [
* 'meta' => [
* [
* 'name' => 'copyright',
* 'content' => 'Roman Zipp'
* ],
* [
* 'name' => 'theme-color',
* 'content' => 'red'
* ]
* ]
* ];
*/
'meta' => AttributeArraySchema::make(Structs\Meta::class)->callback(function (Structs\Meta $struct, array $attributes) {
$this->seo->add(
$struct->attrs($attributes)
);
}),
'link' => AttributeArraySchema::make(Structs\Link::class)->callback(function (Structs\Link $struct, array $attributes) {
$this->seo->add(
$struct->attrs($attributes)
);
}),
];
}
/**
* Get a array schema based on index.
*
* @param string $index
*
* @return \romanzipp\Seo\Conductors\ArrayStructures\AbstractArraySchema|null
*/
private function getSchema(string $index): ?AbstractArraySchema
{
return $this->getSchemas()[$index] ?? null;
}
/**
* Set the array data and pass it to the seo service.
*
* @param array $data
*/
public function setData(array $data): void
{
foreach ($data as $key => $value) {
$schema = $this->getSchema($key);
if (null === $schema) {
throw new \InvalidArgumentException("Unknown key {$key} provided for seo array format");
}
$schema->apply($value);
}
}
}
================================================
FILE: src/Conductors/ArrayStructures/AbstractArraySchema.php
================================================
class = $class;
}
/**
* Create a new array schema instance.
*
* @param string|null $class
*
* @return static
*/
public static function make(?string $class = null)
{
return new static($class);
}
/**
* Set the callback.
*
* @param \Closure $callback
*
* @return static
*/
public function callback(\Closure $callback)
{
$this->callback = $callback;
return $this;
}
/**
* Get the callback.
*
* @return \Closure
*/
public function getCallback(): \Closure
{
return $this->callback;
}
/**
* Call the callback with given parameters.
*
* @param mixed[] $parameters
*/
protected function call(array $parameters): void
{
call_user_func(
$this->getCallback(),
...$parameters
);
}
/**
* @param mixed $data
*/
abstract public function apply($data): void;
}
================================================
FILE: src/Conductors/ArrayStructures/AttributeArraySchema.php
================================================
> $data
*/
public function apply($data): void
{
if ( ! is_array($data)) {
throw new \InvalidArgumentException('Invalid argument supplied for attribute array schema');
}
foreach ($data as $attributes) {
$this->call([
new $this->class(),
$attributes,
]);
}
}
}
================================================
FILE: src/Conductors/ArrayStructures/NestedArraySchema.php
================================================
$data
*/
public function apply($data): void
{
if ( ! is_array($data)) {
throw new \InvalidArgumentException('Invalid argument supplied for nested array schema');
}
foreach ($data as $attribute => $value) {
$this->call([$attribute, $value]);
}
}
}
================================================
FILE: src/Conductors/ArrayStructures/SingleArraySchema.php
================================================
call([
$value,
]);
}
}
================================================
FILE: src/Conductors/MixManifestConductor.php
================================================
seo = $seo;
$this->path = public_path('mix-manifest.json');
}
/**
* @return string
*/
public function getPath(): string
{
return $this->path;
}
/**
* @return \romanzipp\Seo\Conductors\Types\ManifestAsset[]
*/
public function getAssets(): array
{
return $this->assets;
}
/**
* Add a callback function which will be applied to every asset.
*
* @param \Closure $callback
*
* @return \romanzipp\Seo\Conductors\MixManifestConductor
*/
public function map(\Closure $callback): self
{
$this->mapCallback = $callback;
return $this;
}
/**
* Do not throw exception if the mix manifest is not found.
*
* @return $this
*/
public function ignoreMissing(): self
{
$this->ignoreMissing = true;
return $this;
}
/**
* Do not throw exception if the mix manifest is not found.
*
* @deprecated Use ignoreMissing() instead
*
* @return $this
*/
public function ignore(): self
{
return $this->ignoreMissing();
}
/**
* @param string|null $path
*
* @throws \romanzipp\Seo\Exceptions\ManifestNotFoundException
*
* @return \romanzipp\Seo\Conductors\MixManifestConductor
*/
public function load(?string $path = null): self
{
if (null !== $path) {
$this->path = $path;
}
$this->assets = $this->readContents();
if (null !== $this->mapCallback) {
$this->assets = array_map($this->mapCallback, $this->assets);
}
$this->assets = array_filter($this->assets);
foreach ($this->assets as $asset) {
$this->generateStruct($asset);
}
return $this;
}
/**
* @param \romanzipp\Seo\Conductors\Types\ManifestAsset $asset
*
* @return void
*/
private function generateStruct(ManifestAsset $asset): void
{
$link = Link::make()
->rel($asset->rel)
->href($asset->url);
if (null !== $asset->as) {
$link->as($asset->as);
}
if (null !== $asset->type) {
$link->type($asset->type);
}
$this->seo->add($link);
}
/**
* @throws \romanzipp\Seo\Exceptions\ManifestNotFoundException
*
* @return \romanzipp\Seo\Conductors\Types\ManifestAsset[]
*/
private function readContents(): array
{
$content = @file_get_contents($this->getPath());
if (false === $content) {
if ($this->ignoreMissing) {
return [];
}
throw new ManifestNotFoundException('The manifest file could not be found');
}
$data = @json_decode($content, true) ?? [];
return array_map(static function ($path, $url) {
return new ManifestAsset($path, $url);
}, array_keys($data), $data);
}
}
================================================
FILE: src/Conductors/RenderConductor.php
================================================
structs = $structs;
$this->schemes = $schemes;
}
/**
* Get all structs.
*
* @return \romanzipp\Seo\Structs\Struct[]
*/
public function getStructs(): array
{
return $this->structs;
}
/**
* Get all structs.
*
* @return \Spatie\SchemaOrg\Type[]
*/
public function getSchemes(): array
{
return $this->schemes;
}
/**
* Build all applied structs.
*
* @return \Illuminate\Support\HtmlString
*/
public function build(): HtmlString
{
$contents = $this->toArray();
return new HtmlString(
implode(StructBuilder::$separator, $contents)
);
}
/**
* Get array of rendered html strings.
*
* @return string[]
*/
public function toArray(): array
{
$structs = array_map(static function ($struct) {
return StructBuilder::build($struct)->toHtml();
}, $this->getStructs());
$schemas = array_map(static function (Type $schema) {
return $schema->toScript();
}, $this->getSchemes());
return array_values(
array_merge($structs, $schemas)
);
}
/**
* Get the evaluated contents of the object.
*
* @return string
*/
public function render(): string
{
return (string) $this->build();
}
/**
* Get content as a string of HTML.
*
* @return string
*/
public function toHtml(): string
{
return (string) $this->build();
}
/**
* Get content as a string of HTML.
*
* @return string
*/
public function __toString(): string
{
return (string) $this->build();
}
}
================================================
FILE: src/Conductors/Types/ManifestAsset.php
================================================
path = $path;
$this->url = $url;
$this->as = $this->guessResourceType($path);
}
private function guessResourceType(string $path): ?string
{
$extension = pathinfo($path, PATHINFO_EXTENSION);
if (empty($extension)) {
return null;
}
switch ($extension) {
case 'js':
return 'script';
case 'css':
return 'style';
case 'ttf':
case 'otf':
return 'font';
case 'jpg':
case 'jpeg':
case 'png':
case 'webp':
return 'image';
case 'mp4':
return 'video';
}
return null;
}
}
================================================
FILE: src/Enums/HookTarget.php
================================================
*/
protected $filterAttributes = [];
/**
* Callback to be applied on the target.
*
* @var callable
*/
protected $callback;
/**
* Weather the current hook callback has been executed.
*
* @var bool
*/
protected $executed = false;
/**
* Create new Hook instance.
*
* @return self
*/
public static function make(): self
{
return new self();
}
/*
*--------------------------------------------------------------------------
* Getters
*--------------------------------------------------------------------------
*/
/**
* Get the specified hook target defined in \romanzipp\Seo\Enums\HookTarget.
*
* @return int \romanzipp\Seo\Enums\HookTarget enum value
*/
public function getTarget(): int
{
return $this->target;
}
/**
* Get the specified hook target enum (attribute, attributes, body).
*
* @return mixed
*/
public function getTargetAttribute()
{
return $this->targetAttribute;
}
/**
* Get specified attribute to filter for the hook.
*
* @return array
*/
public function getFilterAttributes(): array
{
return $this->filterAttributes;
}
/**
* Get the callback to be applied.
*
* @return callable
*/
public function getCallback(): callable
{
return $this->callback;
}
/*
*--------------------------------------------------------------------------
* Setters
*--------------------------------------------------------------------------
*/
/**
* Set hook target to body.
*
* @return $this
*/
public function onBody(): self
{
$this->target = HookTarget::BODY;
return $this;
}
/**
* Set hook target on attributes.
*
* @return $this
*/
public function onAttributes(): self
{
$this->target = HookTarget::ATTRIBUTES;
return $this;
}
/**
* Set hook target on specified attribute.
*
* @param string $attribute Struct attribute
*
* @return $this
*/
public function onAttribute(string $attribute): self
{
$this->target = HookTarget::ATTRIBUTE;
$this->targetAttribute = $attribute;
return $this;
}
/**
* Add a hook attribute filter.
*
* @param string $attribute Attribute to search for
* @param mixed $value Attribute value to search for
*
* @return $this
*/
public function whereAttribute(string $attribute, $value): self
{
$this->filterAttributes[$attribute] = $value;
return $this;
}
/**
* Set the callback to be applied.
*
* @param callable $callback Callback
*
* @return $this
*/
public function callback(callable $callback): self
{
$this->callback = $callback;
return $this;
}
/**
* Set executed state.
*
* @param bool $status State
*
* @return \romanzipp\Seo\Helpers\Hook
*/
public function setExecuted(bool $status): self
{
$this->executed = $status;
return $this;
}
/*
*--------------------------------------------------------------------------
* Methods
*--------------------------------------------------------------------------
*/
/**
* Modify the data that will be handed over to the
* hook callback as parameter.
*
* @param mixed $data
*
* @return mixed
*/
public function translateCallbackData($data)
{
switch ($this->target) {
case HookTarget::BODY:
return $data->data();
case HookTarget::ATTRIBUTE:
return array_values($data)[0]->data();
case HookTarget::ATTRIBUTES:
return array_map(static function ($value) {
return $value->data();
}, $data);
}
return null;
}
}
================================================
FILE: src/Providers/SeoServiceProvider.php
================================================
publishes([
dirname(__DIR__) . '/../config/seo.php' => config_path('seo.php'),
], 'config');
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
$this->mergeConfigFrom(
dirname(__DIR__) . '/../config/seo.php',
'seo'
);
$this->app->singleton(StructCollection::class, function (Application $app) {
return new StructCollection();
});
$this->app->singleton(SchemaCollection::class, function (Application $app) {
return new SchemaCollection();
});
$this->app->bind(SeoService::class, function (Application $app) {
return new SeoService(
$app->make(StructCollection::class),
$app->make(SchemaCollection::class)
);
});
}
/**
* Get the services provided by the provider.
*
* @return string[]
*/
public function provides()
{
return [SeoService::class];
}
}
================================================
FILE: src/Schema/Schema.php
================================================
type = $type;
}
/**
* Get the schema type.
*
* @return \Spatie\SchemaOrg\Type
*/
public function getType(): Type
{
return $this->type;
}
/**
* Get the section in which the struct should rest. Default: "default".
*
* @return string
*/
public function getSection(): string
{
return $this->section;
}
/**
* Set the section. This is mainly done in the SeoService class.
*
* @param string $section
*
* @return $this
*/
public function setSection(string $section): self
{
$this->section = $section;
return $this;
}
}
================================================
FILE: src/Services/SeoService.php
================================================
*/
protected $config;
/**
* The section used to add new structs and retrieve existing structs.
* All structs for all sections will be added to the same service instance.
*
* @var string
*/
protected $section = 'default';
/**
* Applied schema.org schemes.
*
* @var \romanzipp\Seo\Collections\SchemaCollection
*/
protected $schemaCollection;
/**
* @var \romanzipp\Seo\Collections\StructCollection
*/
protected $structCollection;
/**
* Constructor.
*
* @param \romanzipp\Seo\Collections\StructCollection $structCollection
* @param \romanzipp\Seo\Collections\SchemaCollection $schemaCollection
*/
public function __construct(StructCollection $structCollection, SchemaCollection $schemaCollection)
{
$this->structCollection = $structCollection;
$this->schemaCollection = $schemaCollection;
$this->config = config('seo');
}
/**
* Create service instance.
*
* @return self
*/
public static function make(): self
{
return app(self::class);
}
/**
* Get config.
*
* @return array
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Fluent section setter.
*
* @param string $section
*
* @return $this
*/
public function section(string $section): self
{
$this->section = $section;
return $this;
}
/**
* Get structs.
*
* @return \romanzipp\Seo\Structs\Struct[]
*/
public function getStructs(): array
{
return array_filter($this->structCollection->all(), function (Struct $struct): bool {
return $struct->getSection() === $this->section;
});
}
/**
* Get Struct by class.
*
* @param string $class
*
* @return \romanzipp\Seo\Structs\Struct|null
*/
public function getStruct(string $class): ?Struct
{
foreach ($this->getStructs() as $struct) {
if (get_class($struct) !== $class) {
continue;
}
return $struct;
}
return null;
}
/**
* Set structs.
*
* @param \romanzipp\Seo\Structs\Struct[] $structCollection
*/
public function setStructCollection(array $structCollection): void
{
$this->clearStructs();
foreach ($structCollection as $struct) {
$this->appendStruct($struct);
}
}
/**
* Remove a struct from the collection by given array index.
*
* @param int $index
*/
public function unsetStruct(int $index): void
{
$this->structCollection->unset($index);
}
/**
* Removes all structs from service instance.
*
* @return void
*/
public function clearStructs(): void
{
$this->structCollection->set([]);
}
/**
* Append a given struct. This is an internal method called by all add/set public methods
* which also sets the current section to the struct.
*
* @param \romanzipp\Seo\Structs\Struct $struct
*/
public function appendStruct(Struct $struct): void
{
$struct->setSection($this->section);
$this->structCollection->add($struct);
}
/**
* Add struct.
*
* @param Struct $struct
*
* @return $this
*/
public function add(Struct $struct): self
{
$this->removeDuplicateStruct($struct);
$this->appendStruct($struct);
return $this;
}
/**
* Add a given Struct if the given condition is true.
*
* @param bool $boolean
* @param Struct $struct
*
* @return $this
*/
public function addIf(bool $boolean, Struct $struct): self
{
if ($boolean) {
$this->add($struct);
}
return $this;
}
/**
* Add many structs.
*
* @param \romanzipp\Seo\Structs\Struct[] $structs
*
* @return $this
*/
public function addMany(array $structs): self
{
foreach ($structs as $struct) {
$this->add($struct);
}
return $this;
}
/**
* Add structs from array format.
*
* @param array $data
*
* @return $this
*/
public function addFromArray(array $data): self
{
$this->arrayFormat()->setData($data);
return $this;
}
/**
* Add hook to given struct class. This is just an
* alias for the Struct::hook() method.
*
* @param string $structClass
* @param \romanzipp\Seo\Helpers\Hook $hook
*
* @return void
*/
public function hook(string $structClass, Hook $hook): void
{
app($structClass)::hook($hook);
}
/**
* @return \romanzipp\Seo\Conductors\MixManifestConductor
*/
public function mix(): MixManifestConductor
{
return new MixManifestConductor($this);
}
/**
* @return \romanzipp\Seo\Conductors\RenderConductor
*/
public function render(): RenderConductor
{
return new RenderConductor(
$this->getStructs(),
$this->getSchemes()
);
}
/**
* @return \romanzipp\Seo\Conductors\ArrayFormatConductor
*/
public function arrayFormat(): ArrayFormatConductor
{
return new ArrayFormatConductor($this);
}
}
================================================
FILE: src/Services/Traits/CollisionTrait.php
================================================
getDuplicateStruct($struct)) {
return;
}
[$existing, $key] = $result;
if (null === $existing || null === $key) {
return;
}
$this->unsetStruct($key);
}
/**
* Get matching struct duplicate.
*
* @param \romanzipp\Seo\Structs\Struct $struct
*
* @return (\romanzipp\Seo\Structs\Struct|int|null)[]|null
*/
public function getDuplicateStruct(Struct $struct): ?array
{
if (false === $struct->isUnique()) {
return null;
}
foreach ($this->getStructs() as $key => $existing) {
/** @var \romanzipp\Seo\Structs\Struct $existing */
if (get_class($existing) !== get_class($struct)) {
continue;
}
if (empty($existing->getUniqueAttributes())) {
return [$existing, $key];
}
$diff = array_diff(
$existing->getComputedUniqueAttributes(),
$struct->getComputedUniqueAttributes()
);
if (empty($diff)) {
return [$existing, $key];
}
}
return null;
}
}
================================================
FILE: src/Services/Traits/SchemaOrgTrait.php
================================================
getType();
},
array_filter(
$this->schemaCollection->all(),
function (SchemaContainer $container): bool {
return $container->getSection() === $this->section;
}
)
)
);
}
/**
* Add spatie/schema-org object.
*
* @param Type $schema schema.org Type
*
* @return $this
*/
public function addSchema(Type $schema): self
{
$container = new SchemaContainer($schema);
$container->setSection($this->section);
$this->schemaCollection->add($container);
return $this;
}
/**
* Set array of spatie/schema-org objects.
*
* @param \Spatie\SchemaOrg\Type[] $schemes
*
* @return $this
*/
public function setSchemes(array $schemes): self
{
$containers = [];
foreach ($schemes as $schema) {
$container = new SchemaContainer($schema);
$container->setSection($this->section);
$containers[] = $container;
}
$this->schemaCollection->set($containers);
return $this;
}
/**
* Add a list of breadcrumbs.
*
* @param array> $crumbs
*
* @return $this
*/
public function addSchemaBreadcrumbs(array $crumbs): self
{
$itemListElement = [];
foreach ($crumbs as $key => $crumb) {
$itemListElement[] = Schema::listItem()
->position($key + 1)
->name(
Arr::get($crumb, 'name')
)
->item(
Arr::get($crumb, 'item')
);
}
$this->addSchema(
Schema::breadcrumbList()
->itemListElement($itemListElement)
);
return $this;
}
}
================================================
FILE: src/Services/Traits/ShorthandSetterTrait.php
================================================
config, 'shorthand.title');
$this->addIf(
$config['tag'] ?? true,
Title::make()->body($title, $escape)
);
$this->addIf(
$config['opengraph'] ?? true,
OpenGraph::make()->property('title')->content($title, $escape)
);
$this->addIf(
$config['twitter'] ?? true,
Twitter::make()->name('title')->content($title, $escape)
);
$this->addIf(
$config['embedx'] ?? true,
EmbedX::make()->name('title')->content($title, $escape)
);
return $this;
}
/**
* Add description.
*
* @param string|null $description
* @param bool $escape
*
* @return $this
*/
public function description(?string $description = null, bool $escape = true): self
{
$config = Arr::get($this->config, 'shorthand.description');
$this->addIf(
$config['meta'] ?? true,
Description::make()->content($description, $escape)
);
$this->addIf(
$config['opengraph'] ?? true,
OpenGraph::make()->property('description')->content($description, $escape)
);
$this->addIf(
$config['twitter'] ?? true,
Twitter::make()->name('description')->content($description, $escape)
);
$this->addIf(
$config['embedx'] ?? true,
EmbedX::make()->name('description')->content($description, $escape)
);
return $this;
}
/**
* Add image.
*
* @param string|null $image
* @param bool $escape
*
* @return $this
*/
public function image(?string $image = null, bool $escape = true): self
{
$config = Arr::get($this->config, 'shorthand.image');
$this->addIf(
$config['meta'] ?? true,
Meta::make()->name('image')->content($image, $escape)
);
$this->addIf(
$config['opengraph'] ?? true,
OpenGraph::make()->property('image')->content($image, $escape)
);
$this->addIf(
$config['twitter'] ?? true,
Twitter::make()->name('image')->content($image, $escape)
);
$this->addIf(
$config['embedx'] ?? true,
EmbedX::make()->name('image')->content($image, $escape)
);
return $this;
}
/**
* Add name-content Meta struct.
*
* @param string $name
* @param mixed|null $content
* @param bool $escape
*
* @return $this
*/
public function meta(string $name, $content = null, bool $escape = true): self
{
return $this->add(
Meta::make()->name($name)->content($content, $escape)
);
}
/**
* Add Twitter struct.
*
* @param string $name
* @param mixed|null $content
* @param bool $escape
*
* @return $this
*/
public function twitter(string $name, $content = null, bool $escape = true): self
{
return $this->add(
Twitter::make()->name($name)->content($content, $escape)
);
}
/**
* Add OpenGraph struct.
*
* @param string $property
* @param mixed|null $content
* @param bool $escape
*
* @return $this
*/
public function og(string $property, $content = null, bool $escape = true): self
{
return $this->add(
OpenGraph::make()->property($property)->content($content, $escape)
);
}
/**
* Add EmbedX struct.
*
* @see https://embedx.app
*
* @param string $property
* @param mixed|null $content
* @param bool $escape
*
* @return $this
*/
public function embedx(string $property, $content = null, bool $escape = true): self
{
return $this->add(
EmbedX::make()->name($property)->content($content, $escape)
);
}
/**
* Add the meta charset struct.
*
* @param string $charset
*
* @return $this
*/
public function charset(string $charset = 'utf-8'): self
{
return $this->add(
Charset::make()->charset($charset)
);
}
/**
* Add the meta viewport struct.
*
* @param string $viewport
*
* @return $this
*/
public function viewport(string $viewport = 'width=device-width, initial-scale=1'): self
{
return $this->add(
Viewport::make()->content($viewport)
);
}
/**
* Add the canonical struct.
*
* @param string $canonical
*
* @return $this
*/
public function canonical(string $canonical): self
{
return $this->add(
Canonical::make()->href($canonical)
);
}
/**
* Add the CSRF token meta struct.
*
* @param string|null $token
*
* @return $this
*/
public function csrfToken(?string $token = null): self
{
return $this->add(
CsrfToken::make()->token($token ?? csrf_token())
);
}
abstract public function add(Struct $struct): SeoService;
abstract public function addIf(bool $boolean, Struct $struct): SeoService;
}
================================================
FILE: src/Structs/Base.php
================================================
addAttribute('rel', 'canonical');
}
}
================================================
FILE: src/Structs/Link.php
================================================
addAttribute('rel', $value, $escape);
return $this;
}
/**
* @param mixed|null $value
* @param bool $escape
*
* @return $this
*/
public function href($value = null, bool $escape = true)
{
$this->addAttribute('href', $value, $escape);
return $this;
}
/**
* @param mixed|null $value
* @param bool $escape
*
* @return $this
*/
public function as($value = null, bool $escape = true)
{
$this->addAttribute('as', $value, $escape);
return $this;
}
/**
* @param mixed|null $value
* @param bool $escape
*
* @return $this
*/
public function type($value = null, bool $escape = true)
{
$this->addAttribute('type', $value, $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/AppLink.php
================================================
addAttribute('property', "al:{$value}", $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/Article.php
================================================
addAttribute('property', "article:{$value}", $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/Charset.php
================================================
addAttribute('charset', 'utf-8');
}
/**
* @param mixed|null $charset
* @param bool $escape
*
* @return $this
*/
public function charset($charset = null, bool $escape = true)
{
$this->addAttribute('charset', $charset, $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/CsrfToken.php
================================================
addAttribute('name', 'csrf-token');
}
/**
* @param mixed|null $token
* @param bool $escape
*
* @return $this
*/
public function token($token = null, bool $escape = true)
{
$this->addAttribute('content', $token, $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/Description.php
================================================
addAttribute('name', 'description');
}
}
================================================
FILE: src/Structs/Meta/EmbedX.php
================================================
addAttribute('name', "embedx:{$value}", $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/OpenGraph.php
================================================
addAttribute('property', "og:{$value}", $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/Robots.php
================================================
addAttribute('name', 'robots');
}
}
================================================
FILE: src/Structs/Meta/Twitter.php
================================================
addAttribute('name', "twitter:{$value}", $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/Viewport.php
================================================
addAttribute('name', 'viewport');
}
}
================================================
FILE: src/Structs/Meta.php
================================================
addAttribute('name', $value, $escape);
return $this;
}
/**
* @param mixed|null $value
* @param bool $escape
*
* @return $this
*/
public function httpEquiv($value = null, bool $escape = true)
{
$this->addAttribute('http-equiv', $value, $escape);
return $this;
}
/**
* @param mixed|null $value
* @param bool $escape
*
* @return $this
*/
public function content($value = null, bool $escape = true)
{
$this->addAttribute('content', $value, $escape);
return $this;
}
/**
* @param mixed $value
* @param bool $escape
*
* @return $this
*/
public function value($value, bool $escape = true)
{
$this->addAttribute('value', $value, $escape);
return $this;
}
}
================================================
FILE: src/Structs/Noscript.php
================================================
addAttribute('src', $value, $escape);
return $this;
}
/**
* @param null $value
* @param bool $escape
*
* @return $this
*/
public function type($value = null, bool $escape = true)
{
$this->addAttribute('type', $value, $escape);
return $this;
}
}
================================================
FILE: src/Structs/Struct.php
================================================
contain more
* than one element of this type.
*
* @var bool
*/
protected $unique = false;
/**
* Attribute names which should be unique across
* all existing elements combined with the struct tag.
*
* @var string[]
*/
protected $uniqueAttributes = [];
/**
* Attributes.
*
* @var array
*/
protected $attributes = [];
/**
* Struct body.
*
* @var \romanzipp\Seo\Values\Body|null
*/
protected $body;
/**
* @var string
*/
protected $section;
/**
* Constructor.
*/
final public function __construct()
{
static::defaults($this);
}
/**
* Create struct instance.
*
* @return static
*/
public static function make()
{
return new static();
}
/**
* Modify struct after creation.
*
* @param self $struct
*/
public static function defaults(self $struct): void
{
}
/*
*--------------------------------------------------------------------------
* Getters
*--------------------------------------------------------------------------
*/
/**
* Get struct tag.
*
* @return string
*/
public function getTag(): string
{
return $this->tag();
}
/**
* Get struct body.
*
* @return \romanzipp\Seo\Values\Body|null
*/
public function getBody(): ?Body
{
return $this->body;
}
/**
* Get struct attributes.
*
* @return array
*/
public function getAttributes(): array
{
return $this->attributes;
}
/**
* Get computed attributes. Converting objects to string values.
*
* @return array
*/
public function getComputedAttributes(): array
{
return $this->getAttributes();
}
/**
* Get computed single attribute.
*
* @param string $attribute
*
* @return \romanzipp\Seo\Values\Attribute|null
*/
public function getComputedAttribute(string $attribute): ?Attribute
{
return $this->getComputedAttributes()[$attribute] ?? null;
}
/**
* Get struct unique attributes for collision detection.
*
* @return string[]
*/
public function getUniqueAttributes(): array
{
return $this->uniqueAttributes;
}
/**
* Get all attributes with values that have been declared as unique.
*
* @return \romanzipp\Seo\Values\Attribute[]
*/
public function getComputedUniqueAttributes(): array
{
return array_filter($this->getAttributes(), function ($value, $key) {
return in_array($key, $this->getUniqueAttributes(), false);
}, ARRAY_FILTER_USE_BOTH);
}
/**
* Is struct unique.
*
* @return bool
*/
public function isUnique(): bool
{
return $this->unique;
}
/**
* Set the unique-flag.
*
* @param bool $unique
*
* @return $this
*/
public function setUnique(bool $unique = true)
{
$this->unique = $unique;
return $this;
}
/**
* Get the section in which the struct should rest. Default: "default".
*
* @return string
*/
public function getSection(): string
{
return $this->section;
}
/**
* Set the section. This is mainly done in the SeoService class.
*
* @param string $section
*
* @return static
*/
public function setSection(string $section)
{
$this->section = $section;
return $this;
}
/**
* Determines if struct is void element.
*
* @see https://www.w3.org/TR/html/syntax.html#void-elements
*
* @return bool
*/
public function isVoidElement(): bool
{
return in_array($this->getTag(), [
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'link',
'meta',
'param',
'source',
'track',
'wbr',
]);
}
/*
*--------------------------------------------------------------------------
* Setters
*--------------------------------------------------------------------------
*/
/**
* Fluid body setter.
*
* @param mixed $body
* @param bool $escape Escape body
*
* @return $this
*/
public function body($body, bool $escape = true)
{
if ($escape) {
$body = $this->escapeValue($body);
}
$this->setBody($body);
return $this;
}
/**
* Fluid attributes setter.
*
* @param string $attribute
* @param mixed|null $value
* @param bool $escape
*
* @return $this
*/
public function attr(string $attribute, $value = null, bool $escape = true)
{
$this->addAttribute($attribute, $value, $escape);
return $this;
}
/**
* Fluid setter for multiple attributes.
*
* @param array $attributes
* @param bool $escape
*
* @return $this
*/
public function attrs(array $attributes, bool $escape = true)
{
foreach ($attributes as $attribute => $value) {
$this->attr($attribute, $value, $escape);
}
return $this;
}
/**
* Set body.
*
* @param mixed $body
*/
protected function setBody($body): void
{
$this->body = new Body($body);
$this->triggerHook(HookTarget::BODY, $this->body);
}
/**
* Add attribute.
*
* @param string $key
* @param mixed $value
* @param bool $escape
*/
protected function addAttribute(string $key, $value, bool $escape = true): void
{
if ($escape) {
$value = $this->escapeValue($value);
}
$this->attributes[$key] = new Attribute($value);
$this->triggerHook(HookTarget::ATTRIBUTE, [$key => $this->attributes[$key]]);
$this->triggerHook(HookTarget::ATTRIBUTES, $this->attributes);
}
/**
* Set attributes.
*
* @param array $attributes
*/
protected function setAttributes(array $attributes): void
{
foreach ($attributes as $key => $value) {
$this->addAttribute($key, $value);
}
}
/**
* Escape attribute value.
*
* @param mixed $value
*
* @return string|null
*/
protected function escapeValue($value): ?string
{
switch (gettype($value)) {
case 'NULL':
return null;
case 'integer':
return (string) $value;
case 'boolean':
return true === $value ? '1' : '0';
}
$value = trim($value);
if ('' === $value) {
return null;
}
return e($value);
}
abstract protected function tag(): string;
}
================================================
FILE: src/Structs/Title.php
================================================
getMatchingHooks($target, $data) as $hook) {
$callback = $hook->getCallback();
// We can pass body or multiple attributes to the user callback without
// worring about data pre-formating.
// In case of single-attribute manipulation we only need to pass the
// attribute value to the callback.
$callbackData = $hook->translateCallbackData($data);
$this->setModifiedHookData(
$hook,
$callback($callbackData)
);
}
}
/**
* Get all matching hooks applied to the struct
* given by a target.
*
* @param int $target
* @param mixed $data
*
* @return \romanzipp\Seo\Helpers\Hook[]
*/
public function getMatchingHooks(int $target, $data): array
{
$hooks = [];
foreach (self::$hooks as $key => $hook) {
// Continue, if applied hook target does not match
// the intendet target.
if ($hook->getTarget() !== $target) {
continue;
}
// Filter by attributes applied to the hook with the
// whereAttribute() method.
$filterAttributes = $hook->getFilterAttributes();
foreach ($filterAttributes as $fAttribute => $fValue) {
if ($this->getComputedAttribute($fAttribute) != $fValue) {
continue 2;
}
}
// Dont't make any more processing if we are targeting the
// Struct body or attributes array.
if (HookTarget::BODY == $target || HookTarget::ATTRIBUTES == $target) {
$hooks[] = $hook;
continue;
}
// $data = ['attribute' => 'value']
$attribute = array_keys($data)[0];
if ($attribute != $hook->getTargetAttribute()) {
continue;
}
$hooks[] = $hook;
}
return $hooks;
}
/**
* Set the modified struct data from hook
* as struct value.
*
* @param \romanzipp\Seo\Helpers\Hook $hook
* @param mixed $data
*
* @return void
*/
public function setModifiedHookData(Hook $hook, $data): void
{
switch ($hook->getTarget()) {
case HookTarget::BODY:
$this->body->setData($data);
break;
case HookTarget::ATTRIBUTES:
$this->setModifiedHookAttributes($data);
break;
case HookTarget::ATTRIBUTE:
$this->attributes[$hook->getTargetAttribute()]->setData($data);
break;
}
}
/**
* Set HookTarget::ATTRIBUTES data from hook callback.
*
* @param mixed $data
*/
protected function setModifiedHookAttributes($data): void
{
$attributes = $this->getAttributes();
foreach ($data as $modifiedAttribute => $modifiedAttributeValue) {
if (array_key_exists($modifiedAttribute, $attributes)) {
$attributes[$modifiedAttribute]->setData($modifiedAttributeValue);
} else {
// Set the attribute directly to avoid triggering
// further Hooks
$this->attributes[$modifiedAttribute] = new Attribute($modifiedAttributeValue);
}
}
}
abstract public function getComputedAttribute(string $attribute);
abstract public function getAttributes(): array;
}
================================================
FILE: src/Values/Attribute.php
================================================
originalData = $data;
}
/**
* Get value data.
*
* @return mixed|null
*/
public function data()
{
if (null !== $this->data) {
return $this->data;
}
return $this->originalData;
}
/**
* Get original value data.
*
* @return mixed|null
*/
public function getOriginalData()
{
return $this->originalData;
}
/**
* Set modified data.
*
* @param mixed $data
*/
public function setData($data): void
{
$this->data = $data;
}
/**
* Get data string representation.
*
* @return string
*/
public function __toString()
{
return (string) $this->data();
}
}
================================================
FILE: src/helpers.php
================================================
section($section);
}
}
================================================
FILE: tests/ArrayFormatTest.php
================================================
addFromArray([
'title' => 'Foo',
]);
$this->assertCount(4, seo()->getStructs());
$this->assertInstanceOf(Title::class, $struct = seo()->getStruct(Title::class));
$this->assertEquals('Foo', (string) $struct->getBody());
$this->assertInstanceOf(Twitter::class, $struct = seo()->getStruct(Twitter::class));
$this->assertEquals('Foo', (string) $struct->getComputedAttribute('content'));
$this->assertInstanceOf(OpenGraph::class, $struct = seo()->getStruct(OpenGraph::class));
$this->assertEquals('Foo', (string) $struct->getComputedAttribute('content'));
}
public function testTitleIndexTagOnly()
{
config([
'seo.shorthand.title.tag' => true,
'seo.shorthand.title.opengraph' => false,
'seo.shorthand.title.twitter' => false,
'seo.shorthand.title.embedx' => false,
]);
seo()->addFromArray([
'title' => 'Foo',
]);
$this->assertCount(1, seo()->getStructs());
$this->assertInstanceOf(Title::class, $struct = seo()->getStruct(Title::class));
$this->assertEquals('Foo', (string) $struct->getBody());
}
// Description
public function testDescriptionIndex()
{
seo()->addFromArray([
'description' => 'Foo',
]);
$this->assertCount(4, seo()->getStructs());
$this->assertInstanceOf(Description::class, $struct = seo()->getStruct(Description::class));
$this->assertEquals('Foo', (string) $struct->getComputedAttribute('content'));
$this->assertInstanceOf(Twitter::class, $struct = seo()->getStruct(Twitter::class));
$this->assertEquals('Foo', (string) $struct->getComputedAttribute('content'));
$this->assertInstanceOf(OpenGraph::class, $struct = seo()->getStruct(OpenGraph::class));
$this->assertEquals('Foo', (string) $struct->getComputedAttribute('content'));
}
public function testDescriptionIndexTagOnly()
{
config([
'seo.shorthand.description.tag' => true,
'seo.shorthand.description.opengraph' => false,
'seo.shorthand.description.twitter' => false,
'seo.shorthand.description.embedx' => false,
]);
seo()->addFromArray([
'description' => 'Foo',
]);
$this->assertCount(1, seo()->getStructs());
$this->assertInstanceOf(Description::class, $struct = seo()->getStruct(Description::class));
$this->assertEquals('Foo', (string) $struct->getComputedAttribute('content'));
}
// Twitter
public function testTwitterIndex()
{
seo()->addFromArray([
'twitter' => [
'card' => 'summary',
],
]);
$this->assertCount(1, seo()->getStructs());
$this->assertInstanceOf(Twitter::class, $struct = seo()->getStruct(Twitter::class));
$this->assertEquals('twitter:card', (string) $struct->getComputedAttribute('name'));
$this->assertEquals('summary', (string) $struct->getComputedAttribute('content'));
}
// OG
public function testOpenGraphIndex()
{
seo()->addFromArray([
'og' => [
'locale' => 'de',
],
]);
$this->assertCount(1, seo()->getStructs());
$this->assertInstanceOf(OpenGraph::class, $struct = seo()->getStruct(OpenGraph::class));
$this->assertEquals('og:locale', (string) $struct->getComputedAttribute('property'));
$this->assertEquals('de', (string) $struct->getComputedAttribute('content'));
}
// Meta
public function testMetaIndex()
{
seo()->addFromArray([
'meta' => [
[
'name' => 'copyright',
'content' => 'Roman Zipp',
],
],
]);
$this->assertCount(1, seo()->getStructs());
$this->assertInstanceOf(Meta::class, $struct = seo()->getStruct(Meta::class));
$this->assertEquals('copyright', (string) $struct->getComputedAttribute('name'));
$this->assertEquals('Roman Zipp', (string) $struct->getComputedAttribute('content'));
}
public function testMetaIndexMultiple()
{
seo()->addFromArray([
'meta' => [
[
'name' => 'copyright',
'content' => 'Roman Zipp',
],
[
'name' => 'theme-color',
'content' => '#3053C6',
],
],
]);
$this->assertCount(2, seo()->getStructs());
$this->assertInstanceOf(Meta::class, $struct = seo()->getStructs()[0]);
$this->assertEquals('copyright', (string) $struct->getComputedAttribute('name'));
$this->assertEquals('Roman Zipp', (string) $struct->getComputedAttribute('content'));
$this->assertInstanceOf(Meta::class, $struct = seo()->getStructs()[1]);
$this->assertEquals('theme-color', (string) $struct->getComputedAttribute('name'));
$this->assertEquals('#3053C6', (string) $struct->getComputedAttribute('content'));
}
// Link
public function testLinkIndex()
{
seo()->addFromArray([
'link' => [
[
'rel' => 'icon',
'href' => '/favicon.ico',
],
],
]);
$this->assertCount(1, seo()->getStructs());
$this->assertInstanceOf(Link::class, $struct = seo()->getStruct(Link::class));
$this->assertEquals('icon', (string) $struct->getComputedAttribute('rel'));
$this->assertEquals('/favicon.ico', (string) $struct->getComputedAttribute('href'));
}
public function testLinkIndexMultiple()
{
seo()->addFromArray([
'link' => [
[
'rel' => 'icon',
'href' => '/favicon.ico',
],
[
'rel' => 'preload',
'href' => '/font.woff2',
],
],
]);
$this->assertCount(2, seo()->getStructs());
$this->assertInstanceOf(Link::class, $struct = seo()->getStructs()[0]);
$this->assertEquals('icon', (string) $struct->getComputedAttribute('rel'));
$this->assertEquals('/favicon.ico', (string) $struct->getComputedAttribute('href'));
$this->assertInstanceOf(Link::class, $struct = seo()->getStructs()[1]);
$this->assertEquals('preload', (string) $struct->getComputedAttribute('rel'));
$this->assertEquals('/font.woff2', (string) $struct->getComputedAttribute('href'));
}
}
================================================
FILE: tests/CollisionTest.php
================================================
add(
Charset::make()
);
seo()->add(
Viewport::make()->content('width=device-width, initial-scale=1')
);
$contents = seo()->render()->toArray();
$this->assertCount(2, $contents);
}
public function testNormalElementCollisions()
{
seo()->add(
Title::make()->body('My Title')
);
seo()->add(
Title::make()->body('My Second Title')
);
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertEquals('My Second Title', $contents[0]);
}
public function testRobotsElementCollisions()
{
seo()->add(
Robots::make()->content('index')
);
seo()->add(
Robots::make()->content('noindex')
);
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertEquals('', $contents[0]);
}
public function testVoidElementSingleAttributeCollisions()
{
seo()->add(
UniqueSingleAttributeStruct::make()
->attr('first', 'unique')
->attr('content', 'My Site Name')
);
seo()->add(
UniqueSingleAttributeStruct::make()
->attr('first', 'unique')
->attr('content', 'My Second Site Name')
);
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertMatchesRegularExpressionCustom('/content\=\"My Second Site Name\"/', $contents[0]);
}
public function testVoidElementSingleOptionalAttributeCollisions()
{
seo()->add(
UniqueSingleAttributeStruct::make()
->attr('first', 'unique')
->attr('content', 'My Site Name')
);
seo()->add(
UniqueSingleAttributeStruct::make()
->attr('second', 'unique')
->attr('content', 'My Second Site Name')
);
$contents = seo()->render()->toArray();
$this->assertCount(2, $contents);
$this->assertMatchesRegularExpressionCustom('/content\=\"My Site Name\"/', $contents[0]);
$this->assertMatchesRegularExpressionCustom('/content\=\"My Second Site Name\"/', $contents[1]);
}
public function testVoidElementMultipleAttributesCollisions()
{
seo()->add(
UniqueMultiAttributeStruct::make()
->attr('first', 'unique')
->attr('second', 'also unique')
->attr('content', 'My Value')
);
seo()->add(
UniqueMultiAttributeStruct::make()
->attr('first', 'unique')
->attr('second', 'also unique')
->attr('content', 'My Second Value')
);
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertMatchesRegularExpressionCustom('/content\=\"My Second Value\"/', $contents[0]);
}
}
================================================
FILE: tests/EscapingTest.php
================================================
alert("malicious");';
seo()->add(
Title::make()->body($malicious)
);
$title = seo()->render()->toArray()[0];
$this->assertEquals('' . e($malicious) . '', $title);
}
public function testAttributeEscaping()
{
$malicious = '';
seo()->add(
Meta::make()->attr('content', $malicious)
);
$meta = seo()->render()->toArray()[0];
$this->assertEquals('', $meta);
}
public function testSkipEscaping()
{
$url = 'http://example.com/something?param1=123¶m2=456';
$expected = '';
seo()->add(
Meta::make()->attr('name', 'url')->attr('content', $url, false)
);
$meta = seo()->render()->toArray()[0];
$this->assertEquals($expected, $meta);
}
public function testShorthandSkipEscaping()
{
$url = 'http://example.com/something?param1=123¶m2=456';
$expected = '';
seo()->twitter('player', $url, false);
$meta = seo()->render()->toArray()[0];
$this->assertEquals($expected, $meta);
}
}
================================================
FILE: tests/HooksTest.php
================================================
onBody()
->callback(function ($body) {
return $body . ' 1';
})
);
seo()->add(
Title::make()->body('My Title')
);
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertEquals('My Title 1', $contents[0]);
Title::clearHooks();
}
public function testMultipleBodyHooks()
{
Title::hook(
Hook::make()
->onBody()
->callback(function ($body) {
return $body . ' 1';
})
);
Title::hook(
Hook::make()
->onBody()
->callback(function ($body) {
return $body . ' 2';
})
);
seo()->add(
Title::make()->body('My Title')
);
$contents = seo()->render()->toArray();
$this->assertEquals('My Title 1 2', $contents[0]);
Title::clearHooks();
}
public function testBodyHookMultipleExecutions()
{
Title::hook(
Hook::make()
->onBody()
->callback(function ($body) {
return $body . ' 1';
})
);
seo()->add(
Title::make()->body('My Title')
);
seo()->add(
Title::make()->body('Some Title')
);
$contents = seo()->render()->toArray();
$this->assertEquals('Some Title 1', $contents[0]);
Title::clearHooks();
}
public function testExistingAttributesHooks()
{
OpenGraph::hook(
Hook::make()
->onAttributes()
->whereAttribute('property', 'og:title')
->callback(function ($attributes) {
return array_merge($attributes, ['content' => 'My Second Title']);
})
);
seo()->add(
OpenGraph::make()->property('title')->content('My Title')
);
$contents = seo()->render()->toArray();
$this->assertMatchesRegularExpressionCustom('/content\=\"My Second Title\"/', $contents[0]);
OpenGraph::clearHooks();
}
public function testAppendingAttributesHooks()
{
OpenGraph::hook(
Hook::make()
->onAttributes()
->whereAttribute('property', 'og:title')
->callback(function ($attributes) {
return array_merge(['should' => 'exist'], $attributes);
})
);
seo()->add(
OpenGraph::make()->property('title')->content('My Title')
);
$contents = seo()->render()->toArray();
$this->assertMatchesRegularExpressionCustom('/should\=\"exist\"/', $contents[0]);
OpenGraph::clearHooks();
}
public function testAttributeHooks()
{
OpenGraph::hook(
Hook::make()
->onAttribute('content')
->whereAttribute('property', 'og:title')
->callback(function ($content) {
return $content . ' 1';
})
);
seo()->add(
OpenGraph::make()->property('title')->content('My Title')
);
$contents = seo()->render()->toArray();
$this->assertMatchesRegularExpressionCustom('/content\=\"My Title 1\"/', $contents[0]);
OpenGraph::clearHooks();
}
public function testEmptyStructTargetHooks()
{
Title::hook(
Hook::make()
->onBody()
->callback(function ($body) {
return $body . ' 1';
})
);
seo()->add(
Title::make()->attr('ignore', 'me')
);
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
Title::clearHooks();
}
}
================================================
FILE: tests/InstantiationTest.php
================================================
assertInstanceOf(SeoService::class, app(SeoService::class));
$this->assertInstanceOf(SeoService::class, Seo::make());
$this->assertInstanceOf(SeoService::class, seo());
}
public function testHookInstance()
{
$this->assertInstanceOf(Hook::class, Hook::make());
}
public function testStructInstance()
{
$this->assertInstanceOf(Meta::class, Meta::make());
}
}
================================================
FILE: tests/MixManifestAssetAttributesTest.php
================================================
assertEquals('script', (new ManifestAsset('/js/app.js', '/js/app.123456.js'))->as);
$this->assertEquals('script', (new ManifestAsset('/js/app.js', '/js/app.js?id=123456'))->as);
}
public function testGuessStyleType()
{
$this->assertEquals('style', (new ManifestAsset('/js/app.css', '/js/app.123456.css'))->as);
$this->assertEquals('style', (new ManifestAsset('/js/app.css', '/js/app.css?id=123456'))->as);
}
public function testGuessFontType()
{
$this->assertEquals('font', (new ManifestAsset('/fonts/Comic-Sans.otf', '/fonts/Comic-Sans.123456.otf'))->as);
$this->assertEquals('font', (new ManifestAsset('/fonts/Comic-Sans.otf', '/fonts/Comic-Sans.otf?id=123456'))->as);
$this->assertEquals('font', (new ManifestAsset('/fonts/Comic-Sans.ttf', '/fonts/Comic-Sans.123456.ttf'))->as);
$this->assertEquals('font', (new ManifestAsset('/fonts/Comic-Sans.ttf', '/fonts/Comic-Sans.ttf?id=123456'))->as);
}
public function testUnsupportedExtension()
{
$this->assertNull((new ManifestAsset('/totally-not-a-virus/app.exe', '/totally-not-a-virus/app.123456.exe'))->as);
$this->assertNull((new ManifestAsset('/totally-not-a-virus/app.exe', '/totally-not-a-virus/app.exe?id=123456'))->as);
}
public function testInvalidExtension()
{
$this->assertNull((new ManifestAsset('/totally-not-a-virus/app', '/totally-not-a-virus/app'))->as);
}
}
================================================
FILE: tests/MixManifestTest.php
================================================
mix();
$this->assertInstanceOf(MixManifestConductor::class, $mix);
}
public function testLoadingOk()
{
$path = $this->path('mix-manifest.json');
$mix = seo()
->mix()
->load($path);
$assets = json_decode(
file_get_contents($path),
true
);
$this->assertEquals([
new ManifestAsset(array_keys($assets)[0], array_values($assets)[0]),
new ManifestAsset(array_keys($assets)[1], array_values($assets)[1]),
], $mix->getAssets());
}
public function testLoadingInvalidPath()
{
$this->expectException(ManifestNotFoundException::class);
$path = $this->path('mix-manifest.not-found.json');
seo()
->mix()
->load($path);
}
public function testLoadInvalidPathIgnoredException()
{
$path = $this->path('mix-manifest.not-found.json');
$mix = seo()
->mix()
->ignore()
->load($path);
$this->assertEquals([], $mix->getAssets());
}
public function testLoadingInvalidJson()
{
$path = $this->path('mix-manifest.empty.json');
$mix = seo()
->mix()
->load($path);
$this->assertEquals([], $mix->getAssets());
}
public function testLoadingEmptyFile()
{
$path = $this->path('mix-manifest.empty.json');
$mix = seo()
->mix()
->load($path);
$this->assertEquals([], $mix->getAssets());
}
public function testDefaultRel()
{
$path = $this->path('mix-manifest.json');
$mix = seo()
->mix()
->load($path);
$this->assertEquals(
['prefetch', 'prefetch'],
[
$mix->getAssets()[0]->rel,
$mix->getAssets()[1]->rel,
]
);
}
public function testMapCallbackNoChanges()
{
$path = $this->path('mix-manifest.json');
$mix = seo()
->mix()
->map(function (ManifestAsset $asset): ?ManifestAsset {
return $asset;
})
->load($path);
$assets = json_decode(
file_get_contents($path),
true
);
$this->assertEquals([
new ManifestAsset(array_keys($assets)[0], array_values($assets)[0]),
new ManifestAsset(array_keys($assets)[1], array_values($assets)[1]),
], $mix->getAssets());
}
public function testMapCallbackRejectAll()
{
$path = $this->path('mix-manifest.json');
$mix = seo()
->mix()
->map(function (ManifestAsset $asset): ?ManifestAsset {
return null;
})
->load($path);
$this->assertCount(0, $mix->getAssets());
}
public function testMapCallbackModifyUrl()
{
$path = $this->path('mix-manifest.json');
$mix = seo()
->mix()
->map(function (ManifestAsset $asset): ?ManifestAsset {
$asset->url = 'http://localhost' . $asset->url;
return $asset;
})
->load($path);
$assets = json_decode(
file_get_contents($path),
true
);
$this->assertEquals(
[
'http://localhost' . array_values($assets)[0],
'http://localhost' . array_values($assets)[1],
],
[
$mix->getAssets()[0]->url,
$mix->getAssets()[1]->url,
]
);
}
public function testMapCallbackModifyPath()
{
$path = $this->path('mix-manifest.json');
$mix = seo()
->mix()
->map(function (ManifestAsset $asset): ?ManifestAsset {
$asset->path = '/somewhere' . $asset->path;
return $asset;
})
->load($path);
$assets = json_decode(
file_get_contents($path),
true
);
$this->assertEquals(
[
'/somewhere' . array_keys($assets)[0],
'/somewhere' . array_keys($assets)[1],
],
[
$mix->getAssets()[0]->path,
$mix->getAssets()[1]->path,
]
);
}
public function testMapCallbackModifyRel()
{
$path = $this->path('mix-manifest.json');
$mix = seo()
->mix()
->map(function (ManifestAsset $asset): ?ManifestAsset {
$asset->rel = 'preload';
return $asset;
})
->load($path);
$this->assertEquals(
['preload', 'preload'],
[
$mix->getAssets()[0]->rel,
$mix->getAssets()[1]->rel,
]
);
}
public function testBasicStructs()
{
$path = __DIR__ . '/Support/mix-manifest.json';
seo()
->mix()
->load($path);
$this->assertCount(2, seo()->getStructs());
$this->assertInstanceOf(Link::class, seo()->getStructs()[0]);
$this->assertInstanceOf(Link::class, seo()->getStructs()[1]);
}
private function path(string $file): string
{
return sprintf('%s/Support/%s', __DIR__, $file);
}
}
================================================
FILE: tests/RenderTest.php
================================================
title('My Title');
$this->assertInstanceOf(HtmlString::class, seo()->render()->build());
}
public function testRenderSingleStruct()
{
$struct = Title::make()->body('My Title');
$this->assertInstanceOf(HtmlString::class, StructBuilder::build($struct));
}
public function testAttributeRenderResult()
{
seo()->add(
Title::make()->attr('attribute', 'value')
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testSpacedAttributeRenderResult()
{
seo()->add(
Title::make()->attr('attribute', 'value ')
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testWrongSpacedAttributeRenderResult()
{
seo()->add(
Title::make()->attr(' attribute ', 'value')
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testBodyRenderResult()
{
seo()->add(
Title::make()->body('My Body')
);
$this->assertEquals('My Body', seo()->render()->toHtml());
}
public function testSpacedBodyRenderResult()
{
seo()->add(
Title::make()->body('My Body ')
);
$this->assertEquals('My Body', seo()->render()->toHtml());
}
public function testNullStringAttributeValue()
{
seo()->add(
Meta::make()->attr('name', '0')
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testZeroIntegerAttributeValue()
{
seo()->add(
Meta::make()->attr('name', 0)
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testEmptyStringAttributeValue()
{
seo()->add(
Meta::make()->attr('name', '')
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testEmptySpaceStringAttributeValue()
{
seo()->add(
Meta::make()->attr('name', ' ')
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testTrueBooleanAttributeValue()
{
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testFalseBooleanAttributeValue()
{
seo()->add(
Meta::make()->attr('name', false)
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testSeparator()
{
StructBuilder::$separator = ' ';
seo()->add(
Meta::make()->attr('name', 'first')
);
seo()->add(
Meta::make()->attr('name', 'second')
);
$this->assertEquals(' ', seo()->render()->toHtml());
}
public function testIndent()
{
StructBuilder::$separator = PHP_EOL;
StructBuilder::$indent = ' ';
seo()->add(
Meta::make()->attr('name', 'first')
);
seo()->add(
Meta::make()->attr('name', 'second')
);
$this->assertEquals(' ' . PHP_EOL . ' ', seo()->render()->toHtml());
}
public function testTagSyntaxHtml5()
{
config(['seo.tag_syntax' => StructBuilder::TAG_SYNTAX_HTML5]);
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testTagSyntaxXhtml()
{
config(['seo.tag_syntax' => StructBuilder::TAG_SYNTAX_XHTML]);
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testTagSyntaxXhtmlStrict()
{
config(['seo.tag_syntax' => StructBuilder::TAG_SYNTAX_XHTML_STRICT]);
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testTagSyntaxUnset()
{
config(['seo.tag_syntax' => null]);
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertEquals('', seo()->render()->toHtml());
}
public function testTagSyntaxUnknown()
{
config(['seo.tag_syntax' => 'invalid']);
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertEquals('', seo()->render()->toHtml());
}
}
================================================
FILE: tests/SchemaOrgTest.php
================================================
addSchema(
Schema::localBusiness()->name('Spatie')
);
$this->assertCount(
1,
seo()->render()->toArray()
);
}
public function testSetter()
{
seo()->addSchema(
Schema::localBusiness()->name('Spatie')
);
seo()->setSchemes([
Schema::airline()->name('Spatie'),
]);
$this->assertCount(
1,
seo()->render()->toArray()
);
}
public function testBasicRender()
{
seo()->addSchema(
Schema::localBusiness()->name('Spatie')
);
$this->assertStringStartsWith(
'