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
================================================
<?php
return romanzipp\Fixer\Config::make()
->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
================================================
<?php
return [
'shorthand' => [
/*
* Decide, which tags should be created when using the
* Seo Service shorthand methods like seo()->title(...)
*/
'title' => [
// <title>...</title>
'tag' => true,
// <meta property="og:title" content="..." />
'opengraph' => true,
// <meta name="twitter:title" content="..." />
'twitter' => true,
// <meta name="embedx:title" content="..." />
'embedx' => true,
],
'description' => [
// <meta name="description" content="..." />
'meta' => true,
// <meta property="og:description" content="..." />
'opengraph' => true,
// <meta name="twitter:description" content="..." />
'twitter' => true,
// <meta name="embedx:description" content="..." />
'embedx' => true,
],
'image' => [
// <meta name="image" content="..." />
'meta' => true,
// <meta property="og:image" content="..." />
'opengraph' => true,
// <meta name="twitter:image" content="..." />
'twitter' => true,
// <meta name="embedx:image" content="..." />
'embedx' => true,
],
],
// Available options:
// - StructBuilder::TAG_SYNTAX_HTML5: <meta name="description">
// - StructBuilder::TAG_SYNTAX_XHTML: <meta name="description" />
// - StructBuilder::TAG_SYNTAX_XHTML_STRICT: <meta name="description"></meta>
'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')` | `<title>Laravel</title>` |
| `seo()->description('Laravel')` | `<meta name="description" content="Laravel" />` |
| `seo()->meta('author', 'Roman Zipp')` | `<meta name="author" content="Roman Zipp" />` |
| `seo()->twitter('card', 'summary')` | `<meta name="twitter:card" content="summary" />` |
| `seo()->og('site_name', 'Laravel')` | `<meta name="og:site_name" content="Laravel" />` |
| `seo()->charset()` | `<meta charset="utf-8" />` |
| `seo()->viewport()` | `<meta name="viewport" content="width=device-width, ..." />` |
| `seo()->csrfToken()` | `<meta name="csrf-token" content="..." />` |
| **Adding Structs** | |
| `seo()->add(...)` | `<meta name="foo" />` |
| `seo()->addMany([...])` | `<meta name="foo" />` |
| `seo()->addIf(true, ...)` | `<meta name="foo" />` |
| **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
<!DOCTYPE html>
<html>
<head>
{{ seo()->render() }}
</head>
<body>
@yield('content')
{{ seo('body')->render() }}
</body>
</html>
```
================================================
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 `<title>` 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')); // <title>Home | Site-Name</title>
seo()->add(Title::make()->body(null)); // <title>Site-Name</title>
```
----
#### 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')); // <meta ... content="Home | Site-Name" />
$seo->add(OpenGraph::make()->property('title')->content(null)); // <meta ... content="Site-Name" />
```
## 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 `<head>`**
```html
<link rel="prefetch" href="/js/app.js?id=123456789" />
<link rel="prefetch" href="/css/app.css?id=123456789" />
```
## 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 `<head>`**
```html
<link rel="prefetch" href="/js/app.js?id=123456789" />
<link rel="prefetch" href="/css/app.css?id=123456789" />
```
## 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 `<head>`**
```html
<link rel="prefetch" href="http://localhost/js/app.js?id=123456789" />
<link rel="prefetch" href="http://localhost/css/app.css?id=123456789" />
```
## 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 `<head>`**
```html
<link rel="prefetch" href="/js/app.js?id=123456789" />
<link rel="prefetch" href="/js/app.routes.js?id=123456789" />
<link rel="preload" href="/js/app.user-component.js?id=123456789" />
<link rel="preload" href="/js/app.news-component.js?id=123456789" />
```
## 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 `<head>`**
```html
<link rel="prefetch" as="style" href="/css/app.css?id=123456789" />
<link rel="prefetch" as="script" href="/js/app.js?id=123456789" />
<link rel="prefetch" as="virus" href="/data/totally-not-a-virus?id=123456789" />
<link rel="prefetch" as="virus" href="/data/totally-not-a-virus?id=123456789" />
```
================================================
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);
```
<details>
<summary>same as ...</summary>
```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),
]);
```
</details>
<details>
<summary>renders to ...</summary>
```html
<title>{title}</title>
<meta property="og:title" content="{title}" />
<meta name="twitter:title" content="{title}" />
<meta name="embedx:title" content="{title}" />
```
</details>
### Description
```php
seo()->description(string $description = null, bool $escape = true);
```
<details>
<summary>same as ...</summary>
```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),
]);
```
</details>
<details>
<summary>renders to ...</summary>
```html
<meta name="description" content="{description}" />
<meta property="og:description" content="{description}" />
<meta name="twitter:description" content="{description}" />
<meta name="embedx:description" content="{description}" />
```
</details>
### Image
```php
seo()->image(string $image = null, bool $escape = true);
```
<details>
<summary>same as ...</summary>
```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),
]);
```
</details>
<details>
<summary>renders to ...</summary>
```html
<meta name="image" content="{image}" />
<meta property="og:image" content="{image}" />
<meta name="twitter:image" content="{image}" />
<meta name="embedx:image" content="{image}" />
```
</details>
### Meta
```php
seo()->meta(string $name, $content = null, bool $escape = true);
```
<details>
<summary>same as ...</summary>
```php
use romanzipp\Seo\Structs\Meta;
seo()->add(
Meta::make()
->name(string $name, bool $escape = true)
->content($content = null, bool $escape = true)
);
```
</details>
<details>
<summary>renders to ...</summary>
```html
<meta name="{name}" content="{content}" />
```
</details>
### OpenGraph
```php
seo()->og(string $property, $content = null, bool $escape = true);
```
<details>
<summary>same as ...</summary>
```php
use romanzipp\Seo\Structs\Meta\OpenGraph;
seo()->add(
OpenGraph::make()
->property(string $property, bool $escape = true)
->content($content = null, bool $escape = true)
);
```
</details>
<details>
<summary>renders to ...</summary>
```html
<meta name="og:{property}" content="{content}" />
```
</details>
### Twitter
```php
seo()->twitter(string $name, $content = null, bool $escape = true);
```
<details>
<summary>same as ...</summary>
```php
use romanzipp\Seo\Structs\Meta\Twitter;
seo()->add(
Twitter::make()
->name(string $name, bool $escape = true)
->content($content = null, bool $escape = true)
);
```
</details>
<details>
<summary>renders to ...</summary>
```html
<meta name="twitter:{name}" content="{content}" />
```
</details>
### [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);
```
<details>
<summary>same as ...</summary>
```php
use romanzipp\Seo\Structs\Meta\EmbedX;
seo()->add(
EmbedX::make()
->name(string $name, bool $escape = true)
->content($content = null, bool $escape = true)
);
```
</details>
<details>
<summary>renders to ...</summary>
```html
<meta name="embedx:{name}" content="{content}" />
```
</details>
### Canonical
```php
seo()->canonical(string $canonical);
```
<details>
<summary>same as ...</summary>
```php
use romanzipp\Seo\Structs\Link\Canonical;
seo()->add(
Canonical::make()
->href($canonical = null)
);
```
</details>
<details>
<summary>renders to ...</summary>
```html
<link rel="canonical" href="{canonical}" />
```
</details>
### CSRF Token
```php
seo()->csrfToken(string $token = null);
```
<details>
<summary>same as ...</summary>
```php
use romanzipp\Seo\Structs\Meta\CsrfToken;
seo()->add(
CsrfToken::make()
->token($token = null)
);
```
</details>
<details>
<summary>renders to ...</summary>
```html
<meta name="csrf-token" content="{token}" />
```
</details>
## 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
<title>This is a Title</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
<meta name="theme-color" content="red" />
```
### OpenGraph
Because **OpenGraph** tags are `<meta />` 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
<meta property="og:site_name" content="Laravel" />
```
### 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
<meta name="twitter:card" content="summary" />
```
## 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 `<meta />` can not have a closing tag other than **normal elements** like `<title></title>`.
### 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 `<head>` can only exist once, like the `<title></title>` 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
<foo name="my-description" content="This is the FIRST description" />
<foo name="my-description" content="This is the SECOND description" />
```
**After**: (`$unique = true`)
```html
<foo name="my-description" content="This is the SECOND description" />
```
### 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
<foo name="my-description" content="This is the FIRST description" />
<foo name="my-description" content="This is the SECOND description" />
<foo name="my-title" content="This is the FIRST title" />
<foo name="my-title" content="This is the SECOND title" />
```
**After**: (`$uniqueAttributes = ['name']`)
```html
<foo name="my-description" content="This is the SECOND description" />
<foo name="my-title" content="This is the SECOND title" />
```
### 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 `<head>`.
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
<title>Laravel</title>
<meta property="og:title" content="Laravel" />
<meta name="twitter:title" content="Laravel" />
```
#### Meta
```php
seo()->meta('copyright', 'Roman Zipp');
```
... renders to ...
```html
<meta name="copyright" content="Roman Zipp" />
```
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' => [
// <meta name="twitter:card" content="summary" />
// <meta name="twitter:creator" content="@romanzipp" />
'card' => 'summary',
'creator' => '@romanzipp',
],
'og' => [
// <meta property="og:locale" content="de" />
// <meta property="og:site_name" content="Laravel" />
'locale' => 'de',
'site_name' => 'Laravel',
],
// Custom meta & link structs. Each child array defines an attribute => value mapping.
'meta' => [
// <meta name="copyright" content="Roman Zipp" />
// <meta name="theme-color" content="#f03a17" />
[
'name' => 'copyright',
'content' => 'Roman Zipp',
],
[
'name' => 'theme-color',
'content' => '#f03a17',
],
],
'link' => [
// <link rel="icon" href="/favicon.ico" />
// <link rel="preload" href="/fonts/IBMPlexSans.woff2" />
[
'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 `<head>` 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
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>My Title</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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
verbose="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Seo Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
</phpunit>
================================================
FILE: src/Builders/StructBuilder.php
================================================
<?php
namespace romanzipp\Seo\Builders;
use Illuminate\Support\HtmlString;
use romanzipp\Seo\Structs\Struct;
class StructBuilder
{
public const TAG_SYNTAX_HTML5 = 'html5';
public const TAG_SYNTAX_XHTML = 'xhtml';
public const TAG_SYNTAX_XHTML_STRICT = 'xhtml_strict';
/**
* Indent rendered struct.
*
* @var string|null
*/
public static $indent;
/**
* Separator for rendered structs.
*
* @var mixed|null
*/
public static $separator = PHP_EOL;
/**
* Struct object.
*
* @var \romanzipp\Seo\Structs\Struct
*/
private $struct;
/**
* Constructor.
*
* @param \romanzipp\Seo\Structs\Struct $struct
*/
public function __construct(Struct $struct)
{
$this->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
================================================
<?php
namespace romanzipp\Seo\Collections\Contracts;
interface CollectionContract
{
}
================================================
FILE: src/Collections/SchemaCollection.php
================================================
<?php
namespace romanzipp\Seo\Collections;
use romanzipp\Seo\Collections\Contracts\CollectionContract;
use romanzipp\Seo\Schema\Schema as SchemaContainer;
class SchemaCollection implements CollectionContract
{
/**
* @var \romanzipp\Seo\Schema\Schema[]
*/
protected $schemas = [];
/**
* @return \romanzipp\Seo\Schema\Schema[]
*/
public function all(): array
{
return $this->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
================================================
<?php
namespace romanzipp\Seo\Collections;
use romanzipp\Seo\Collections\Contracts\CollectionContract;
use romanzipp\Seo\Structs\Struct;
/**
* The Seo class functions as an intermediate layer between the laravel dependency container
* and the SeoService singleton instance.
*
* This intermediate class has been introduced to support the sections feature.
*/
class StructCollection implements CollectionContract
{
/**
* @var \romanzipp\Seo\Structs\Struct[]
*/
protected $structs = [];
/**
* @return \romanzipp\Seo\Structs\Struct[]
*/
public function all(): array
{
return $this->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
================================================
<?php
namespace romanzipp\Seo\Conductors;
use romanzipp\Seo\Conductors\ArrayStructures\AbstractArraySchema;
use romanzipp\Seo\Conductors\ArrayStructures\AttributeArraySchema;
use romanzipp\Seo\Conductors\ArrayStructures\NestedArraySchema;
use romanzipp\Seo\Conductors\ArrayStructures\SingleArraySchema;
use romanzipp\Seo\Services\SeoService;
use romanzipp\Seo\Structs;
class ArrayFormatConductor
{
/**
* @var \romanzipp\Seo\Services\SeoService
*/
private $seo;
public function __construct(SeoService $seo)
{
$this->seo = $seo;
}
/**
* Get the predefined schemas for array formatting.
*
* @return array<string, \romanzipp\Seo\Conductors\ArrayStructures\AbstractArraySchema>
*/
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<string, mixed> $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
================================================
<?php
namespace romanzipp\Seo\Conductors\ArrayStructures;
abstract class AbstractArraySchema
{
/**
* @var string
*/
protected $class;
/**
* @var \Closure
*/
protected $callback;
final public function __construct(?string $class = null)
{
$this->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
================================================
<?php
namespace romanzipp\Seo\Conductors\ArrayStructures;
class AttributeArraySchema extends AbstractArraySchema
{
/**
* @param array<array<string>> $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
================================================
<?php
namespace romanzipp\Seo\Conductors\ArrayStructures;
class NestedArraySchema extends AbstractArraySchema
{
/**
* @param array<string, mixed> $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
================================================
<?php
namespace romanzipp\Seo\Conductors\ArrayStructures;
class SingleArraySchema extends AbstractArraySchema
{
/**
* @param string $value
*/
public function apply($value): void
{
if ( ! is_string($value)) {
throw new \InvalidArgumentException('Invalid argument supplied for single array schema');
}
$this->call([
$value,
]);
}
}
================================================
FILE: src/Conductors/MixManifestConductor.php
================================================
<?php
namespace romanzipp\Seo\Conductors;
use romanzipp\Seo\Conductors\Types\ManifestAsset;
use romanzipp\Seo\Exceptions\ManifestNotFoundException;
use romanzipp\Seo\Services\SeoService;
use romanzipp\Seo\Structs\Link;
class MixManifestConductor
{
/**
* @var \romanzipp\Seo\Services\SeoService
*/
private $seo;
/**
* @var string
*/
private $path;
/**
* @var \romanzipp\Seo\Conductors\Types\ManifestAsset[]
*/
private $assets = [];
/**
* @var \Closure|null
*/
private $mapCallback;
/**
* @var bool
*/
private $ignoreMissing = false;
/**
* MixManifestService constructor.
*/
public function __construct(SeoService $seo)
{
$this->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
================================================
<?php
namespace romanzipp\Seo\Conductors;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Support\HtmlString;
use romanzipp\Seo\Builders\StructBuilder;
use Spatie\SchemaOrg\Type;
class RenderConductor implements Htmlable, Renderable, Arrayable
{
/**
* @var \romanzipp\Seo\Structs\Struct[]
*/
private $structs;
/**
* @var \Spatie\SchemaOrg\Type[]
*/
private $schemes;
/**
* RenderConductor constructor.
*
* @param \romanzipp\Seo\Structs\Struct[] $structs
* @param \Spatie\SchemaOrg\Type[] $schemes
*/
public function __construct(array $structs, array $schemes)
{
$this->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
================================================
<?php
namespace romanzipp\Seo\Conductors\Types;
class ManifestAsset
{
/**
* @var string
*/
public $path;
/**
* @var string
*/
public $url;
/**
* @var string
*/
public $rel = 'prefetch';
/**
* @var string|null
*/
public $as;
/**
* @var mixed
*/
public $type;
public function __construct(string $path, string $url)
{
$this->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
================================================
<?php
namespace romanzipp\Seo\Enums;
class HookTarget
{
public const BODY = 0;
public const ATTRIBUTES = 1;
public const ATTRIBUTE = 2;
}
================================================
FILE: src/Exceptions/ManifestNotFoundException.php
================================================
<?php
namespace romanzipp\Seo\Exceptions;
class ManifestNotFoundException extends \Exception
{
}
================================================
FILE: src/Facades/Seo.php
================================================
<?php
namespace romanzipp\Seo\Facades;
use Illuminate\Support\Facades\Facade;
use romanzipp\Seo\Services\SeoService;
/**
* @method static void macro($name, $macro)
* @method static void mixin($mixin, $replace = true)
* @method static bool hasMacro($name)
*/
class Seo extends Facade
{
protected static function getFacadeAccessor()
{
return SeoService::class;
}
}
================================================
FILE: src/Helpers/Hook.php
================================================
<?php
namespace romanzipp\Seo\Helpers;
use romanzipp\Seo\Enums\HookTarget;
class Hook
{
/**
* Struct attribute to modify, defined in the
* \romanzipp\Seo\Enums\HookTarget enum.
*
* @var int
*/
protected $target;
/**
* If HookTarget::ATTRIBUTE is used as target, this defines
* the attribute to be modified.
*
* @var mixed|null
*/
protected $targetAttribute;
/**
* Filter the structs by certain attributes and values.
*
* @var array<string, mixed>
*/
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<string, mixed>
*/
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
================================================
<?php
namespace romanzipp\Seo\Providers;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use romanzipp\Seo\Collections\SchemaCollection;
use romanzipp\Seo\Collections\StructCollection;
use romanzipp\Seo\Services\SeoService;
class SeoServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
$this->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
================================================
<?php
namespace romanzipp\Seo\Schema;
use romanzipp\Seo\Structs\Struct;
use Spatie\SchemaOrg\Type;
final class Schema
{
/**
* @var \Spatie\SchemaOrg\Type
*/
private $type;
/**
* @var string
*/
private $section;
public function __construct(Type $type)
{
$this->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
================================================
<?php
namespace romanzipp\Seo\Services;
use Illuminate\Support\Traits\Macroable;
use romanzipp\Seo\Collections\SchemaCollection;
use romanzipp\Seo\Collections\StructCollection;
use romanzipp\Seo\Conductors\ArrayFormatConductor;
use romanzipp\Seo\Conductors\MixManifestConductor;
use romanzipp\Seo\Conductors\RenderConductor;
use romanzipp\Seo\Helpers\Hook;
use romanzipp\Seo\Services\Traits\CollisionTrait;
use romanzipp\Seo\Services\Traits\SchemaOrgTrait;
use romanzipp\Seo\Services\Traits\ShorthandSetterTrait;
use romanzipp\Seo\Structs\Struct;
class SeoService
{
use CollisionTrait;
use Macroable;
use SchemaOrgTrait;
use ShorthandSetterTrait;
/**
* Config.
*
* @var array<string, mixed>
*/
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<string, mixed>
*/
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<string, mixed> $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
================================================
<?php
namespace romanzipp\Seo\Services\Traits;
use romanzipp\Seo\Structs\Struct;
trait CollisionTrait
{
abstract public function getStructs(): array;
abstract public function unsetStruct(int $index): void;
/**
* Remove struct from existing structs.
*
* @param \romanzipp\Seo\Structs\Struct $struct
*
* @return void
*/
public function removeDuplicateStruct(Struct $struct): void
{
if ( ! $result = $this->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
================================================
<?php
namespace romanzipp\Seo\Services\Traits;
use Illuminate\Support\Arr;
use romanzipp\Seo\Schema\Schema as SchemaContainer;
use Spatie\SchemaOrg\Schema;
use Spatie\SchemaOrg\Type;
trait SchemaOrgTrait
{
/**
* Get spatie/schema-org types.
*
* @return \Spatie\SchemaOrg\Type[]
*/
public function getSchemes(): array
{
return array_values(
array_map(
static function (SchemaContainer $container): Type {
return $container->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<array<string, string>> $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
================================================
<?php
namespace romanzipp\Seo\Services\Traits;
use Illuminate\Support\Arr;
use romanzipp\Seo\Services\SeoService;
use romanzipp\Seo\Structs\Link\Canonical;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta\Charset;
use romanzipp\Seo\Structs\Meta\CsrfToken;
use romanzipp\Seo\Structs\Meta\Description;
use romanzipp\Seo\Structs\Meta\EmbedX;
use romanzipp\Seo\Structs\Meta\OpenGraph;
use romanzipp\Seo\Structs\Meta\Twitter;
use romanzipp\Seo\Structs\Meta\Viewport;
use romanzipp\Seo\Structs\Struct;
use romanzipp\Seo\Structs\Title;
trait ShorthandSetterTrait
{
/**
* Add title.
*
* @param string|null $title
* @param bool $escape
*
* @return $this
*/
public function title(?string $title = null, bool $escape = true): self
{
$config = Arr::get($this->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
================================================
<?php
namespace romanzipp\Seo\Structs;
/**
* @see https://github.com/joshbuchea/HEAD#elements
*/
class Base extends Struct
{
protected $unique = true;
protected function tag(): string
{
return 'base';
}
}
================================================
FILE: src/Structs/Link/Canonical.php
================================================
<?php
namespace romanzipp\Seo\Structs\Link;
use romanzipp\Seo\Structs\Link;
use romanzipp\Seo\Structs\Struct;
class Canonical extends Link
{
protected $unique = true;
public static function defaults(Struct $struct): void
{
$struct->addAttribute('rel', 'canonical');
}
}
================================================
FILE: src/Structs/Link.php
================================================
<?php
namespace romanzipp\Seo\Structs;
/**
* @see https://github.com/joshbuchea/HEAD#link
*/
class Link extends Struct
{
protected $unique = false;
protected function tag(): string
{
return 'link';
}
/**
* @param mixed|null $value
* @param bool $escape
*
* @return $this
*/
public function rel($value = null, bool $escape = true)
{
$this->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
================================================
<?php
namespace romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta;
/**
* @see https://github.com/joshbuchea/HEAD#app-links
*/
class AppLink extends Meta
{
/**
* @param mixed $value
* @param bool $escape
*
* @return $this
*/
public function property($value, bool $escape = true)
{
$this->addAttribute('property', "al:{$value}", $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/Article.php
================================================
<?php
namespace romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta;
/**
* @see https://github.com/joshbuchea/HEAD#facebook-open-graph
*/
class Article extends Meta
{
protected $unique = true;
protected $uniqueAttributes = ['property'];
/**
* @param mixed|null $value
* @param bool $escape
*
* @return $this
*/
public function property($value = null, bool $escape = true)
{
$this->addAttribute('property', "article:{$value}", $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/Charset.php
================================================
<?php
namespace romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Struct;
/**
* @see https://github.com/joshbuchea/HEAD#recommended-minimum
*/
class Charset extends Meta
{
protected $unique = true;
public static function defaults(Struct $struct): void
{
$struct->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
================================================
<?php
namespace romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Struct;
/**
* @see https://laravel.com/docs/csrf#csrf-x-csrf-token
*/
class CsrfToken extends Meta
{
protected $unique = true;
public static function defaults(Struct $struct): void
{
$struct->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
================================================
<?php
namespace romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Struct;
/**
* @see https://github.com/joshbuchea/HEAD#recommended-minimum
*/
class Description extends Meta
{
protected $unique = true;
public static function defaults(Struct $struct): void
{
$struct->addAttribute('name', 'description');
}
}
================================================
FILE: src/Structs/Meta/EmbedX.php
================================================
<?php
namespace romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta;
/**
* @see https://github.com/joshbuchea/HEAD#twitter-card
* @see https://embedx.app
*/
class EmbedX extends Meta
{
protected $unique = true;
protected $uniqueAttributes = ['name'];
/**
* @param mixed|null $value
* @param bool $escape
*
* @return $this
*/
public function name($value = null, bool $escape = true)
{
$this->addAttribute('name', "embedx:{$value}", $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/OpenGraph.php
================================================
<?php
namespace romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta;
/**
* @see https://github.com/joshbuchea/HEAD#facebook-open-graph
*/
class OpenGraph extends Meta
{
protected $unique = true;
protected $uniqueAttributes = ['property'];
/**
* @param mixed|null $value
* @param bool $escape
*
* @return $this
*/
public function property($value = null, bool $escape = true)
{
$this->addAttribute('property', "og:{$value}", $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/Robots.php
================================================
<?php
namespace romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Struct;
class Robots extends Meta
{
protected $unique = true;
public static function defaults(Struct $struct): void
{
$struct->addAttribute('name', 'robots');
}
}
================================================
FILE: src/Structs/Meta/Twitter.php
================================================
<?php
namespace romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta;
/**
* @see https://github.com/joshbuchea/HEAD#twitter-card
*/
class Twitter extends Meta
{
protected $unique = true;
protected $uniqueAttributes = ['name'];
/**
* @param mixed|null $value
* @param bool $escape
*
* @return $this
*/
public function name($value = null, bool $escape = true)
{
$this->addAttribute('name', "twitter:{$value}", $escape);
return $this;
}
}
================================================
FILE: src/Structs/Meta/Viewport.php
================================================
<?php
namespace romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Struct;
/**
* @see https://github.com/joshbuchea/HEAD#recommended-minimum
*/
class Viewport extends Meta
{
protected $unique = true;
public static function defaults(Struct $struct): void
{
$struct->addAttribute('name', 'viewport');
}
}
================================================
FILE: src/Structs/Meta.php
================================================
<?php
namespace romanzipp\Seo\Structs;
/**
* @see https://github.com/joshbuchea/HEAD#meta
*/
class Meta extends Struct
{
protected function tag(): string
{
return 'meta';
}
/**
* @param mixed|null $value
* @param bool $escape
*
* @return $this
*/
public function name($value = null, bool $escape = true)
{
$this->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
================================================
<?php
namespace romanzipp\Seo\Structs;
/**
* @see https://github.com/joshbuchea/HEAD#elements
*/
class Noscript extends Struct
{
protected function tag(): string
{
return 'noscript';
}
}
================================================
FILE: src/Structs/Script.php
================================================
<?php
namespace romanzipp\Seo\Structs;
/**
* @see https://github.com/joshbuchea/HEAD#elements
*/
class Script extends Struct
{
protected function tag(): string
{
return 'script';
}
/**
* @param null $value
* @param bool $escape
*
* @return $this
*/
public function src($value = null, bool $escape = true)
{
$this->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
================================================
<?php
namespace romanzipp\Seo\Structs;
use romanzipp\Seo\Enums\HookTarget;
use romanzipp\Seo\Structs\Traits\HookableTrait;
use romanzipp\Seo\Values\Attribute;
use romanzipp\Seo\Values\Body;
abstract class Struct
{
use HookableTrait;
/**
* Can the website <head> 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<string, \romanzipp\Seo\Values\Attribute>
*/
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<string, \romanzipp\Seo\Values\Attribute>
*/
public function getAttributes(): array
{
return $this->attributes;
}
/**
* Get computed attributes. Converting objects to string values.
*
* @return array<string, \romanzipp\Seo\Values\Attribute>
*/
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<string, mixed> $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<string, mixed> $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
================================================
<?php
namespace romanzipp\Seo\Structs;
/**
* @see https://github.com/joshbuchea/HEAD#elements
*/
class Title extends Struct
{
protected $unique = true;
protected function tag(): string
{
return 'title';
}
}
================================================
FILE: src/Structs/Traits/HookableTrait.php
================================================
<?php
namespace romanzipp\Seo\Structs\Traits;
use romanzipp\Seo\Enums\HookTarget;
use romanzipp\Seo\Helpers\Hook;
use romanzipp\Seo\Values\Attribute;
trait HookableTrait
{
/**
* Applied hooks.
*
* @var \romanzipp\Seo\Helpers\Hook[]
*/
protected static $hooks = [];
/**
* Add given Hook to the struct.
*
* @param \romanzipp\Seo\Helpers\Hook $hook
*
* @return void
*/
public static function hook(Hook $hook): void
{
self::$hooks[] = $hook;
}
/**
* Remove all hooks.
*
* @return void
*/
public static function clearHooks(): void
{
self::$hooks = [];
}
/**
* Trigger all possible hooks by given target.
* This is getting called if struct values are changed.
*
* @param int $target
* @param mixed $data
*
* @return void
*/
public function triggerHook(int $target, $data): void
{
foreach ($this->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
================================================
<?php
namespace romanzipp\Seo\Values;
class Attribute extends Value
{
}
================================================
FILE: src/Values/Body.php
================================================
<?php
namespace romanzipp\Seo\Values;
class Body extends Value
{
}
================================================
FILE: src/Values/Value.php
================================================
<?php
namespace romanzipp\Seo\Values;
class Value
{
/**
* Value object original data.
*
* @var mixed|null
*/
protected $originalData;
/**
* Value object data.
*
* @var mixed|null
*/
protected $data;
/**
* Constructor.
*
* @param mixed|null $data
*/
public function __construct($data = null)
{
$this->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
================================================
<?php
use romanzipp\Seo\Services\SeoService;
if ( ! function_exists('seo')) {
/**
* Create SeoService instance.
*
* @param string|null $section
*
* @return \romanzipp\Seo\Services\SeoService
*/
function seo(?string $section = null): SeoService
{
if (null === $section) {
return app(SeoService::class);
}
return app(SeoService::class)->section($section);
}
}
================================================
FILE: tests/ArrayFormatTest.php
================================================
<?php
namespace romanzipp\Seo\Test;
use romanzipp\Seo\Structs\Link;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta\Description;
use romanzipp\Seo\Structs\Meta\OpenGraph;
use romanzipp\Seo\Structs\Meta\Twitter;
use romanzipp\Seo\Structs\Title;
class ArrayFormatTest extends TestCase
{
// Title
public function testTitleIndex()
{
seo()->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
================================================
<?php
namespace romanzipp\Seo\Test;
use romanzipp\Seo\Structs\Meta\Charset;
use romanzipp\Seo\Structs\Meta\Robots;
use romanzipp\Seo\Structs\Meta\Viewport;
use romanzipp\Seo\Structs\Title;
use romanzipp\Seo\Test\Structs\UniqueMultiAttributeStruct;
use romanzipp\Seo\Test\Structs\UniqueSingleAttributeStruct;
class CollisionTest extends TestCase
{
public function testShouldNotCollide()
{
seo()->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('<title>My Second Title</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('<meta name="robots" content="noindex" />', $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
================================================
<?php
namespace romanzipp\Seo\Test;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Title;
class EscapingTest extends TestCase
{
public function testBodyEscaping()
{
$malicious = '<script>alert("malicious");</script>';
seo()->add(
Title::make()->body($malicious)
);
$title = seo()->render()->toArray()[0];
$this->assertEquals('<title>' . e($malicious) . '</title>', $title);
}
public function testAttributeEscaping()
{
$malicious = '<script>alert("malicious");</script>';
seo()->add(
Meta::make()->attr('content', $malicious)
);
$meta = seo()->render()->toArray()[0];
$this->assertEquals('<meta content="' . e($malicious) . '" />', $meta);
}
public function testSkipEscaping()
{
$url = 'http://example.com/something?param1=123¶m2=456';
$expected = '<meta name="url" content="' . $url . '" />';
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 = '<meta name="twitter:player" content="' . $url . '" />';
seo()->twitter('player', $url, false);
$meta = seo()->render()->toArray()[0];
$this->assertEquals($expected, $meta);
}
}
================================================
FILE: tests/HooksTest.php
================================================
<?php
namespace romanzipp\Seo\Test;
use romanzipp\Seo\Helpers\Hook;
use romanzipp\Seo\Structs\Meta\OpenGraph;
use romanzipp\Seo\Structs\Title;
class HooksTest extends TestCase
{
public function testBodyHooks()
{
Title::hook(
Hook::make()
->onBody()
->callback(function ($body) {
return $body . ' 1';
})
);
seo()->add(
Title::make()->body('My Title')
);
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertEquals('<title>My Title 1</title>', $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('<title>My Title 1 2</title>', $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('<title>Some Title 1</title>', $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
================================================
<?php
namespace romanzipp\Seo\Test;
use romanzipp\Seo\Facades\Seo;
use romanzipp\Seo\Helpers\Hook;
use romanzipp\Seo\Services\SeoService;
use romanzipp\Seo\Structs\Meta;
class InstantiationTest extends TestCase
{
public function testServiceInstance()
{
$this->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
================================================
<?php
namespace romanzipp\Seo\Test;
use romanzipp\Seo\Conductors\Types\ManifestAsset;
class MixManifestAssetAttributesTest extends TestCase
{
public function testGuessScriptType()
{
$this->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
================================================
<?php
namespace romanzipp\Seo\Test;
use romanzipp\Seo\Conductors\MixManifestConductor;
use romanzipp\Seo\Conductors\Types\ManifestAsset;
use romanzipp\Seo\Exceptions\ManifestNotFoundException;
use romanzipp\Seo\Structs\Link;
class MixManifestTest extends TestCase
{
public function testInstance()
{
$mix = seo()->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
================================================
<?php
namespace romanzipp\Seo\Test;
use Illuminate\Support\HtmlString;
use romanzipp\Seo\Builders\StructBuilder;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Title;
class RenderTest extends TestCase
{
public function testRenderAll()
{
seo()->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('<title attribute="value"></title>', seo()->render()->toHtml());
}
public function testSpacedAttributeRenderResult()
{
seo()->add(
Title::make()->attr('attribute', 'value ')
);
$this->assertEquals('<title attribute="value"></title>', seo()->render()->toHtml());
}
public function testWrongSpacedAttributeRenderResult()
{
seo()->add(
Title::make()->attr(' attribute ', 'value')
);
$this->assertEquals('<title attribute="value"></title>', seo()->render()->toHtml());
}
public function testBodyRenderResult()
{
seo()->add(
Title::make()->body('My Body')
);
$this->assertEquals('<title>My Body</title>', seo()->render()->toHtml());
}
public function testSpacedBodyRenderResult()
{
seo()->add(
Title::make()->body('My Body ')
);
$this->assertEquals('<title>My Body</title>', seo()->render()->toHtml());
}
public function testNullStringAttributeValue()
{
seo()->add(
Meta::make()->attr('name', '0')
);
$this->assertEquals('<meta name="0" />', seo()->render()->toHtml());
}
public function testZeroIntegerAttributeValue()
{
seo()->add(
Meta::make()->attr('name', 0)
);
$this->assertEquals('<meta name="0" />', seo()->render()->toHtml());
}
public function testEmptyStringAttributeValue()
{
seo()->add(
Meta::make()->attr('name', '')
);
$this->assertEquals('<meta name />', seo()->render()->toHtml());
}
public function testEmptySpaceStringAttributeValue()
{
seo()->add(
Meta::make()->attr('name', ' ')
);
$this->assertEquals('<meta name />', seo()->render()->toHtml());
}
public function testTrueBooleanAttributeValue()
{
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertEquals('<meta name="1" />', seo()->render()->toHtml());
}
public function testFalseBooleanAttributeValue()
{
seo()->add(
Meta::make()->attr('name', false)
);
$this->assertEquals('<meta name="0" />', seo()->render()->toHtml());
}
public function testSeparator()
{
StructBuilder::$separator = ' ';
seo()->add(
Meta::make()->attr('name', 'first')
);
seo()->add(
Meta::make()->attr('name', 'second')
);
$this->assertEquals('<meta name="first" /> <meta name="second" />', 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(' <meta name="first" />' . PHP_EOL . ' <meta name="second" />', seo()->render()->toHtml());
}
public function testTagSyntaxHtml5()
{
config(['seo.tag_syntax' => StructBuilder::TAG_SYNTAX_HTML5]);
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertEquals('<meta name="1">', seo()->render()->toHtml());
}
public function testTagSyntaxXhtml()
{
config(['seo.tag_syntax' => StructBuilder::TAG_SYNTAX_XHTML]);
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertEquals('<meta name="1" />', seo()->render()->toHtml());
}
public function testTagSyntaxXhtmlStrict()
{
config(['seo.tag_syntax' => StructBuilder::TAG_SYNTAX_XHTML_STRICT]);
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertEquals('<meta name="1"></meta>', seo()->render()->toHtml());
}
public function testTagSyntaxUnset()
{
config(['seo.tag_syntax' => null]);
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertEquals('<meta name="1" />', seo()->render()->toHtml());
}
public function testTagSyntaxUnknown()
{
config(['seo.tag_syntax' => 'invalid']);
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertEquals('<meta name="1" />', seo()->render()->toHtml());
}
}
================================================
FILE: tests/SchemaOrgTest.php
================================================
<?php
namespace romanzipp\Seo\Test;
use Spatie\SchemaOrg\BreadcrumbList;
use Spatie\SchemaOrg\Schema;
class SchemaOrgTest extends TestCase
{
public function testAppending()
{
seo()->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(
'<script type="application/ld+json">',
seo()->render()->toHtml()
);
}
public function testBreadcrumbs()
{
seo()->addSchemaBreadcrumbs([
['name' => 'First', 'item' => 'https://example.com/first'],
['name' => 'Second', 'item' => 'https://example.com/second'],
]);
$breadcrumbList = seo()->getSchemes()[0];
$this->assertInstanceOf(BreadcrumbList::class, $breadcrumbList);
$itemListElement = $breadcrumbList->getProperty('itemListElement');
$this->assertTrue(
is_array($itemListElement)
);
$this->assertCount(2, $itemListElement);
$this->assertEquals(
1,
$itemListElement[0]->getProperty('position')
);
$this->assertEquals(
'First',
$itemListElement[0]->getProperty('name')
);
}
}
================================================
FILE: tests/SectionsTest.php
================================================
<?php
namespace romanzipp\Seo\Test;
use romanzipp\Seo\Services\SeoService;
use romanzipp\Seo\Structs\Meta;
use Spatie\SchemaOrg\NightClub;
use Spatie\SchemaOrg\PetStore;
class SectionsTest extends TestCase
{
public function testDefaultSection()
{
seo()->twitter('card', 'default');
self::assertSame(
seo()->section('default')->getStructs(),
seo()->getStructs()
);
}
public function testDefaultSectionExplicitlyDeclared()
{
seo()->section('default')->twitter('card', 'default');
self::assertSame(
seo()->section('default')->getStructs(),
seo()->getStructs()
);
}
public function testDefaultSectionUntouched()
{
seo()->section('secondary')->twitter('card', 'default');
self::assertSame(
seo()->section('default')->getStructs(),
seo()->getStructs()
);
}
public function testSectionsDoNotMatch()
{
seo()
->add(Meta::make()->attr('section', 'default'));
seo()
->section('secondary')
->add(Meta::make()->attr('section', 'secondary'));
self::assertCount(1, seo()->getStructs());
self::assertSame('default', (string) seo()->getStruct(Meta::class)->getComputedAttribute('section'));
self::assertCount(1, seo()->section('default')->getStructs());
self::assertSame('default', (string) seo()->section('default')->getStruct(Meta::class)->getComputedAttribute('section'));
self::assertCount(1, seo()->section('secondary')->getStructs());
self::assertSame('secondary', (string) seo()->section('secondary')->getStruct(Meta::class)->getComputedAttribute('section'));
}
public function testSectionPassedAsParameterToHelper()
{
seo('secondary')->twitter('card', 'default');
self::assertSame(
seo()->section('secondary')->getStructs(),
seo('secondary')->getStructs()
);
}
public function testSectionSetterOnMutableInstance()
{
$seo = app(SeoService::class);
$seo->section('secondary');
$seo->twitter('card', 'default');
$seo->twitter('author', 'Roman');
self::assertCount(2, $seo->getStructs());
self::assertSame(
$seo->getStructs(),
seo('secondary')->getStructs()
);
}
public function testSectionRender()
{
seo()->twitter('card', 'default');
seo()->section('secondary')->twitter('card', 'secondary');
self::assertEquals(
'<meta name="twitter:card" content="default" />',
seo()->render()->toHtml()
);
self::assertEquals(
'<meta name="twitter:card" content="secondary" />',
seo()->section('secondary')->render()->toHtml()
);
self::assertEquals(
'<meta name="twitter:card" content="secondary" />',
seo('secondary')->render()->toHtml()
);
}
public function testSchemes()
{
seo()->addSchema(new NightClub());
seo('secondary')->addSchema(new PetStore());
self::assertCount(1, seo()->getSchemes());
self::assertInstanceOf(NightClub::class, seo()->getSchemes()[0]);
self::assertCount(1, seo('secondary')->getSchemes());
self::assertInstanceOf(PetStore::class, seo('secondary')->getSchemes()[0]);
}
}
================================================
FILE: tests/SetterTest.php
================================================
<?php
namespace romanzipp\Seo\Test;
use romanzipp\Seo\Structs\Meta\OpenGraph;
use romanzipp\Seo\Structs\Meta\Twitter;
class SetterTest extends TestCase
{
public function testClear()
{
seo()->twitter('card', 'image');
self::assertCount(1, seo()->getStructs());
seo()->clearStructs();
self::assertCount(0, seo()->getStructs());
}
public function testSetOverride()
{
seo()->twitter('card', 'image');
self::assertCount(1, seo()->getStructs());
seo()->setStructCollection([
OpenGraph::make(),
]);
self::assertCount(1, seo()->getStructs());
self::assertInstanceOf(OpenGraph::class, seo()->getStruct(OpenGraph::class));
}
public function testAdd()
{
seo()->add(Twitter::make());
self::assertCount(1, seo()->getStructs());
seo()->add(OpenGraph::make());
self::assertCount(2, seo()->getStructs());
}
public function testAddIf()
{
seo()->addIf(false, Twitter::make());
self::assertCount(0, seo()->getStructs());
seo()->addIf(true, Twitter::make());
self::assertCount(1, seo()->getStructs());
}
}
================================================
FILE: tests/ShorthandSettersTest.php
================================================
<?php
namespace romanzipp\Seo\Test;
use romanzipp\Seo\Structs\Link\Canonical;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Meta\Charset;
use romanzipp\Seo\Structs\Meta\OpenGraph;
use romanzipp\Seo\Structs\Meta\Twitter;
use romanzipp\Seo\Structs\Meta\Viewport;
class ShorthandSettersTest extends TestCase
{
public function testTitleSingleSetter()
{
config([
'seo.shorthand.title.tag' => true,
'seo.shorthand.title.twitter' => false,
'seo.shorthand.title.opengraph' => false,
'seo.shorthand.title.embedx' => false,
]);
seo()->title('My Title');
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
}
public function testTitleMultipleSetter()
{
config([
'seo.shorthand.title.tag' => true,
'seo.shorthand.title.twitter' => true,
'seo.shorthand.title.opengraph' => true,
'seo.shorthand.title.embedx' => true,
]);
seo()->title('My Title');
$contents = seo()->render()->toArray();
$this->assertCount(4, $contents);
}
public function testDescriptionSingleSetter()
{
config([
'seo.shorthand.description.meta' => true,
'seo.shorthand.description.twitter' => false,
'seo.shorthand.description.opengraph' => false,
'seo.shorthand.description.embedx' => false,
]);
seo()->description('My Description');
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
}
public function testDescriptionMultipleSetter()
{
config([
'seo.shorthand.description.meta' => true,
'seo.shorthand.description.twitter' => true,
'seo.shorthand.description.opengraph' => true,
'seo.shorthand.description.embedx' => true,
]);
seo()->description('My Description');
$contents = seo()->render()->toArray();
$this->assertCount(4, $contents);
}
public function testTwitterSetter()
{
seo()->twitter('card', 'summary');
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertInstanceOf(Twitter::class, seo()->getStructs()[0]);
}
public function testOpenGraphSetter()
{
seo()->og('site_name', 'My Site Name');
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertInstanceOf(OpenGraph::class, seo()->getStructs()[0]);
}
public function testEmbedXSetter()
{
seo()->embedx('title', 'My Site Name');
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertInstanceOf(Meta\EmbedX::class, seo()->getStructs()[0]);
}
public function testMetaSetter()
{
seo()->meta('author', 'My Little Pony');
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertInstanceOf(Meta::class, seo()->getStructs()[0]);
}
public function testCharsetSetter()
{
seo()->charset('utf-8');
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertInstanceOf(Charset::class, seo()->getStructs()[0]);
}
public function testViewportSetter()
{
seo()->viewport('width=device-width, initial-scale=1');
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertInstanceOf(Viewport::class, seo()->getStructs()[0]);
}
public function testCanonicalSetter()
{
seo()->canonical('https://test.com/example');
$contents = seo()->render()->toArray();
$this->assertCount(1, $contents);
$this->assertInstanceOf(Canonical::class, seo()->getStructs()[0]);
}
}
================================================
FILE: tests/Structs/UniqueMultiAttributeStruct.php
================================================
<?php
namespace romanzipp\Seo\Test\Structs;
use romanzipp\Seo\Structs\Struct;
class UniqueMultiAttributeStruct extends Struct
{
protected $unique = true;
protected $uniqueAttributes = ['first', 'second'];
protected function tag(): string
{
return 'unique-multi-attr';
}
}
================================================
FILE: tests/Structs/UniqueSingleAttributeStruct.php
================================================
<?php
namespace romanzipp\Seo\Test\Structs;
use romanzipp\Seo\Structs\Struct;
class UniqueSingleAttributeStruct extends Struct
{
protected $unique = true;
protected $uniqueAttributes = ['first'];
protected function tag(): string
{
return 'unique-single-attr';
}
}
================================================
FILE: tests/Support/mix-manifest.empty.json
================================================
{}
================================================
FILE: tests/Support/mix-manifest.json
================================================
{
"/js/app.js": "/js/app.0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33.js",
"/css/app.css": "/css/app.62cdb7020ff920e5aa642c3d4066950dd1f01f4d.css"
}
================================================
FILE: tests/Support/mix-manifest.null.json
================================================
null
================================================
FILE: tests/Support/mix-manifest.versioned.json
================================================
{
"/js/app.js": "/js/app.js?id=4c8b94c7a94dd6137b79",
"/css/app.css": "/css/app.css?id=35f9f53a2e3a7804169d"
}
================================================
FILE: tests/TestCase.php
================================================
<?php
namespace romanzipp\Seo\Test;
use Orchestra\Testbench\TestCase as BaseTestCase;
use PHPUnit\Framework\Constraint\RegularExpression;
use romanzipp\Seo\Builders\StructBuilder;
use romanzipp\Seo\Facades\Seo;
use romanzipp\Seo\Providers\SeoServiceProvider;
abstract class TestCase extends BaseTestCase
{
public function setUp(): void
{
parent::setUp();
StructBuilder::$separator = PHP_EOL;
StructBuilder::$indent = null;
}
protected function getPackageProviders($app)
{
return [
SeoServiceProvider::class,
];
}
protected function getPackageAliases($app)
{
return [
'Seo' => Seo::class,
];
}
public static function assertMatchesRegularExpressionCustom(string $pattern, string $string, string $message = ''): void
{
// If parent has method assertMatchesRegularExpression, call it
if (method_exists(BaseTestCase::class, 'assertMatchesRegularExpression')) {
parent::assertMatchesRegularExpression($pattern, $string, $message);
return;
}
static::assertThat($string, new RegularExpression($pattern), $message);
}
}
================================================
FILE: tests/ValueTypesTest.php
================================================
<?php
namespace romanzipp\Seo\Test;
use romanzipp\Seo\Helpers\Hook;
use romanzipp\Seo\Structs\Meta;
use romanzipp\Seo\Structs\Title;
class ValueTypesTest extends TestCase
{
public function testBodyNullValue()
{
seo()->add(
Title::make()->body(null)
);
$this->assertNull(seo()->getStructs()[0]->getBody()->data());
}
public function testBodyEmptyStringValue()
{
seo()->add(
Title::make()->body('')
);
$this->assertNull(seo()->getStructs()[0]->getBody()->data());
}
public function testZeroStringAttributeValue()
{
seo()->add(
Meta::make()->attr('name', '0')
);
$this->assertTrue('0' === seo()->getStructs()[0]->getAttributes()['name']->data());
}
// --- legacy
public function testZeroIntegerAttributeValue()
{
seo()->add(
Meta::make()->attr('name', 0)
);
$this->assertTrue('0' === seo()->getStructs()[0]->getAttributes()['name']->data());
}
public function testNullAttributeValue()
{
seo()->add(
Meta::make()->attr('name', null)
);
$this->assertNull(seo()->getStructs()[0]->getAttributes()['name']->data());
}
public function testEmptyStringAttributeValue()
{
seo()->add(
Meta::make()->attr('name', '')
);
$this->assertNull(seo()->getStructs()[0]->getAttributes()['name']->data());
}
public function testEmptySpaceStringAttributeValue()
{
seo()->add(
Meta::make()->attr('name', ' ')
);
$this->assertNull(seo()->getStructs()[0]->getAttributes()['name']->data());
}
public function testTrueBooleanAttributeValue()
{
seo()->add(
Meta::make()->attr('name', true)
);
$this->assertSame('1', seo()->getStructs()[0]->getAttributes()['name']->data());
}
public function testFalseBooleanAttributeValue()
{
seo()->add(
Meta::make()->attr('name', false)
);
$this->assertSame('0', seo()->getStructs()[0]->getAttributes()['name']->data());
}
public function testHookCallbackBodyType()
{
Title::hook(
Hook::make()
->onBody()
->callback(function ($body) {
$this->assertTrue(is_string($body));
return $body;
})
);
seo()->add(
Title::make()->body('My Title')
);
Title::clearHooks();
}
public function testHookCallbackNullableBodyType()
{
Title::hook(
Hook::make()
->onBody()
->callback(function ($body) {
$this->assertNull($body);
return $body;
})
);
seo()->add(
Title::make()->body(null)
);
Title::clearHooks();
}
}
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
SYMBOL INDEX (342 symbols across 61 files)
FILE: src/Builders/StructBuilder.php
class StructBuilder (line 8) | class StructBuilder
method __construct (line 42) | public function __construct(Struct $struct)
method build (line 54) | public static function build(Struct $struct): HtmlString
method render (line 64) | public function render(): HtmlString
method renderAttributes (line 108) | private function renderAttributes(): string
FILE: src/Collections/Contracts/CollectionContract.php
type CollectionContract (line 5) | interface CollectionContract
FILE: src/Collections/SchemaCollection.php
class SchemaCollection (line 8) | class SchemaCollection implements CollectionContract
method all (line 18) | public function all(): array
method add (line 23) | public function add(SchemaContainer $schema): void
method set (line 31) | public function set(array $schemas): void
FILE: src/Collections/StructCollection.php
class StructCollection (line 14) | class StructCollection implements CollectionContract
method all (line 24) | public function all(): array
method add (line 29) | public function add(Struct $struct): void
method set (line 37) | public function set(array $structs): void
method unset (line 42) | public function unset(int $index): void
method remove (line 47) | public function remove(Struct $struct): void
FILE: src/Conductors/ArrayFormatConductor.php
class ArrayFormatConductor (line 12) | class ArrayFormatConductor
method __construct (line 19) | public function __construct(SeoService $seo)
method getSchemas (line 29) | private function getSchemas(): array
method getSchema (line 121) | private function getSchema(string $index): ?AbstractArraySchema
method setData (line 131) | public function setData(array $data): void
FILE: src/Conductors/ArrayStructures/AbstractArraySchema.php
class AbstractArraySchema (line 5) | abstract class AbstractArraySchema
method __construct (line 17) | final public function __construct(?string $class = null)
method make (line 29) | public static function make(?string $class = null)
method callback (line 41) | public function callback(\Closure $callback)
method getCallback (line 53) | public function getCallback(): \Closure
method call (line 63) | protected function call(array $parameters): void
method apply (line 74) | abstract public function apply($data): void;
FILE: src/Conductors/ArrayStructures/AttributeArraySchema.php
class AttributeArraySchema (line 5) | class AttributeArraySchema extends AbstractArraySchema
method apply (line 10) | public function apply($data): void
FILE: src/Conductors/ArrayStructures/NestedArraySchema.php
class NestedArraySchema (line 5) | class NestedArraySchema extends AbstractArraySchema
method apply (line 10) | public function apply($data): void
FILE: src/Conductors/ArrayStructures/SingleArraySchema.php
class SingleArraySchema (line 5) | class SingleArraySchema extends AbstractArraySchema
method apply (line 10) | public function apply($value): void
FILE: src/Conductors/MixManifestConductor.php
class MixManifestConductor (line 10) | class MixManifestConductor
method __construct (line 40) | public function __construct(SeoService $seo)
method getPath (line 49) | public function getPath(): string
method getAssets (line 57) | public function getAssets(): array
method map (line 69) | public function map(\Closure $callback): self
method ignoreMissing (line 81) | public function ignoreMissing(): self
method ignore (line 95) | public function ignore(): self
method load (line 107) | public function load(?string $path = null): self
method generateStruct (line 133) | private function generateStruct(ManifestAsset $asset): void
method readContents (line 155) | private function readContents(): array
FILE: src/Conductors/RenderConductor.php
class RenderConductor (line 12) | class RenderConductor implements Htmlable, Renderable, Arrayable
method __construct (line 30) | public function __construct(array $structs, array $schemes)
method getStructs (line 41) | public function getStructs(): array
method getSchemes (line 51) | public function getSchemes(): array
method build (line 61) | public function build(): HtmlString
method toArray (line 75) | public function toArray(): array
method render (line 95) | public function render(): string
method toHtml (line 105) | public function toHtml(): string
method __toString (line 115) | public function __toString(): string
FILE: src/Conductors/Types/ManifestAsset.php
class ManifestAsset (line 5) | class ManifestAsset
method __construct (line 32) | public function __construct(string $path, string $url)
method guessResourceType (line 39) | private function guessResourceType(string $path): ?string
FILE: src/Enums/HookTarget.php
class HookTarget (line 5) | class HookTarget
FILE: src/Exceptions/ManifestNotFoundException.php
class ManifestNotFoundException (line 5) | class ManifestNotFoundException extends \Exception
FILE: src/Facades/Seo.php
class Seo (line 13) | class Seo extends Facade
method getFacadeAccessor (line 15) | protected static function getFacadeAccessor()
FILE: src/Helpers/Hook.php
class Hook (line 7) | class Hook
method make (line 51) | public static function make(): self
method getTarget (line 67) | public function getTarget(): int
method getTargetAttribute (line 77) | public function getTargetAttribute()
method getFilterAttributes (line 87) | public function getFilterAttributes(): array
method getCallback (line 97) | public function getCallback(): callable
method onBody (line 113) | public function onBody(): self
method onAttributes (line 125) | public function onAttributes(): self
method onAttribute (line 139) | public function onAttribute(string $attribute): self
method whereAttribute (line 156) | public function whereAttribute(string $attribute, $value): self
method callback (line 170) | public function callback(callable $callback): self
method setExecuted (line 184) | public function setExecuted(bool $status): self
method translateCallbackData (line 205) | public function translateCallbackData($data)
FILE: src/Providers/SeoServiceProvider.php
class SeoServiceProvider (line 11) | class SeoServiceProvider extends ServiceProvider
method boot (line 18) | public function boot()
method register (line 30) | public function register()
method provides (line 58) | public function provides()
FILE: src/Schema/Schema.php
class Schema (line 8) | final class Schema
method __construct (line 20) | public function __construct(Type $type)
method getType (line 30) | public function getType(): Type
method getSection (line 40) | public function getSection(): string
method setSection (line 52) | public function setSection(string $section): self
FILE: src/Services/SeoService.php
class SeoService (line 17) | class SeoService
method __construct (line 57) | public function __construct(StructCollection $structCollection, Schema...
method make (line 69) | public static function make(): self
method getConfig (line 79) | public function getConfig(): array
method section (line 91) | public function section(string $section): self
method getStructs (line 103) | public function getStructs(): array
method getStruct (line 117) | public function getStruct(string $class): ?Struct
method setStructCollection (line 135) | public function setStructCollection(array $structCollection): void
method unsetStruct (line 149) | public function unsetStruct(int $index): void
method clearStructs (line 159) | public function clearStructs(): void
method appendStruct (line 170) | public function appendStruct(Struct $struct): void
method add (line 184) | public function add(Struct $struct): self
method addIf (line 201) | public function addIf(bool $boolean, Struct $struct): self
method addMany (line 217) | public function addMany(array $structs): self
method addFromArray (line 233) | public function addFromArray(array $data): self
method hook (line 249) | public function hook(string $structClass, Hook $hook): void
method mix (line 257) | public function mix(): MixManifestConductor
method render (line 265) | public function render(): RenderConductor
method arrayFormat (line 276) | public function arrayFormat(): ArrayFormatConductor
FILE: src/Services/Traits/CollisionTrait.php
type CollisionTrait (line 7) | trait CollisionTrait
method getStructs (line 9) | abstract public function getStructs(): array;
method unsetStruct (line 11) | abstract public function unsetStruct(int $index): void;
method removeDuplicateStruct (line 20) | public function removeDuplicateStruct(Struct $struct): void
method getDuplicateStruct (line 42) | public function getDuplicateStruct(Struct $struct): ?array
FILE: src/Services/Traits/SchemaOrgTrait.php
type SchemaOrgTrait (line 10) | trait SchemaOrgTrait
method getSchemes (line 17) | public function getSchemes(): array
method addSchema (line 41) | public function addSchema(Type $schema): self
method setSchemes (line 58) | public function setSchemes(array $schemes): self
method addSchemaBreadcrumbs (line 81) | public function addSchemaBreadcrumbs(array $crumbs): self
FILE: src/Services/Traits/ShorthandSetterTrait.php
type ShorthandSetterTrait (line 19) | trait ShorthandSetterTrait
method title (line 29) | public function title(?string $title = null, bool $escape = true): self
method description (line 64) | public function description(?string $description = null, bool $escape ...
method image (line 99) | public function image(?string $image = null, bool $escape = true): self
method meta (line 135) | public function meta(string $name, $content = null, bool $escape = tru...
method twitter (line 151) | public function twitter(string $name, $content = null, bool $escape = ...
method og (line 167) | public function og(string $property, $content = null, bool $escape = t...
method embedx (line 185) | public function embedx(string $property, $content = null, bool $escape...
method charset (line 199) | public function charset(string $charset = 'utf-8'): self
method viewport (line 213) | public function viewport(string $viewport = 'width=device-width, initi...
method canonical (line 227) | public function canonical(string $canonical): self
method csrfToken (line 241) | public function csrfToken(?string $token = null): self
method add (line 248) | abstract public function add(Struct $struct): SeoService;
method addIf (line 250) | abstract public function addIf(bool $boolean, Struct $struct): SeoServ...
FILE: src/Structs/Base.php
class Base (line 8) | class Base extends Struct
method tag (line 12) | protected function tag(): string
FILE: src/Structs/Link.php
class Link (line 8) | class Link extends Struct
method tag (line 12) | protected function tag(): string
method rel (line 23) | public function rel($value = null, bool $escape = true)
method href (line 36) | public function href($value = null, bool $escape = true)
method as (line 49) | public function as($value = null, bool $escape = true)
method type (line 62) | public function type($value = null, bool $escape = true)
FILE: src/Structs/Link/Canonical.php
class Canonical (line 8) | class Canonical extends Link
method defaults (line 12) | public static function defaults(Struct $struct): void
FILE: src/Structs/Meta.php
class Meta (line 8) | class Meta extends Struct
method tag (line 10) | protected function tag(): string
method name (line 21) | public function name($value = null, bool $escape = true)
method httpEquiv (line 34) | public function httpEquiv($value = null, bool $escape = true)
method content (line 47) | public function content($value = null, bool $escape = true)
method value (line 60) | public function value($value, bool $escape = true)
FILE: src/Structs/Meta/AppLink.php
class AppLink (line 10) | class AppLink extends Meta
method property (line 18) | public function property($value, bool $escape = true)
FILE: src/Structs/Meta/Article.php
class Article (line 10) | class Article extends Meta
method property (line 22) | public function property($value = null, bool $escape = true)
FILE: src/Structs/Meta/Charset.php
class Charset (line 11) | class Charset extends Meta
method defaults (line 15) | public static function defaults(Struct $struct): void
method charset (line 26) | public function charset($charset = null, bool $escape = true)
FILE: src/Structs/Meta/CsrfToken.php
class CsrfToken (line 11) | class CsrfToken extends Meta
method defaults (line 15) | public static function defaults(Struct $struct): void
method token (line 26) | public function token($token = null, bool $escape = true)
FILE: src/Structs/Meta/Description.php
class Description (line 11) | class Description extends Meta
method defaults (line 15) | public static function defaults(Struct $struct): void
FILE: src/Structs/Meta/EmbedX.php
class EmbedX (line 11) | class EmbedX extends Meta
method name (line 23) | public function name($value = null, bool $escape = true)
FILE: src/Structs/Meta/OpenGraph.php
class OpenGraph (line 10) | class OpenGraph extends Meta
method property (line 22) | public function property($value = null, bool $escape = true)
FILE: src/Structs/Meta/Robots.php
class Robots (line 8) | class Robots extends Meta
method defaults (line 12) | public static function defaults(Struct $struct): void
FILE: src/Structs/Meta/Twitter.php
class Twitter (line 10) | class Twitter extends Meta
method name (line 22) | public function name($value = null, bool $escape = true)
FILE: src/Structs/Meta/Viewport.php
class Viewport (line 11) | class Viewport extends Meta
method defaults (line 15) | public static function defaults(Struct $struct): void
FILE: src/Structs/Noscript.php
class Noscript (line 8) | class Noscript extends Struct
method tag (line 10) | protected function tag(): string
FILE: src/Structs/Script.php
class Script (line 8) | class Script extends Struct
method tag (line 10) | protected function tag(): string
method src (line 21) | public function src($value = null, bool $escape = true)
method type (line 34) | public function type($value = null, bool $escape = true)
FILE: src/Structs/Struct.php
class Struct (line 10) | abstract class Struct
method __construct (line 52) | final public function __construct()
method make (line 62) | public static function make()
method defaults (line 72) | public static function defaults(self $struct): void
method getTag (line 87) | public function getTag(): string
method getBody (line 97) | public function getBody(): ?Body
method getAttributes (line 107) | public function getAttributes(): array
method getComputedAttributes (line 117) | public function getComputedAttributes(): array
method getComputedAttribute (line 129) | public function getComputedAttribute(string $attribute): ?Attribute
method getUniqueAttributes (line 139) | public function getUniqueAttributes(): array
method getComputedUniqueAttributes (line 149) | public function getComputedUniqueAttributes(): array
method isUnique (line 161) | public function isUnique(): bool
method setUnique (line 173) | public function setUnique(bool $unique = true)
method getSection (line 185) | public function getSection(): string
method setSection (line 197) | public function setSection(string $section)
method isVoidElement (line 211) | public function isVoidElement(): bool
method body (line 245) | public function body($body, bool $escape = true)
method attr (line 265) | public function attr(string $attribute, $value = null, bool $escape = ...
method attrs (line 280) | public function attrs(array $attributes, bool $escape = true)
method setBody (line 294) | protected function setBody($body): void
method addAttribute (line 308) | protected function addAttribute(string $key, $value, bool $escape = tr...
method setAttributes (line 326) | protected function setAttributes(array $attributes): void
method escapeValue (line 340) | protected function escapeValue($value): ?string
method tag (line 362) | abstract protected function tag(): string;
FILE: src/Structs/Title.php
class Title (line 8) | class Title extends Struct
method tag (line 12) | protected function tag(): string
FILE: src/Structs/Traits/HookableTrait.php
type HookableTrait (line 9) | trait HookableTrait
method hook (line 25) | public static function hook(Hook $hook): void
method clearHooks (line 35) | public static function clearHooks(): void
method triggerHook (line 49) | public function triggerHook(int $target, $data): void
method getMatchingHooks (line 77) | public function getMatchingHooks(int $target, $data): array
method setModifiedHookData (line 132) | public function setModifiedHookData(Hook $hook, $data): void
method setModifiedHookAttributes (line 157) | protected function setModifiedHookAttributes($data): void
method getComputedAttribute (line 173) | abstract public function getComputedAttribute(string $attribute);
method getAttributes (line 175) | abstract public function getAttributes(): array;
FILE: src/Values/Attribute.php
class Attribute (line 5) | class Attribute extends Value
FILE: src/Values/Body.php
class Body (line 5) | class Body extends Value
FILE: src/Values/Value.php
class Value (line 5) | class Value
method __construct (line 26) | public function __construct($data = null)
method data (line 36) | public function data()
method getOriginalData (line 50) | public function getOriginalData()
method setData (line 60) | public function setData($data): void
method __toString (line 70) | public function __toString()
FILE: src/helpers.php
function seo (line 13) | function seo(?string $section = null): SeoService
FILE: tests/ArrayFormatTest.php
class ArrayFormatTest (line 12) | class ArrayFormatTest extends TestCase
method testTitleIndex (line 16) | public function testTitleIndex()
method testTitleIndexTagOnly (line 34) | public function testTitleIndexTagOnly()
method testDescriptionIndex (line 55) | public function testDescriptionIndex()
method testDescriptionIndexTagOnly (line 73) | public function testDescriptionIndexTagOnly()
method testTwitterIndex (line 94) | public function testTwitterIndex()
method testOpenGraphIndex (line 111) | public function testOpenGraphIndex()
method testMetaIndex (line 128) | public function testMetaIndex()
method testMetaIndexMultiple (line 146) | public function testMetaIndexMultiple()
method testLinkIndex (line 174) | public function testLinkIndex()
method testLinkIndexMultiple (line 192) | public function testLinkIndexMultiple()
FILE: tests/CollisionTest.php
class CollisionTest (line 12) | class CollisionTest extends TestCase
method testShouldNotCollide (line 14) | public function testShouldNotCollide()
method testNormalElementCollisions (line 29) | public function testNormalElementCollisions()
method testRobotsElementCollisions (line 46) | public function testRobotsElementCollisions()
method testVoidElementSingleAttributeCollisions (line 63) | public function testVoidElementSingleAttributeCollisions()
method testVoidElementSingleOptionalAttributeCollisions (line 84) | public function testVoidElementSingleOptionalAttributeCollisions()
method testVoidElementMultipleAttributesCollisions (line 106) | public function testVoidElementMultipleAttributesCollisions()
FILE: tests/EscapingTest.php
class EscapingTest (line 8) | class EscapingTest extends TestCase
method testBodyEscaping (line 10) | public function testBodyEscaping()
method testAttributeEscaping (line 23) | public function testAttributeEscaping()
method testSkipEscaping (line 36) | public function testSkipEscaping()
method testShorthandSkipEscaping (line 51) | public function testShorthandSkipEscaping()
FILE: tests/HooksTest.php
class HooksTest (line 9) | class HooksTest extends TestCase
method testBodyHooks (line 11) | public function testBodyHooks()
method testMultipleBodyHooks (line 34) | public function testMultipleBodyHooks()
method testBodyHookMultipleExecutions (line 63) | public function testBodyHookMultipleExecutions()
method testExistingAttributesHooks (line 88) | public function testExistingAttributesHooks()
method testAppendingAttributesHooks (line 110) | public function testAppendingAttributesHooks()
method testAttributeHooks (line 132) | public function testAttributeHooks()
method testEmptyStructTargetHooks (line 154) | public function testEmptyStructTargetHooks()
FILE: tests/InstantiationTest.php
class InstantiationTest (line 10) | class InstantiationTest extends TestCase
method testServiceInstance (line 12) | public function testServiceInstance()
method testHookInstance (line 21) | public function testHookInstance()
method testStructInstance (line 26) | public function testStructInstance()
FILE: tests/MixManifestAssetAttributesTest.php
class MixManifestAssetAttributesTest (line 7) | class MixManifestAssetAttributesTest extends TestCase
method testGuessScriptType (line 9) | public function testGuessScriptType()
method testGuessStyleType (line 15) | public function testGuessStyleType()
method testGuessFontType (line 21) | public function testGuessFontType()
method testUnsupportedExtension (line 30) | public function testUnsupportedExtension()
method testInvalidExtension (line 36) | public function testInvalidExtension()
FILE: tests/MixManifestTest.php
class MixManifestTest (line 10) | class MixManifestTest extends TestCase
method testInstance (line 12) | public function testInstance()
method testLoadingOk (line 19) | public function testLoadingOk()
method testLoadingInvalidPath (line 38) | public function testLoadingInvalidPath()
method testLoadInvalidPathIgnoredException (line 49) | public function testLoadInvalidPathIgnoredException()
method testLoadingInvalidJson (line 61) | public function testLoadingInvalidJson()
method testLoadingEmptyFile (line 72) | public function testLoadingEmptyFile()
method testDefaultRel (line 83) | public function testDefaultRel()
method testMapCallbackNoChanges (line 100) | public function testMapCallbackNoChanges()
method testMapCallbackRejectAll (line 122) | public function testMapCallbackRejectAll()
method testMapCallbackModifyUrl (line 136) | public function testMapCallbackModifyUrl()
method testMapCallbackModifyPath (line 166) | public function testMapCallbackModifyPath()
method testMapCallbackModifyRel (line 196) | public function testMapCallbackModifyRel()
method testBasicStructs (line 218) | public function testBasicStructs()
method path (line 232) | private function path(string $file): string
FILE: tests/RenderTest.php
class RenderTest (line 10) | class RenderTest extends TestCase
method testRenderAll (line 12) | public function testRenderAll()
method testRenderSingleStruct (line 19) | public function testRenderSingleStruct()
method testAttributeRenderResult (line 26) | public function testAttributeRenderResult()
method testSpacedAttributeRenderResult (line 35) | public function testSpacedAttributeRenderResult()
method testWrongSpacedAttributeRenderResult (line 44) | public function testWrongSpacedAttributeRenderResult()
method testBodyRenderResult (line 53) | public function testBodyRenderResult()
method testSpacedBodyRenderResult (line 62) | public function testSpacedBodyRenderResult()
method testNullStringAttributeValue (line 71) | public function testNullStringAttributeValue()
method testZeroIntegerAttributeValue (line 80) | public function testZeroIntegerAttributeValue()
method testEmptyStringAttributeValue (line 89) | public function testEmptyStringAttributeValue()
method testEmptySpaceStringAttributeValue (line 98) | public function testEmptySpaceStringAttributeValue()
method testTrueBooleanAttributeValue (line 107) | public function testTrueBooleanAttributeValue()
method testFalseBooleanAttributeValue (line 116) | public function testFalseBooleanAttributeValue()
method testSeparator (line 125) | public function testSeparator()
method testIndent (line 140) | public function testIndent()
method testTagSyntaxHtml5 (line 156) | public function testTagSyntaxHtml5()
method testTagSyntaxXhtml (line 167) | public function testTagSyntaxXhtml()
method testTagSyntaxXhtmlStrict (line 178) | public function testTagSyntaxXhtmlStrict()
method testTagSyntaxUnset (line 189) | public function testTagSyntaxUnset()
method testTagSyntaxUnknown (line 200) | public function testTagSyntaxUnknown()
FILE: tests/SchemaOrgTest.php
class SchemaOrgTest (line 8) | class SchemaOrgTest extends TestCase
method testAppending (line 10) | public function testAppending()
method testSetter (line 22) | public function testSetter()
method testBasicRender (line 38) | public function testBasicRender()
method testBreadcrumbs (line 50) | public function testBreadcrumbs()
FILE: tests/SectionsTest.php
class SectionsTest (line 10) | class SectionsTest extends TestCase
method testDefaultSection (line 12) | public function testDefaultSection()
method testDefaultSectionExplicitlyDeclared (line 22) | public function testDefaultSectionExplicitlyDeclared()
method testDefaultSectionUntouched (line 32) | public function testDefaultSectionUntouched()
method testSectionsDoNotMatch (line 42) | public function testSectionsDoNotMatch()
method testSectionPassedAsParameterToHelper (line 61) | public function testSectionPassedAsParameterToHelper()
method testSectionSetterOnMutableInstance (line 71) | public function testSectionSetterOnMutableInstance()
method testSectionRender (line 88) | public function testSectionRender()
method testSchemes (line 110) | public function testSchemes()
FILE: tests/SetterTest.php
class SetterTest (line 8) | class SetterTest extends TestCase
method testClear (line 10) | public function testClear()
method testSetOverride (line 21) | public function testSetOverride()
method testAdd (line 35) | public function testAdd()
method testAddIf (line 46) | public function testAddIf()
FILE: tests/ShorthandSettersTest.php
class ShorthandSettersTest (line 12) | class ShorthandSettersTest extends TestCase
method testTitleSingleSetter (line 14) | public function testTitleSingleSetter()
method testTitleMultipleSetter (line 30) | public function testTitleMultipleSetter()
method testDescriptionSingleSetter (line 46) | public function testDescriptionSingleSetter()
method testDescriptionMultipleSetter (line 62) | public function testDescriptionMultipleSetter()
method testTwitterSetter (line 78) | public function testTwitterSetter()
method testOpenGraphSetter (line 89) | public function testOpenGraphSetter()
method testEmbedXSetter (line 100) | public function testEmbedXSetter()
method testMetaSetter (line 111) | public function testMetaSetter()
method testCharsetSetter (line 122) | public function testCharsetSetter()
method testViewportSetter (line 133) | public function testViewportSetter()
method testCanonicalSetter (line 144) | public function testCanonicalSetter()
FILE: tests/Structs/UniqueMultiAttributeStruct.php
class UniqueMultiAttributeStruct (line 7) | class UniqueMultiAttributeStruct extends Struct
method tag (line 13) | protected function tag(): string
FILE: tests/Structs/UniqueSingleAttributeStruct.php
class UniqueSingleAttributeStruct (line 7) | class UniqueSingleAttributeStruct extends Struct
method tag (line 13) | protected function tag(): string
FILE: tests/TestCase.php
class TestCase (line 11) | abstract class TestCase extends BaseTestCase
method setUp (line 13) | public function setUp(): void
method getPackageProviders (line 21) | protected function getPackageProviders($app)
method getPackageAliases (line 28) | protected function getPackageAliases($app)
method assertMatchesRegularExpressionCustom (line 35) | public static function assertMatchesRegularExpressionCustom(string $pa...
FILE: tests/ValueTypesTest.php
class ValueTypesTest (line 9) | class ValueTypesTest extends TestCase
method testBodyNullValue (line 11) | public function testBodyNullValue()
method testBodyEmptyStringValue (line 20) | public function testBodyEmptyStringValue()
method testZeroStringAttributeValue (line 29) | public function testZeroStringAttributeValue()
method testZeroIntegerAttributeValue (line 40) | public function testZeroIntegerAttributeValue()
method testNullAttributeValue (line 49) | public function testNullAttributeValue()
method testEmptyStringAttributeValue (line 58) | public function testEmptyStringAttributeValue()
method testEmptySpaceStringAttributeValue (line 67) | public function testEmptySpaceStringAttributeValue()
method testTrueBooleanAttributeValue (line 76) | public function testTrueBooleanAttributeValue()
method testFalseBooleanAttributeValue (line 85) | public function testFalseBooleanAttributeValue()
method testHookCallbackBodyType (line 94) | public function testHookCallbackBodyType()
method testHookCallbackNullableBodyType (line 113) | public function testHookCallbackNullableBodyType()
Condensed preview — 90 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (172K chars).
[
{
"path": ".editorconfig",
"chars": 235,
"preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\ntrim_"
},
{
"path": ".github/FUNDING.yml",
"chars": 18,
"preview": "github: romanzipp\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 834,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 595,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".github/workflows/php-cs-fixer.yml",
"chars": 602,
"preview": "name: PHP-CS-Fixer\n\non: [ push ]\n\njobs:\n phpcs:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v2"
},
{
"path": ".github/workflows/phpstan.yml",
"chars": 444,
"preview": "name: PHPStan\n\non: [ push ]\n\njobs:\n phpstan:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v2\n\n "
},
{
"path": ".github/workflows/tests.yml",
"chars": 1103,
"preview": "name: Tests\n\non: [ push, pull_request ]\n\njobs:\n test:\n strategy:\n fail-fast: false\n matrix:\n php: ["
},
{
"path": ".gitignore",
"chars": 490,
"preview": "\n# Created by https://www.gitignore.io/api/composer\n\n### Composer ###\ncomposer.phar\ncomposer.lock\n/vendor/\n\n# Commit you"
},
{
"path": ".php-cs-fixer.dist.php",
"chars": 148,
"preview": "<?php\n\nreturn romanzipp\\Fixer\\Config::make()\n ->in(__DIR__)\n ->preset(\n new romanzipp\\Fixer\\Presets\\PrettyL"
},
{
"path": "LICENSE.md",
"chars": 1071,
"preview": "The MIT License\n\nCopyright (c) 2020 Roman Zipp\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README.md",
"chars": 1190,
"preview": "# Laravel SEO\n\n["
},
{
"path": "composer.json",
"chars": 1341,
"preview": "{\n \"name\": \"romanzipp/laravel-seo\",\n \"description\": \"Laravel SEO package\",\n \"license\": \"MIT\",\n \"type\": \"libr"
},
{
"path": "config/seo.php",
"chars": 1685,
"preview": "<?php\n\nreturn [\n 'shorthand' => [\n /*\n * Decide, which tags should be created when using the\n "
},
{
"path": "deploy-docs.sh",
"chars": 259,
"preview": "#!/usr/bin/env sh\n\n# abort on errors\nset -e\n\n# build\nnpm run docs:build\n\n# navigate into the build output directory\ncd d"
},
{
"path": "docs/.vuepress/config.js",
"chars": 720,
"preview": "module.exports = {\n base: '/Laravel-SEO/',\n title: 'Laravel SEO',\n description: 'SEO package made for maximum c"
},
{
"path": "docs/README.md",
"chars": 1755,
"preview": "# Introduction\n\n## Installation\n\n```\ncomposer require romanzipp/laravel-seo\n```\n\n## Configuration\n\nCopy configuration to"
},
{
"path": "docs/example-app.md",
"chars": 3886,
"preview": "# Example App\n\n## Service Provider\n\n```\n$ php artisan make:provider SeoServiceProvider\n```\n\n#### `Providers/SeoServicePr"
},
{
"path": "docs/hooks.md",
"chars": 3388,
"preview": "# Hooks\n\nHooks allow the modification of a Structs **body** or **attributes**.\n\n### Adding hooks to Structs\n\n```php\nuse "
},
{
"path": "docs/laravel-mix.md",
"chars": 4986,
"preview": "# Laravel-Mix\n\nYou can include your `mix-manifest.json` file generated by [Laravel-Mix](https://laravel-mix.com) to auto"
},
{
"path": "docs/schema-org.md",
"chars": 478,
"preview": "# Schema.org Integration\n\nThis package features a basic integration for [Spaties Schema.org](https://github.com/spatie/s"
},
{
"path": "docs/structs.md",
"chars": 13580,
"preview": "# Structs\n\n**Structs** are a code representation of **HTML head elements**.\n\n## Available Shorthand Methods\n\nShorthand m"
},
{
"path": "docs/usage.md",
"chars": 7043,
"preview": "# Usage\n\n## Instantiation\n\nYou can access the SEO service in many different ways. Just use what you prefer! We will use "
},
{
"path": "package.json",
"chars": 173,
"preview": "{\n \"scripts\": {\n \"docs:dev\": \"vuepress dev docs\",\n \"docs:build\": \"vuepress build docs\"\n },\n \"devD"
},
{
"path": "phpstan.neon.dist",
"chars": 62,
"preview": "parameters:\n phpVersion: 70100\n level: 6\n paths:\n - src\n"
},
{
"path": "phpunit.xml",
"chars": 706,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit bootstrap=\"vendor/autoload.php\"\n backupGlobals=\"false\"\n "
},
{
"path": "src/Builders/StructBuilder.php",
"chars": 2813,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Builders;\n\nuse Illuminate\\Support\\HtmlString;\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass St"
},
{
"path": "src/Collections/Contracts/CollectionContract.php",
"chars": 88,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Collections\\Contracts;\n\ninterface CollectionContract\n{\n}\n"
},
{
"path": "src/Collections/SchemaCollection.php",
"chars": 710,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Collections;\n\nuse romanzipp\\Seo\\Collections\\Contracts\\CollectionContract;\nuse romanzipp\\S"
},
{
"path": "src/Collections/StructCollection.php",
"chars": 1228,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Collections;\n\nuse romanzipp\\Seo\\Collections\\Contracts\\CollectionContract;\nuse romanzipp\\S"
},
{
"path": "src/Conductors/ArrayFormatConductor.php",
"chars": 4395,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Conductors;\n\nuse romanzipp\\Seo\\Conductors\\ArrayStructures\\AbstractArraySchema;\nuse romanz"
},
{
"path": "src/Conductors/ArrayStructures/AbstractArraySchema.php",
"chars": 1307,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Conductors\\ArrayStructures;\n\nabstract class AbstractArraySchema\n{\n /**\n * @var str"
},
{
"path": "src/Conductors/ArrayStructures/AttributeArraySchema.php",
"chars": 535,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Conductors\\ArrayStructures;\n\nclass AttributeArraySchema extends AbstractArraySchema\n{\n "
},
{
"path": "src/Conductors/ArrayStructures/NestedArraySchema.php",
"chars": 478,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Conductors\\ArrayStructures;\n\nclass NestedArraySchema extends AbstractArraySchema\n{\n /*"
},
{
"path": "src/Conductors/ArrayStructures/SingleArraySchema.php",
"chars": 415,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Conductors\\ArrayStructures;\n\nclass SingleArraySchema extends AbstractArraySchema\n{\n /*"
},
{
"path": "src/Conductors/MixManifestConductor.php",
"chars": 3745,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Conductors;\n\nuse romanzipp\\Seo\\Conductors\\Types\\ManifestAsset;\nuse romanzipp\\Seo\\Exceptio"
},
{
"path": "src/Conductors/RenderConductor.php",
"chars": 2510,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Conductors;\n\nuse Illuminate\\Contracts\\Support\\Arrayable;\nuse Illuminate\\Contracts\\Support"
},
{
"path": "src/Conductors/Types/ManifestAsset.php",
"chars": 1183,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Conductors\\Types;\n\nclass ManifestAsset\n{\n /**\n * @var string\n */\n public $p"
},
{
"path": "src/Enums/HookTarget.php",
"chars": 152,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Enums;\n\nclass HookTarget\n{\n public const BODY = 0;\n public const ATTRIBUTES = 1;\n "
},
{
"path": "src/Exceptions/ManifestNotFoundException.php",
"chars": 99,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Exceptions;\n\nclass ManifestNotFoundException extends \\Exception\n{\n}\n"
},
{
"path": "src/Facades/Seo.php",
"chars": 390,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Facades;\n\nuse Illuminate\\Support\\Facades\\Facade;\nuse romanzipp\\Seo\\Services\\SeoService;\n\n"
},
{
"path": "src/Helpers/Hook.php",
"chars": 4654,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Helpers;\n\nuse romanzipp\\Seo\\Enums\\HookTarget;\n\nclass Hook\n{\n /**\n * Struct attribu"
},
{
"path": "src/Providers/SeoServiceProvider.php",
"chars": 1518,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Providers;\n\nuse Illuminate\\Contracts\\Foundation\\Application;\nuse Illuminate\\Support\\Servi"
},
{
"path": "src/Schema/Schema.php",
"chars": 986,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Schema;\n\nuse romanzipp\\Seo\\Structs\\Struct;\nuse Spatie\\SchemaOrg\\Type;\n\nfinal class Schema"
},
{
"path": "src/Services/SeoService.php",
"chars": 6237,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Services;\n\nuse Illuminate\\Support\\Traits\\Macroable;\nuse romanzipp\\Seo\\Collections\\SchemaC"
},
{
"path": "src/Services/Traits/CollisionTrait.php",
"chars": 1657,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Services\\Traits;\n\nuse romanzipp\\Seo\\Structs\\Struct;\n\ntrait CollisionTrait\n{\n abstract "
},
{
"path": "src/Services/Traits/SchemaOrgTrait.php",
"chars": 2433,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Services\\Traits;\n\nuse Illuminate\\Support\\Arr;\nuse romanzipp\\Seo\\Schema\\Schema as SchemaCo"
},
{
"path": "src/Services/Traits/ShorthandSetterTrait.php",
"chars": 6096,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Services\\Traits;\n\nuse Illuminate\\Support\\Arr;\nuse romanzipp\\Seo\\Services\\SeoService;\nuse "
},
{
"path": "src/Structs/Base.php",
"chars": 234,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#elements\n */\nclass Base extends "
},
{
"path": "src/Structs/Link/Canonical.php",
"chars": 298,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Link;\n\nuse romanzipp\\Seo\\Structs\\Link;\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass C"
},
{
"path": "src/Structs/Link.php",
"chars": 1241,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#link\n */\nclass Link extends Stru"
},
{
"path": "src/Structs/Meta/AppLink.php",
"chars": 427,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\n\n/**\n * @see https://github.com/joshbuchea"
},
{
"path": "src/Structs/Meta/Article.php",
"chars": 534,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\n\n/**\n * @see https://github.com/joshbuchea"
},
{
"path": "src/Structs/Meta/Charset.php",
"chars": 633,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Struct;\n\n/**\n * "
},
{
"path": "src/Structs/Meta/CsrfToken.php",
"chars": 623,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Struct;\n\n/**\n * "
},
{
"path": "src/Structs/Meta/Description.php",
"chars": 374,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Struct;\n\n/**\n * "
},
{
"path": "src/Structs/Meta/EmbedX.php",
"chars": 540,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\n\n/**\n * @see https://github.com/joshbuchea"
},
{
"path": "src/Structs/Meta/OpenGraph.php",
"chars": 531,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\n\n/**\n * @see https://github.com/joshbuchea"
},
{
"path": "src/Structs/Meta/Robots.php",
"chars": 293,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass R"
},
{
"path": "src/Structs/Meta/Twitter.php",
"chars": 515,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\n\n/**\n * @see https://github.com/joshbuchea"
},
{
"path": "src/Structs/Meta/Viewport.php",
"chars": 368,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Struct;\n\n/**\n * "
},
{
"path": "src/Structs/Meta.php",
"chars": 1222,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#meta\n */\nclass Meta extends Stru"
},
{
"path": "src/Structs/Noscript.php",
"chars": 211,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#elements\n */\nclass Noscript exte"
},
{
"path": "src/Structs/Script.php",
"chars": 701,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#elements\n */\nclass Script extend"
},
{
"path": "src/Structs/Struct.php",
"chars": 7506,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\nuse romanzipp\\Seo\\Enums\\HookTarget;\nuse romanzipp\\Seo\\Structs\\Traits\\HookableTr"
},
{
"path": "src/Structs/Title.php",
"chars": 236,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#elements\n */\nclass Title extends"
},
{
"path": "src/Structs/Traits/HookableTrait.php",
"chars": 4520,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Traits;\n\nuse romanzipp\\Seo\\Enums\\HookTarget;\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse "
},
{
"path": "src/Values/Attribute.php",
"chars": 74,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Values;\n\nclass Attribute extends Value\n{\n}\n"
},
{
"path": "src/Values/Body.php",
"chars": 69,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Values;\n\nclass Body extends Value\n{\n}\n"
},
{
"path": "src/Values/Value.php",
"chars": 1152,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Values;\n\nclass Value\n{\n /**\n * Value object original data.\n *\n * @var mixe"
},
{
"path": "src/helpers.php",
"chars": 443,
"preview": "<?php\n\nuse romanzipp\\Seo\\Services\\SeoService;\n\nif ( ! function_exists('seo')) {\n /**\n * Create SeoService instanc"
},
{
"path": "tests/ArrayFormatTest.php",
"chars": 7117,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Structs\\Link;\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\"
},
{
"path": "tests/CollisionTest.php",
"chars": 3520,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Structs\\Meta\\Charset;\nuse romanzipp\\Seo\\Structs\\Meta\\Robots;\nuse"
},
{
"path": "tests/EscapingTest.php",
"chars": 1536,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Title;\n\nclass EscapingTe"
},
{
"path": "tests/HooksTest.php",
"chars": 4282,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\nuse roma"
},
{
"path": "tests/InstantiationTest.php",
"chars": 700,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Facades\\Seo;\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse romanzipp\\Seo\\S"
},
{
"path": "tests/MixManifestAssetAttributesTest.php",
"chars": 1668,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Conductors\\Types\\ManifestAsset;\n\nclass MixManifestAssetAttribute"
},
{
"path": "tests/MixManifestTest.php",
"chars": 5741,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Conductors\\MixManifestConductor;\nuse romanzipp\\Seo\\Conductors\\Ty"
},
{
"path": "tests/RenderTest.php",
"chars": 5194,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse Illuminate\\Support\\HtmlString;\nuse romanzipp\\Seo\\Builders\\StructBuilder;\nuse r"
},
{
"path": "tests/SchemaOrgTest.php",
"chars": 1779,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse Spatie\\SchemaOrg\\BreadcrumbList;\nuse Spatie\\SchemaOrg\\Schema;\n\nclass SchemaOrg"
},
{
"path": "tests/SectionsTest.php",
"chars": 3453,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Services\\SeoService;\nuse romanzipp\\Seo\\Structs\\Meta;\nuse Spatie\\"
},
{
"path": "tests/SetterTest.php",
"chars": 1208,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\nuse romanzipp\\Seo\\Structs\\Meta\\Twitter;\n"
},
{
"path": "tests/ShorthandSettersTest.php",
"chars": 3954,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Structs\\Link\\Canonical;\nuse romanzipp\\Seo\\Structs\\Meta;\nuse roma"
},
{
"path": "tests/Structs/UniqueMultiAttributeStruct.php",
"chars": 305,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test\\Structs;\n\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass UniqueMultiAttributeStruct extend"
},
{
"path": "tests/Structs/UniqueSingleAttributeStruct.php",
"chars": 297,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test\\Structs;\n\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass UniqueSingleAttributeStruct exten"
},
{
"path": "tests/Support/mix-manifest.empty.json",
"chars": 3,
"preview": "{}\n"
},
{
"path": "tests/Support/mix-manifest.json",
"chars": 149,
"preview": "{\n \"/js/app.js\": \"/js/app.0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33.js\",\n \"/css/app.css\": \"/css/app.62cdb7020ff920e5aa6"
},
{
"path": "tests/Support/mix-manifest.null.json",
"chars": 5,
"preview": "null\n"
},
{
"path": "tests/Support/mix-manifest.versioned.json",
"chars": 115,
"preview": "{\n \"/js/app.js\": \"/js/app.js?id=4c8b94c7a94dd6137b79\",\n \"/css/app.css\": \"/css/app.css?id=35f9f53a2e3a7804169d\"\n}\n"
},
{
"path": "tests/TestCase.php",
"chars": 1204,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse PHPUnit\\Framework\\Constraint"
},
{
"path": "tests/ValueTypesTest.php",
"chars": 2992,
"preview": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\"
}
]
About this extraction
This page contains the full source code of the romanzipp/Laravel-SEO GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 90 files (154.7 KB), approximately 41.4k tokens, and a symbol index with 342 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.