Full Code of romanzipp/Laravel-SEO for AI

master 27293c1583e5 cached
90 files
154.7 KB
41.4k tokens
342 symbols
1 requests
Download .txt
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

[![Latest Stable Version](https://img.shields.io/packagist/v/romanzipp/Laravel-SEO.svg?style=flat-square)](https://packagist.org/packages/romanzipp/laravel-seo)
[![Total Downloads](https://img.shields.io/packagist/dt/romanzipp/Laravel-SEO.svg?style=flat-square)](https://packagist.org/packages/romanzipp/laravel-seo)
[![License](https://img.shields.io/packagist/l/romanzipp/Laravel-SEO.svg?style=flat-square)](https://packagist.org/packages/romanzipp/laravel-seo)
[![GitHub Build Status](https://img.shields.io/github/actions/workflow/status/romanzipp/Laravel-SEO/tests.yml?branch=master&style=flat-square)](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/)

![](docs/preview.png)

## Testing

```
./vendor/bin/phpunit
```

## License

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

[![Star History Chart](https://api.star-history.com/svg?repos=romanzipp/laravel-seo&type=Date)](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&param2=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&param2=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();
    }
}
Download .txt
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
Download .txt
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[![Latest Stable Version](https://img.shields.io/packagist/v/romanzipp/Laravel-SEO.svg?style=flat-square)"
  },
  {
    "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.

Copied to clipboard!