[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\ntrim_trailing_whitespace = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[*.{yml,yaml,neon,neon.dist}]\nindent_size = 2\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: romanzipp\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/php-cs-fixer.yml",
    "content": "name: PHP-CS-Fixer\n\non: [ push ]\n\njobs:\n  phpcs:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: 7.4\n          coverage: none\n\n      - name: Install dependencies\n        run: composer install --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist\n\n      - name: Execute PHP-CS-Fixer\n        run: vendor/bin/php-cs-fixer fix\n\n      - name: Commit changes\n        uses: stefanzweifel/git-auto-commit-action@v4\n        with:\n          commit_message: Fix styling\n"
  },
  {
    "path": ".github/workflows/phpstan.yml",
    "content": "name: PHPStan\n\non: [ push ]\n\njobs:\n  phpstan:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: 7.4\n          coverage: none\n\n      - name: Install dependencies\n        run: composer install --no-interaction --no-scripts --no-progress --prefer-dist\n\n      - name: Execute PHPStan\n        run: vendor/bin/phpstan analyse\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non: [ push, pull_request ]\n\njobs:\n  test:\n    strategy:\n      fail-fast: false\n      matrix:\n        php: [ \"7.1\", \"7.2\", \"7.3\", \"7.4\", \"8.0\", \"8.1\", \"8.2\", \"8.3\" ]\n        composer-dependency: [ prefer-stable, prefer-lowest ]\n        exclude:\n          - php: \"8.1\"\n            composer-dependency: prefer-lowest\n          - php: \"8.2\"\n            composer-dependency: prefer-lowest\n          - php: \"8.3\"\n            composer-dependency: prefer-lowest\n    name: \"PHP ${{ matrix.php }} - ${{ matrix.composer-dependency }}\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php }}\n          tools: composer:${{ matrix.php == '7.1' && 'v2.2' || 'v2' }}\n          coverage: none\n\n      - name: Install dependencies\n        run: |\n          composer global update\n          composer update --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist --${{ matrix.composer-dependency }}\n\n      - name: Execute tests\n        run: vendor/bin/phpunit\n"
  },
  {
    "path": ".gitignore",
    "content": "\n# Created by https://www.gitignore.io/api/composer\n\n### Composer ###\ncomposer.phar\ncomposer.lock\n/vendor/\n\n# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file\n# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file\n# composer.lock\n\n# End of https://www.gitignore.io/api/composer\n\nbuild/*\n.phpunit.result.cache\n.idea/\n.php_cs.cache\n.php-cs-fixer.cache\n\ndocs/.vuepress/dist\nnode_modules/\n"
  },
  {
    "path": ".php-cs-fixer.dist.php",
    "content": "<?php\n\nreturn romanzipp\\Fixer\\Config::make()\n    ->in(__DIR__)\n    ->preset(\n        new romanzipp\\Fixer\\Presets\\PrettyLaravel()\n    )\n    ->out();\n"
  },
  {
    "path": "LICENSE.md",
    "content": "The MIT License\n\nCopyright (c) 2020 Roman Zipp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Laravel SEO\n\n[![Latest Stable Version](https://img.shields.io/packagist/v/romanzipp/Laravel-SEO.svg?style=flat-square)](https://packagist.org/packages/romanzipp/laravel-seo)\n[![Total Downloads](https://img.shields.io/packagist/dt/romanzipp/Laravel-SEO.svg?style=flat-square)](https://packagist.org/packages/romanzipp/laravel-seo)\n[![License](https://img.shields.io/packagist/l/romanzipp/Laravel-SEO.svg?style=flat-square)](https://packagist.org/packages/romanzipp/laravel-seo)\n[![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)\n\nA SEO package made for maximum customization and flexibility.\n\n## Documentation\n\nThe full package documentation can be found on [romanzipp.github.io/Laravel-SEO](https://romanzipp.github.io/Laravel-SEO/)\n\n![](docs/preview.png)\n\n## Testing\n\n```\n./vendor/bin/phpunit\n```\n\n## License\n\nThe MIT License (MIT). Please see [License File](LICENSE.md) for more information.\n\n[![Star History Chart](https://api.star-history.com/svg?repos=romanzipp/laravel-seo&type=Date)](https://star-history.com/#romanzipp/laravel-seo&Date)\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"romanzipp/laravel-seo\",\n    \"description\": \"Laravel SEO package\",\n    \"license\": \"MIT\",\n    \"type\": \"library\",\n    \"authors\": [\n        {\n            \"name\": \"romanzipp\",\n            \"email\": \"ich@ich.wtf\",\n            \"homepage\": \"https://ich.wtf\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^7.1|^8.0\",\n        \"ext-json\": \"*\",\n        \"illuminate/console\": \">=5.5\",\n        \"illuminate/support\": \">=5.5\",\n        \"spatie/schema-org\": \"^2.1|^3.2\"\n    },\n    \"require-dev\": {\n        \"friendsofphp/php-cs-fixer\": \"^3.0\",\n        \"orchestra/testbench\": \">=3.8\",\n        \"phpstan/phpstan\": \"^0.12.99|^1.0\",\n        \"phpunit/phpunit\": \">=7.0\",\n        \"romanzipp/php-cs-fixer-config\": \"^3.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"romanzipp\\\\Seo\\\\\": \"src\"\n        },\n        \"files\": [\n            \"src/helpers.php\"\n        ]\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"romanzipp\\\\Seo\\\\Test\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"vendor/bin/phpunit\"\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"romanzipp\\\\Seo\\\\Providers\\\\SeoServiceProvider\"\n            ],\n            \"aliases\": {\n                \"Seo\": \"romanzipp\\\\Seo\\\\Facades\\\\Seo\"\n            }\n        }\n    },\n    \"config\": {\n        \"sort-packages\": true\n    }\n}\n"
  },
  {
    "path": "config/seo.php",
    "content": "<?php\n\nreturn [\n    'shorthand' => [\n        /*\n         * Decide, which tags should be created when using the\n         * Seo Service shorthand methods like seo()->title(...)\n         */\n\n        'title' => [\n            // <title>...</title>\n            'tag' => true,\n\n            // <meta property=\"og:title\" content=\"...\" />\n            'opengraph' => true,\n\n            // <meta name=\"twitter:title\" content=\"...\" />\n            'twitter' => true,\n\n            // <meta name=\"embedx:title\" content=\"...\" />\n            'embedx' => true,\n        ],\n\n        'description' => [\n            // <meta name=\"description\" content=\"...\" />\n            'meta' => true,\n\n            // <meta property=\"og:description\" content=\"...\" />\n            'opengraph' => true,\n\n            // <meta name=\"twitter:description\" content=\"...\" />\n            'twitter' => true,\n\n            // <meta name=\"embedx:description\" content=\"...\" />\n            'embedx' => true,\n        ],\n\n        'image' => [\n            // <meta name=\"image\" content=\"...\" />\n            'meta' => true,\n\n            // <meta property=\"og:image\" content=\"...\" />\n            'opengraph' => true,\n\n            // <meta name=\"twitter:image\" content=\"...\" />\n            'twitter' => true,\n\n            // <meta name=\"embedx:image\" content=\"...\" />\n            'embedx' => true,\n        ],\n    ],\n\n    // Available options:\n    // - StructBuilder::TAG_SYNTAX_HTML5: <meta name=\"description\">\n    // - StructBuilder::TAG_SYNTAX_XHTML: <meta name=\"description\" />\n    // - StructBuilder::TAG_SYNTAX_XHTML_STRICT: <meta name=\"description\"></meta>\n    'tag_syntax' => \\romanzipp\\Seo\\Builders\\StructBuilder::TAG_SYNTAX_XHTML,\n];\n"
  },
  {
    "path": "deploy-docs.sh",
    "content": "#!/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 docs/.vuepress/dist\n\ngit init\ngit add -A\ngit commit -m 'deploy'\n\ngit push -f git@github.com:romanzipp/Laravel-SEO.git master:gh-pages\n\ncd -\n"
  },
  {
    "path": "docs/.vuepress/config.js",
    "content": "module.exports = {\n    base: '/Laravel-SEO/',\n    title: 'Laravel SEO',\n    description: 'SEO package made for maximum customization and flexibility ',\n    host: 'localhost',\n    port: 3001,\n    themeConfig: {\n        nav: [\n            { text: 'Home', link: '/' },\n            { text: 'GitHub', link: 'https://github.com/romanzipp/Laravel-SEO' },\n            { text: 'Packagist', link: 'https://packagist.org/packages/romanzipp/laravel-seo' },\n        ],\n        sidebar: [\n            '/',\n            '/usage',\n            '/structs',\n            '/hooks',\n            '/laravel-mix',\n            '/schema-org',\n            '/example-app',\n        ],\n        displayAllHeaders: true,\n        sidebarDepth: 2\n    }\n};\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Introduction\n\n## Installation\n\n```\ncomposer require romanzipp/laravel-seo\n```\n\n## Configuration\n\nCopy configuration to config folder:\n\n```\n$ php artisan vendor:publish --provider=\"romanzipp\\Seo\\Providers\\SeoServiceProvider\"\n```\n\n## Integrations\n\n### Laravel-Mix\n\nThis package can automatically preload all generated frontend assets via the Laravel Mix manifest.\n\nSee the [Laravel-Mix integration docs](/laravel-mix.html) for more information.\n\n### Schema.org\n\nWe also feature a basic integration for [Spaties Schema.org](https://github.com/spatie/schema-org) package to generate ld+json scripts.\n\nSee the [Schema.org integration docs](/schema-org.html) for more information.\n\n## Upgrading\n\n- [Upgrading from 1.0 to **2.0**](https://github.com/romanzipp/Laravel-SEO/releases/tag/2.0.0)\n\n## Cheat Sheet\n\n| Code | Rendered HTML |\n|----|----|\n| **Shorthand Setters** | |\n| `seo()->title('Laravel')` | `<title>Laravel</title>` |\n| `seo()->description('Laravel')` | `<meta name=\"description\" content=\"Laravel\" />` |\n| `seo()->meta('author', 'Roman Zipp')` | `<meta name=\"author\" content=\"Roman Zipp\" />` |\n| `seo()->twitter('card', 'summary')` | `<meta name=\"twitter:card\" content=\"summary\" />` |\n| `seo()->og('site_name', 'Laravel')` | `<meta name=\"og:site_name\" content=\"Laravel\" />` |\n| `seo()->charset()` | `<meta charset=\"utf-8\" />` |\n| `seo()->viewport()` | `<meta name=\"viewport\" content=\"width=device-width, ...\" />` |\n| `seo()->csrfToken()` | `<meta name=\"csrf-token\" content=\"...\" />` |\n| **Adding Structs** | |\n| `seo()->add(...)` | `<meta name=\"foo\" />` |\n| `seo()->addMany([...])` | `<meta name=\"foo\" />` |\n| `seo()->addIf(true, ...)` | `<meta name=\"foo\" />` |\n| **Various** | |\n| `seo()->mix()` | |\n| `seo()->hook()` | |\n| `seo()->render()` | |\n"
  },
  {
    "path": "docs/example-app.md",
    "content": "# Example App\n\n## Service Provider\n\n```\n$ php artisan make:provider SeoServiceProvider\n```\n\n#### `Providers/SeoServiceProvider.php`\n\n```php\nnamespace App\\Providers;\n\nuse Illuminate\\Support\\ServiceProvider;\nuse romanzipp\\Seo\\Builders\\StructBuilder;\nuse romanzipp\\Seo\\Facades\\Seo;\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Title;\n\nclass SeoServiceProvider extends ServiceProvider\n{\n    public function boot()\n    {\n        StructBuilder::$indent = str_repeat(' ', 4);\n\n        // Add a getTitle method for obtaining the unmodified title\n\n        Seo::macro('getTitle', function () {\n            /** @var \\romanzipp\\Seo\\Services\\SeoService $this */\n\n            if ( ! $title = $this->getStruct(Title::class)) {\n                return null;\n            }\n\n            if ( ! $body = $title->getBody()) {\n                return null;\n            }\n\n            return $body->getOriginalData();\n        });    \n\n        // Create a custom macro\n\n        Seo::macro('customTag', function (string $value) {\n            /** @var \\romanzipp\\Seo\\Services\\SeoService $this */\n\n            return $this->add(\n                Meta::make()->name('custom')->content($value)\n            );\n        });\n\n        // Add a hook to ensure the site name is always appended to the title \n\n        Title::hook(\n            Hook::make()\n                ->onBody()\n                ->callback(function ($body) {\n                    return ($body ? $body . ' | ' : '') . 'Site-Name';\n                })\n        );\n    }\n}\n```\n\n## Middleware\n\n```\n$ php artisan make:middleware AddSeoDefaults\n```\n\n#### `Http/Middleware/AddSeoDefaults.php`\n\n```php\nnamespace App\\Http\\Middleware;\n\nuse Closure;\nuse romanzipp\\Seo\\Structs\\Link;\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\nuse romanzipp\\Seo\\Structs\\Meta\\Twitter;\nuse romanzipp\\Seo\\Structs\\Script;\n\nclass AddSeoDefaults\n{\n    public function handle($request, Closure $next)\n    {\n        seo()->charset();\n        seo()->viewport();\n\n        seo()->title('Home');\n        seo()->description('My Description');\n\n        seo()->csrfToken();\n\n        seo()->addMany([\n\n            Meta::make()->name('copyright')->content('Roman Zipp'),\n\n            Meta::make()->name('mobile-web-app-capable')->content('yes'),\n            Meta::make()->name('theme-color')->content('#f03a17'),\n\n            Link::make()->rel('icon')->href('/assets/images/Logo.png'),\n\n            OpenGraph::make()->property('title')->content('Laravel'),\n            OpenGraph::make()->property('site_name')->content('Laravel'),\n            OpenGraph::make()->property('locale')->content('de_DE'),\n\n            Twitter::make()->name('card')->content('summary_large_image'),\n            Twitter::make()->name('site')->content('@romanzipp'),\n            Twitter::make()->name('creator')->content('@romanzipp'),\n            Twitter::make()->name('image')->content('/assets/images/Banner.jpg', false)\n\n        ]);\n        \n        seo('body')->add(\n            Script::make()->attr('src', '/js/app.js')\n        );\n\n        return $next($request);\n    }\n}\n\n```\n\n## Controllers\n\n```php\nnamespace App\\Http\\Controllers;\n\nuse App\\Models\\Post;\nuse Illuminate\\Http\\Request;\n\nclass PostController extends Controller\n{\n    public function index(Request $request)\n    {\n        seo()->title('All Posts');\n\n        $posts = Post::all();\n\n        return view('posts.index', compact('posts'));\n    }\n\n    public function show(Request $request, Post $post)\n    {\n        seo()->title($post->title ?: \"Post No. {$post->id}\");\n        seo()->description($post->intro);\n        seo()->image($post->thumbnail);\n\n        return view('posts.show', compact('post'));\n    }\n}\n```\n\n## View\n\n```blade\n<!DOCTYPE html>\n<html>\n<head>\n\n    {{ seo()->render() }}\n\n</head>\n<body>\n\n    @yield('content')\n    \n    {{ seo('body')->render() }}\n\n</body>\n</html>\n```\n"
  },
  {
    "path": "docs/hooks.md",
    "content": "# Hooks\n\nHooks allow the modification of a Structs **body** or **attributes**.\n\n### Adding hooks to Structs\n\n```php\nuse romanzipp\\Seo\\Helpers\\Hook;\n\n$hook = Hook::make()\n    ->onBody()\n    ->callback(function ($body) {\n        return $body;\n    });\n```\n\n**Method 1**: Call the `SeoService::hook()` method to apply a given `$hook` to a Struct class.\n\n```php\nuse romanzipp\\Seo\\Structs\\Title;\n\nseo()->hook(Title::class, $hook);\n```\n\n**Method 2**: Apply the `$hook` directly to the Struct.\n\n```php\nuse romanzipp\\Seo\\Structs\\Title;\n\nTitle::hook($hook);\n```\n\nBoth methods are basically the same, choose which one you prefer.\n\n## Examples\n\nFor example, you want to append a site name to the body of every `<title>` tag:\n\n#### Modify the `body` of all `Title` Structs.\n\n```php\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse romanzipp\\Seo\\Structs\\Title;\n\nTitle::hook(\n    Hook::make()\n        ->onBody()\n        ->callback(function ($body) {\n            return ($body ? $body . ' | ' : '') . 'Site-Name';\n        })\n);\n```\n\n```php\nuse romanzipp\\Seo\\Structs\\Title;\n\nseo()->add(Title::make()->body('Home'));  // <title>Home | Site-Name</title>\nseo()->add(Title::make()->body(null));    // <title>Site-Name</title>\n```\n\n----\n\n#### Modify any attribute of the `OpenGraph` Struct which has the attribute `property` with value `og:site_name`\n\n```php\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\n\nOpenGraph::hook(\n    Hook::make()\n        ->whereAttribute('property', 'og:site_name')\n        ->onAttributes()\n        ->callback(function ($attributes) {\n\n            $attributes['new'] = 'This will be added to all meta tags with property=\"og:site_name\"';\n\n            return $attributes;\n        })\n);\n```\n\n----\n\n#### Modify the `content` attribute of the `OpenGraph` Struct which has the attribute `property` with value `og:title`\n\n```php\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\n\nOpenGraph::hook(\n    Hook::make()\n        ->whereAttribute('property', 'og:title')\n        ->onAttribute('content')\n        ->callback(function ($content) {\n            return ($content ? $content . ' | ' : '') . 'Site-Name';\n        })\n);\n```\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\n\n$seo->add(OpenGraph::make()->property('title')->content('Home'));  // <meta ... content=\"Home | Site-Name\" />\n$seo->add(OpenGraph::make()->property('title')->content(null));    // <meta ... content=\"Site-Name\" />\n```\n\n## Reference\n\n### Hook Instance\n\n```php\nuse romanzipp\\Seo\\Helpers\\Hook;\n\n$hook = Hook::make();\n\n$hook = new Hook;\n```\n\n### Hook Targets\n\n#### Target Struct Body\n\nYou will receive `$body` parameter of type `null|string` in the callback function\n\n```php\n\n$hook\n    ->onBody()\n    ->callback(function ($body) {\n        return $body;\n    });\n```\n\n#### Target any Struct Attribute\n\nYou will receive `$attributes` parameter of type `array` in the callback function\n\n```php\n$hook\n    ->onAttributes('content')\n    ->callback(function ($attributes) {\n        return $attributes;\n    });\n```\n\n#### Target a specific Struct Attribute\n\nYou will receive `$attribute` parameter of type `null|string` in the callback function\n\n```php\n$hook\n    ->onAttribute('content')\n    ->callback(function ($attribute) {\n        return $attribute;\n    });\n```\n\n### Hook Filters\n\nFilter Structs by `$attribute` with value `$value`\n\n```php\n$hook->whereAttribute($attribute, $value);\n```\n"
  },
  {
    "path": "docs/laravel-mix.md",
    "content": "# Laravel-Mix\n\nYou 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.\n\n## Basic example\n\n```php\nseo()\n    ->mix()\n    ->load();\n```\n\n**mix-manifest.json**\n\n```json\n{\n  \"/js/app.js\": \"/js/app.js?id=123456789\",\n  \"/css/app.css\": \"/css/app.css?id=123456789\"\n}\n```\n\n**document `<head>`**\n\n```html\n<link rel=\"prefetch\" href=\"/js/app.js?id=123456789\" />\n<link rel=\"prefetch\" href=\"/css/app.css?id=123456789\" />\n```\n\n## Specify an alternate manifest path\n\n```php\nseo()\n    ->mix()\n    ->load(public_path('custom-manifest.json'));\n```\n\n## Ignore certain assets\n\nBy 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.\n\nIn this example, we will stop all **admin** frontend assets from prefetching by returning `null` within the provided map callback.\n\n```php\nuse romanzipp\\Seo\\Conductors\\Types\\ManifestAsset;\n\nseo()\n    ->mix()\n    ->map(static function(ManifestAsset $asset): ?ManifestAsset {\n\n        if (strpos($asset->path, 'admin') !== false) {\n            return null;\n        }\n\n        return $asset;\n    })\n    ->load();\n```\n\n**mix-manifest.json**\n\n```json\n{\n  \"/js/app.js\": \"/js/app.js?id=123456789\",\n  \"/js/admin.js\": \"/js/admin.js?id=123456789\",\n  \"/css/app.css\": \"/css/app.css?id=123456789\",\n  \"/css/admin.css\": \"/css/admin.css?id=123456789\"\n}\n```\n\n**document `<head>`**\n\n```html\n<link rel=\"prefetch\" href=\"/js/app.js?id=123456789\" />\n<link rel=\"prefetch\" href=\"/css/app.css?id=123456789\" />\n```\n\n## Provide an absolute URL\n\nYou can force your preloaded/prefetched assets to use an alternate URL by modifying the `url` attribute.\n\n```php\nuse romanzipp\\Seo\\Conductors\\Types\\ManifestAsset;\n\nseo()\n    ->mix()\n    ->map(static function(ManifestAsset $asset): ?ManifestAsset {\n\n        $asset->url = \"http://localhost{$asset->url}\";\n\n        return $asset;\n    })\n    ->load();\n```\n\n**mix-manifest.json**\n\n```json\n{\n  \"/js/app.js\": \"/js/app.js?id=123456789\",\n  \"/css/app.css\": \"/css/app.css?id=123456789\"\n}\n```\n\n**document `<head>`**\n\n```html\n<link rel=\"prefetch\" href=\"http://localhost/js/app.js?id=123456789\" />\n<link rel=\"prefetch\" href=\"http://localhost/css/app.css?id=123456789\" />\n```\n\n## Change mechanism\n\nBy 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/).\n\nYou 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`.\n\n```php\nuse romanzipp\\Seo\\Conductors\\Types\\ManifestAsset;\n\nseo()\n    ->mix()\n    ->map(static function(ManifestAsset $asset): ?ManifestAsset {\n\n        $asset->rel = 'prefetch';\n\n        if (strpos($asset->path, 'component') !== false) {\n            $asset->rel = 'preload';\n        }\n\n        return $asset;\n    })\n    ->load();\n```\n\n**mix-manifest.json**\n\n```json\n{\n  \"/js/app.js\": \"/js/app.js?id=123456789\",\n  \"/js/app.routes.js\": \"/js/app.routes.js?id=123456789\",\n  \"/js/app.user-component.js\": \"/js/app.user-component.js?id=123456789\",\n  \"/js/app.news-component.js\": \"/js/app.news-component.js?id=123456789\"\n}\n```\n\n**document `<head>`**\n\n```html\n<link rel=\"prefetch\" href=\"/js/app.js?id=123456789\" />\n<link rel=\"prefetch\" href=\"/js/app.routes.js?id=123456789\" />\n<link rel=\"preload\" href=\"/js/app.user-component.js?id=123456789\" />\n<link rel=\"preload\" href=\"/js/app.news-component.js?id=123456789\" />\n```\n\n## Asset resource type\n\nPreloading 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.\nFeels free to change the resource type.\n\n```php\nuse romanzipp\\Seo\\Conductors\\Types\\ManifestAsset;\n\nseo()\n    ->mix()\n    ->map(static function(ManifestAsset $asset): ?ManifestAsset {\n\n        if (strpos($asset->path, 'virus') !== false) {\n            $asset->as = 'virus';\n        }\n\n        return $asset;\n    })\n    ->load();\n```\n\n**mix-manifest.json**\n\n```json\n{\n  \"/css/app.css\": \"/css/app.css?id=123456789\",\n  \"/js/app.js\": \"/js/app.routes.js?id=123456789\",\n  \"/data/totally-not-a-virus\": \"/data/totally-not-a-virus?id=123456789\",\n  \"/data/totally-not-a-virus\": \"/data/totally-not-a-virus?id=123456789\"\n}\n```\n\n**document `<head>`**\n\n```html\n<link rel=\"prefetch\" as=\"style\" href=\"/css/app.css?id=123456789\" />\n<link rel=\"prefetch\" as=\"script\" href=\"/js/app.js?id=123456789\" />\n<link rel=\"prefetch\" as=\"virus\" href=\"/data/totally-not-a-virus?id=123456789\" />\n<link rel=\"prefetch\" as=\"virus\" href=\"/data/totally-not-a-virus?id=123456789\" />\n```\n"
  },
  {
    "path": "docs/schema-org.md",
    "content": "# Schema.org Integration\n\nThis package features a basic integration for [Spaties Schema.org](https://github.com/spatie/schema-org) package to generate ld+json scripts.\nAdded Schema types render with the packages structs.\n\n```php\nuse Spatie\\SchemaOrg\\Schema;\n\nseo()->addSchema(\n    Schema::localBusiness()->name('Spatie')\n);\n```\n\n```php\nuse Spatie\\SchemaOrg\\Schema;\n\nseo()->setSchemes([\n    Schema::localBusiness()->name('Spatie'),\n    Schema::airline()->name('Spatie'),\n]);\n```\n"
  },
  {
    "path": "docs/structs.md",
    "content": "# Structs\n\n**Structs** are a code representation of **HTML head elements**.\n\n## Available Shorthand Methods\n\nShorthand methods are **predefined shortcuts** to add commonly used Structs without the hassle of importing struct classes or chain many methods. \n\nWhen using shorthand methods, you will skip the `seo()->add()` method.\nYou can configure which Structs should be added on shorthand calls in the `seo.php` config file under the `shorthand` key.\n\n### Title\n\n```php\nseo()->title(string $title = null, bool $escape = true);\n```\n\n<details>\n<summary>same as ...</summary>\n\n```php\nuse romanzipp\\Seo\\Structs\\Title;\nuse romanzipp\\Seo\\Structs\\Meta;\n\nseo()->addMany([\n\n    Title::make()\n        ->body(string $title = null),\n\n    Meta\\OpenGraph::make()\n        ->property('title')\n        ->content(string $title = null),\n\n    Meta\\Twitter::make()\n        ->name('title')\n        ->content(string $title = null),\n\n    Meta\\EmbedX::make()\n        ->name('title')\n        ->content(string $title = null),\n\n]);\n```\n\n</details>\n\n<details>\n<summary>renders to ...</summary>\n\n```html\n<title>{title}</title>\n<meta property=\"og:title\" content=\"{title}\" />\n<meta name=\"twitter:title\" content=\"{title}\" />\n<meta name=\"embedx:title\" content=\"{title}\" />\n```\n\n</details>\n\n### Description\n\n```php\nseo()->description(string $description = null, bool $escape = true);\n```\n\n<details>\n<summary>same as ...</summary>\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta;\n\nseo()->addMany([\n\n    Meta\\Description::make()\n        ->name('description')\n        ->content(string $description = null),\n\n    Meta\\OpenGraph::make()\n        ->property('description')\n        ->content(string $description = null),\n\n    Meta\\Twitter::make()\n        ->name('description')\n        ->content(string $description = null),\n\n    Meta\\EmbedX::make()\n        ->name('description')\n        ->content(string $description = null),\n\n]);\n```\n\n</details>\n\n<details>\n<summary>renders to ...</summary>\n\n```html\n<meta name=\"description\" content=\"{description}\" />\n<meta property=\"og:description\" content=\"{description}\" />\n<meta name=\"twitter:description\" content=\"{description}\" />\n<meta name=\"embedx:description\" content=\"{description}\" />\n```\n\n</details>\n\n### Image\n\n```php\nseo()->image(string $image = null, bool $escape = true);\n```\n\n<details>\n<summary>same as ...</summary>\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta;\n\nseo()->addMany([\n\n    Meta::make()\n        ->name('image')\n        ->content($image, $escape),\n    \n    \n    Meta\\OpenGraph::make()\n        ->property('image')\n        ->content($image, $escape),\n    \n    \n    Meta\\Twitter::make()\n        ->name('image')\n        ->content($image, $escape),\n\n    Meta\\EmbedX::make()\n        ->name('image')\n        ->content($image, $escape),\n\n]);\n```\n\n</details>\n\n<details>\n<summary>renders to ...</summary>\n\n```html\n<meta name=\"image\" content=\"{image}\" />\n<meta property=\"og:image\" content=\"{image}\" />\n<meta name=\"twitter:image\" content=\"{image}\" />\n<meta name=\"embedx:image\" content=\"{image}\" />\n```\n\n</details>\n\n### Meta\n\n```php\nseo()->meta(string $name, $content = null, bool $escape = true);\n```\n\n<details>\n<summary>same as ...</summary>\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta;\n\nseo()->add(\n    Meta::make()\n        ->name(string $name, bool $escape = true)\n        ->content($content = null, bool $escape = true)\n);\n```\n\n</details>\n\n<details>\n<summary>renders to ...</summary>\n\n```html\n<meta name=\"{name}\" content=\"{content}\" />\n```\n\n</details>\n\n### OpenGraph\n\n```php\nseo()->og(string $property, $content = null, bool $escape = true);\n```\n\n<details>\n<summary>same as ...</summary>\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\n\nseo()->add(\n    OpenGraph::make()\n        ->property(string $property, bool $escape = true)\n        ->content($content = null, bool $escape = true)\n);\n```\n\n</details>\n\n<details>\n<summary>renders to ...</summary>\n\n```html\n<meta name=\"og:{property}\" content=\"{content}\" />\n```\n\n</details>\n\n### Twitter\n\n```php\nseo()->twitter(string $name, $content = null, bool $escape = true);\n```\n\n<details>\n<summary>same as ...</summary>\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta\\Twitter;\n\nseo()->add(\n    Twitter::make()\n        ->name(string $name, bool $escape = true)\n        ->content($content = null, bool $escape = true)\n);\n```\n\n</details>\n\n<details>\n<summary>renders to ...</summary>\n\n```html\n<meta name=\"twitter:{name}\" content=\"{content}\" />\n```\n\n</details>\n\n### [EmbedX](https://embedx.app)\n\n[EmbedX](https://embedx.app) allows you to display rich embed thumbnails on X/Twitter and other social media platforms.\n\n```php\nseo()->embedx(string $name, $content = null, bool $escape = true);\n```\n\n<details>\n<summary>same as ...</summary>\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta\\EmbedX;\n\nseo()->add(\n    EmbedX::make()\n        ->name(string $name, bool $escape = true)\n        ->content($content = null, bool $escape = true)\n);\n```\n\n</details>\n\n<details>\n<summary>renders to ...</summary>\n\n```html\n<meta name=\"embedx:{name}\" content=\"{content}\" />\n```\n\n</details>\n\n### Canonical\n\n```php\nseo()->canonical(string $canonical);\n```\n\n<details>\n<summary>same as ...</summary>\n\n```php\nuse romanzipp\\Seo\\Structs\\Link\\Canonical;\n\nseo()->add(\n    Canonical::make()\n        ->href($canonical = null)\n);\n```\n\n</details>\n\n<details>\n<summary>renders to ...</summary>\n\n```html\n<link rel=\"canonical\" href=\"{canonical}\" />\n```\n\n</details>\n\n### CSRF Token\n\n```php\nseo()->csrfToken(string $token = null);\n```\n\n<details>\n<summary>same as ...</summary>\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta\\CsrfToken;\n\nseo()->add(\n    CsrfToken::make()\n        ->token($token = null)\n);\n```\n\n</details>\n\n<details>\n<summary>renders to ...</summary>\n\n```html\n<meta name=\"csrf-token\" content=\"{token}\" />\n```\n\n</details>\n\n## Adding single structs\n\nIf 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.\n\n*Remember: [There are many methods available for adding new structs](/usage.html#how-to-register-tags)* \n\n### Titles\n\n```php\nuse romanzipp\\Seo\\Structs\\Title;\n\nseo()->add(\n    Title::make()->body('This is a Title')\n);\n```\n\n```html\n<title>This is a Title</title>\n```\n\n### Meta Tags\n\nUsing the `attr(string $attribute, $value = null)` method, we can append attributes with given values.\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta;\n\nseo()->add(\n    Meta::make()\n        ->attr('name', 'theme-color')\n        ->attr('content', 'red')\n);\n```\n\n```html\n<meta name=\"theme-color\" content=\"red\" />\n```\n\n### OpenGraph\n\nBecause **OpenGraph** tags are `<meta />` elements, the `OpenGraph` Struct extends the `Meta` class.\n\nAll **OpenGraph** elements are defined by `property=\"\"` and `content=\"\"` attributes where the `property` value starts with a `og:` prefix.\n\nInstead of using the `attr()` Struct method, we can use the shorthand `property()` and `content()` methods by the `OpenGraph` class.\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\n\nseo()->add(\n    OpenGraph::make()\n        ->attr('property', 'og:site_name')\n        ->attr('content', 'Laravel')\n);\n```\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\n\nseo()->add(\n    OpenGraph::make()\n        ->property('site_name')\n        ->content('Laravel')\n);\n```\n\n... both render to ...\n\n```html\n<meta property=\"og:site_name\" content=\"Laravel\" />\n```\n\n### Twitter\n\n**Twitter** meta tags share the same behavior as **OpenGraph** tags while the property prefix is `twitter:`.\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta\\Twitter;\n\nseo()->add(\n    Twitter::make()\n        ->attr('name', 'twitter:card')\n        ->attr('content', 'summary')\n);\n```\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta\\Twitter;\n\nseo()->add(\n    Twitter::make()\n        ->name('card')\n        ->content('summary')\n);\n```\n\n... both render to ...\n\n```html\n<meta name=\"twitter:card\" content=\"summary\" />\n```\n\n## Available Structs\n\n### Base\n\n```php\nromanzipp\\Seo\\Structs\\Base::make();\n```\n\n### Link\n\n```php\nromanzipp\\Seo\\Structs\\Link::make();\n```\n\n```php\nromanzipp\\Seo\\Structs\\Link\\Canonical::make();\n```\n\n### Meta\n\n```php\nromanzipp\\Seo\\Structs\\Meta::make();\n```\n\n```php\nromanzipp\\Seo\\Structs\\Meta\\Article::make()\n    ->property(string $value, bool $escape = true)\n    ->content(string $value, bool $escape = true);\n```\n\n```php\nromanzipp\\Seo\\Structs\\Meta\\AppLink::make()\n    ->property(string $value, bool $escape = true)\n    ->content(string $value, bool $escape = true);\n```\n\n```php\nromanzipp\\Seo\\Structs\\Meta\\Charset::make()\n    ->charset(string $charset, bool $escape = true);\n```\n\n```php\nromanzipp\\Seo\\Structs\\Meta\\CsrfToken::make()\n    ->token($token = null, bool $escape = true);\n```\n\n```php\nromanzipp\\Seo\\Structs\\Meta\\Description::make();\n```\n\n```php\nromanzipp\\Seo\\Structs\\Meta\\OpenGraph::make()\n    ->property(string $value, bool $escape = true)\n    ->content(string $value = null, bool $escape = true);\n```\n\n```php\nromanzipp\\Seo\\Structs\\Meta\\Robots::make();\n```\n\n```php\nromanzipp\\Seo\\Structs\\Meta\\Twitter::make()\n    ->name(string $value, bool $escape = true)\n    ->content(string $value, bool $escape = true);\n```\n\n```php\nromanzipp\\Seo\\Structs\\Meta\\Viewport::make()\n    ->content(string $content, bool $escape = true);\n```\n\n### Noscript\n\n```php\nromanzipp\\Seo\\Structs\\Noscript::make();\n```\n\n### Script\n\n```php\nromanzipp\\Seo\\Structs\\Script::make();\n```\n\n### Title\n\n```php\nromanzipp\\Seo\\Structs\\Title::make();\n```\n\n## Escaping\n\nBy 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.\n\n**Use this feature with caution!**\n\n```php\nuse romanzipp\\Seo\\Structs\\Title;\n\nTitle::make()->body('Dont \\' escape me!', false);\n```\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta;\n\nMeta::make()->attr('content', 'Dont \\' escape me!', false);\n```\n\n## Creating custom Structs\n\nYou can create your own Structs simply by extending the `romanzipp\\Seo\\Structs\\Struct` class.\n\n```php\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass MyStruct extends Struct\n{\n    //\n}\n```\n\nWe differentiate between [**void elements**](https://www.w3.org/TR/html5/syntax.html#writing-html-documents-elements) and **normal elements**.\n**Void elements**, like `<meta />` can not have a closing tag other than **normal elements** like `<title></title>`.\n\n### Tag\n\nA struct **always** requires a **tag**. This can be set by implementing the abstract `tag()` method.\n\n```php\nprotected function tag(): string\n{\n    return 'script';\n}\n```\n\n### Unique tags\n\nCertain elements in a documents `<head>` can only exist once, like the `<title></title>` element.\n\nBy default, Structs are **not** unique. To change this behavior, apply the `unique` property.\n\n```php\nprotected $unique = true;\n```\n\nNow, previously created Structs will be overwritten.\n\n```php\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass MyStruct extends Struct\n{\n    protected $unique = false;\n   \n    protected function tag(): string\n    {\n        return 'foo';\n    }\n}\n\nseo()->add(MyStruct::make()->attr('name', 'my-description')->attr('content', 'This is the FIRST description'));\nseo()->add(MyStruct::make()->attr('name', 'my-description')->attr('content', 'This is the SECOND description'));\n```\n\n**Before**: (`$unique = false`)\n\n```html\n<foo name=\"my-description\" content=\"This is the FIRST description\" />\n<foo name=\"my-description\" content=\"This is the SECOND description\" />\n```\n\n**After**: (`$unique = true`)\n\n```html\n<foo name=\"my-description\" content=\"This is the SECOND description\" />\n```\n\n### Unique attributes\n\nYou are also able to modify the unique attributes by setting the `uniqueAttributes` property. If empty, just the tag name will be considered as unique.\n\n```php\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass MyStruct extends Struct\n{\n    protected $uniqueAttributes = ['name'];\n\n    protected function tag(): string\n    {\n        return 'foo';\n    }\n}\n\nseo()->add(MyStruct::make()->attr('name', 'my-description')->attr('content', 'This is the FIRST description'));\nseo()->add(MyStruct::make()->attr('name', 'my-description')->attr('content', 'This is the SECOND description'));\n\nseo()->add(MyStruct::make()->attr('name', 'my-title')->attr('content', 'This is the FIRST title'));\nseo()->add(MyStruct::make()->attr('name', 'my-title')->attr('content', 'This is the SECOND title'));\n```\n\n**Before**: (`$uniqueAttributes = []`)\n\n```html\n<foo name=\"my-description\" content=\"This is the FIRST description\" />\n<foo name=\"my-description\" content=\"This is the SECOND description\" />\n\n<foo name=\"my-title\" content=\"This is the FIRST title\" />\n<foo name=\"my-title\" content=\"This is the SECOND title\" />\n```\n\n**After**: (`$uniqueAttributes = ['name']`)\n\n```html\n<foo name=\"my-description\" content=\"This is the SECOND description\" />\n\n<foo name=\"my-title\" content=\"This is the SECOND title\" />\n```\n\n### Defaults\n\nAfter a Struct instance has been created, we call the static `defaults` method.\n\n```php\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass MyStruct extends Struct\n{\n    public function __construct()\n    {\n        static::defaults($this);\n    }\n\n    public static function defaults(self $struct): void\n    {\n        //\n    }\n}\n```\n\nBy implementing the `defaults` method on your custom Struct, you can run any custom logic like adding default attributes.\n\nThis is used among others in the `romanzipp\\Seo\\Structs\\Meta\\Charset` Struct to set a default charset attribute.\n\n```php\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass Charset extends Meta\n{\n    public static function defaults(Struct $struct): void\n    {\n        $struct->addAttribute('charset', 'utf-8');\n    }\n}\n```\n"
  },
  {
    "path": "docs/usage.md",
    "content": "# Usage\n\n## Instantiation\n\nYou can access the SEO service in many different ways. Just use what you prefer! We will use the `seo()` function in this documentaiton.\n\n```php\nuse romanzipp\\Seo\\Facades\\Seo;\nuse romanzipp\\Seo\\Services\\SeoService;\n\n$seo = seo();\n\n$seo = app(SeoService::class);\n\n$seo = Seo::make();\n```\n\n## Render\n\nPlace this code snippet in your blade view.\n\n```blade\n{{ seo()->render() }}\n```\n\n## How to register tags\n\nℹ️ Going forward we will refer to head/meta elements as **Structs**.\n\nThis package offers many ways of adding new elements (**Structs**) to your `<head>`.\n\n1. Add commonly used structs via [shorthand setters](#shorthand-setters) like `seo()->title('...')`\n2. Manually add single structs via the `seo()->add()` [methods](#add-structs)\n3. Specify an [array of contents](#array-format) via `seo()->addFromArray()`\n\n### Shorthand setters\n\nShorthand setters are **predefined shortcuts** to add commonly used Structs without the hassle of importing struct classes or chain many methods.\n\nWhen using shorthand methods, you will skip the `seo()->add()` method.\nYou can configure which Structs should be added on shorthand calls in the `seo.php` config file under the `shorthand` key.\n\n#### Title\n\n```php\nseo()->title('Laravel');\n```\n\n... renders to ...\n\n```html\n<title>Laravel</title>\n<meta property=\"og:title\" content=\"Laravel\" />\n<meta name=\"twitter:title\" content=\"Laravel\" />\n```\n\n#### Meta\n\n```php\nseo()->meta('copyright', 'Roman Zipp');\n```\n\n... renders to ...\n\n```html\n<meta name=\"copyright\" content=\"Roman Zipp\" />\n```\n\nTake a look at the [shorthand setter docs](/structs.html#available-shorthand-methods) for all available methods.\n\n### Add Structs\n\nIf 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.\n\nFurther reading: [Adding single structs](/structs.html#adding-single-structs)\n\n#### Single Structs\n\n```php\nuse romanzipp\\Seo\\Structs\\Title;\n\nseo()->add(\n    Title::make()->body('My Title')\n);\n```\n\n#### Multiple Structs\n\n```php\nuse romanzipp\\Seo\\Structs\\Title;\nuse romanzipp\\Seo\\Structs\\Meta\\Description;\n\nseo()->addMany([\n    Title::make()->body('My Title'),\n    Description::make()->content('My Description'),\n]);\n```\n\n#### Conditional additions\n\n```php\nuse romanzipp\\Seo\\Structs\\Title;\n\n$boolean = random_int(0, 1) === 1;\n\nseo()->addIf(\n    $boolean,\n    Title::make()->body('My Title')\n);\n```\n\n### Array format\n\nYou can also register structs using the following format. This can be helpful if you are fetching SEO information from a database.\n\n```php\nseo()->addFromArray([\n\n    // The following items share the same behavior as the equally named shorthand setters.\n\n    'title' => 'Laravel',\n    'description' => 'Laravel',\n    'charset' => 'utf-8',\n    'viewport' => 'width=device-width, initial-scale=1',\n\n    // Twitter & Open Graph\n\n    'twitter' => [\n        // <meta name=\"twitter:card\" content=\"summary\" />\n        // <meta name=\"twitter:creator\" content=\"@romanzipp\" />\n        'card' => 'summary',\n        'creator' => '@romanzipp',\n    ],\n\n    'og' => [\n        // <meta property=\"og:locale\" content=\"de\" />\n        // <meta property=\"og:site_name\" content=\"Laravel\" />\n        'locale' => 'de',\n        'site_name' => 'Laravel',\n    ],\n\n    // Custom meta & link structs. Each child array defines an attribute => value mapping.\n\n    'meta' => [\n        // <meta name=\"copyright\" content=\"Roman Zipp\" />\n        // <meta name=\"theme-color\" content=\"#f03a17\" />\n        [\n            'name' => 'copyright',\n            'content' => 'Roman Zipp',\n        ],\n        [\n            'name' => 'theme-color',\n            'content' => '#f03a17',\n        ],\n    ],\n\n    'link' => [\n        // <link rel=\"icon\" href=\"/favicon.ico\" />\n        // <link rel=\"preload\" href=\"/fonts/IBMPlexSans.woff2\" />\n        [\n            'rel' => 'icon',\n            'href' => '/favicon.ico',\n        ],\n        [\n            'rel' => 'preload',\n            'href' => '/fonts/IBMPlexSans.woff2',\n        ],\n    ],\n\n]);\n```\n\n## Sections\n\nYou 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.\n\nSections 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.\n\n### Using sections\n\n```php\n// This struct will be added to the \"default\" section\nseo()->twitter('card', 'summary');\n\n// This struct will be added to the \"secondary\" section\nseo()->section('secondary')->twitter('card', 'image');\n\n// This struct will be also added to the \"default\" section since the section() method changes are not persistent \nseo()->twitter('card', 'summary');\n```\n\nYou can also pass the section as parameter to the helper function.\n\n```php\nseo('secondary')->twitter('card', 'image');\n```\n\n### Rendering sections\n\nThis will render all structs added to the \"default\" section.\n\n```blade\n{{ seo()->render() }}\n```\n\nThis will render all structs added to the \"secondary\" section.\n\n```blade\n{{ seo()->section('secondary')->render() }}\n```\n\nOf course, you can also pass the section as parameter to the helper function.\n\n```blade\n{{ seo('secondary')->render() }}\n```\n\n### Using sections with dependency resolving\n\n```php\nuse romanzipp\\Seo\\Services\\SeoService;\n\n$seo = app(SeoService::class);\n\n// will be applied to \"default\" section\n$seo->twitter('card', 'summary');\n\n// will be applied to \"secondary\" section\n$seo->section('secondary')->twitter('card', 'summary');\n\n// WARNING!\n// This struct will be applied to the \"secondary\" section since the service instance has been resolved\n// once and was set to \"secondary\" section in the previous step\n$seo->twitter('card', 'summary');\n```\n\n## Macros\n\nThe `romanzipp\\Seo\\Services\\SeoService` class uses the Laravel `Macroable` trait which allows creating short macros.\n\n### Example\n\nLet's say you want to display a page title in the document body but added a hook to append the site name.\n\nIn this case, we'll create a macro to retreive the original Title Struct body value.\n\n```php\nuse romanzipp\\Seo\\Facades\\Seo;\nuse romanzipp\\Seo\\Structs\\Title;\n\nSeo::macro('getTitle', function () {\n\n    if ( ! $title = $this->getStruct(Title::class)) {\n        return null;\n    }\n\n    if ( ! $body = $title->getBody()) {\n        return null;\n    }\n\n    return $body->getOriginalData();\n});\n```\n\n## Recommended Minimum\n\nFor a full reference of what **could** go to your `<head>` see [joshbuchea's HEAD](https://github.com/joshbuchea/HEAD)\n\n```php\nseo()->charset('utf-8');\nseo()->viewport('width=device-width, initial-scale=1, viewport-fit=cover');\nseo()->title('My Title');\n```\n\n```html\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<title>My Title</title>\n```\n\n## Clear all added Structs\n\n```php\nseo()->clearStructs();\n```\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"scripts\": {\n        \"docs:dev\": \"vuepress dev docs\",\n        \"docs:build\": \"vuepress build docs\"\n    },\n    \"devDependencies\": {\n        \"vuepress\": \"^1.8.2\"\n    }\n}\n"
  },
  {
    "path": "phpstan.neon.dist",
    "content": "parameters:\n  phpVersion: 70100\n  level: 6\n  paths:\n    - src\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit bootstrap=\"vendor/autoload.php\"\n         backupGlobals=\"false\"\n         backupStaticAttributes=\"false\"\n         colors=\"true\"\n         verbose=\"true\"\n         convertErrorsToExceptions=\"true\"\n         convertNoticesToExceptions=\"true\"\n         convertWarningsToExceptions=\"true\"\n         processIsolation=\"false\"\n         stopOnFailure=\"false\">\n    <testsuites>\n        <testsuite name=\"Seo Test Suite\">\n            <directory>tests</directory>\n        </testsuite>\n    </testsuites>\n    <filter>\n        <whitelist processUncoveredFilesFromWhitelist=\"true\">\n            <directory suffix=\".php\">src</directory>\n        </whitelist>\n    </filter>\n</phpunit>\n"
  },
  {
    "path": "src/Builders/StructBuilder.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Builders;\n\nuse Illuminate\\Support\\HtmlString;\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass StructBuilder\n{\n    public const TAG_SYNTAX_HTML5 = 'html5';\n\n    public const TAG_SYNTAX_XHTML = 'xhtml';\n\n    public const TAG_SYNTAX_XHTML_STRICT = 'xhtml_strict';\n\n    /**\n     * Indent rendered struct.\n     *\n     * @var string|null\n     */\n    public static $indent;\n\n    /**\n     * Separator for rendered structs.\n     *\n     * @var mixed|null\n     */\n    public static $separator = PHP_EOL;\n\n    /**\n     * Struct object.\n     *\n     * @var \\romanzipp\\Seo\\Structs\\Struct\n     */\n    private $struct;\n\n    /**\n     * Constructor.\n     *\n     * @param \\romanzipp\\Seo\\Structs\\Struct $struct\n     */\n    public function __construct(Struct $struct)\n    {\n        $this->struct = $struct;\n    }\n\n    /**\n     * Instantly build struct.\n     *\n     * @param \\romanzipp\\Seo\\Structs\\Struct $struct\n     *\n     * @return \\Illuminate\\Support\\HtmlString\n     */\n    public static function build(Struct $struct): HtmlString\n    {\n        return (new self($struct))->render();\n    }\n\n    /**\n     * Render element.\n     *\n     * @return \\Illuminate\\Support\\HtmlString\n     */\n    public function render(): HtmlString\n    {\n        $element = '';\n\n        if ($indent = self::$indent) {\n            $element .= $indent;\n        }\n\n        $element .= \"<{$this->struct->getTag()}\";\n\n        if ($attributes = $this->renderAttributes()) {\n            $element .= \" {$attributes}\";\n        }\n\n        $body = $this->struct->getBody();\n\n        $syntax = config('seo.tag_syntax') ?? self::TAG_SYNTAX_XHTML;\n\n        if ($body || ! $this->struct->isVoidElement()) {\n            $element .= \">{$body}</{$this->struct->getTag()}>\";\n        } else {\n            switch ($syntax) {\n                case self::TAG_SYNTAX_HTML5:\n                    $element .= '>';\n                    break;\n                case self::TAG_SYNTAX_XHTML:\n                    $element .= ' />';\n                    break;\n                case self::TAG_SYNTAX_XHTML_STRICT:\n                    $element .= \"></{$this->struct->getTag()}>\";\n                    break;\n                default:\n                    $element .= ' />';\n            }\n        }\n\n        return new HtmlString($element);\n    }\n\n    /**\n     * Render struct attributes to string.\n     *\n     * @return string\n     */\n    private function renderAttributes(): string\n    {\n        $attributes = [];\n\n        foreach ($this->struct->getComputedAttributes() as $attribute => $attributeValue) {\n            $attribute = trim($attribute);\n\n            if (null !== $attributeValue->data()) {\n                $attribute .= \"=\\\"{$attributeValue}\\\"\";\n            }\n\n            $attributes[] = $attribute;\n        }\n\n        return implode(' ', $attributes);\n    }\n}\n"
  },
  {
    "path": "src/Collections/Contracts/CollectionContract.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Collections\\Contracts;\n\ninterface CollectionContract\n{\n}\n"
  },
  {
    "path": "src/Collections/SchemaCollection.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Collections;\n\nuse romanzipp\\Seo\\Collections\\Contracts\\CollectionContract;\nuse romanzipp\\Seo\\Schema\\Schema as SchemaContainer;\n\nclass SchemaCollection implements CollectionContract\n{\n    /**\n     * @var \\romanzipp\\Seo\\Schema\\Schema[]\n     */\n    protected $schemas = [];\n\n    /**\n     * @return \\romanzipp\\Seo\\Schema\\Schema[]\n     */\n    public function all(): array\n    {\n        return $this->schemas;\n    }\n\n    public function add(SchemaContainer $schema): void\n    {\n        $this->schemas[] = $schema;\n    }\n\n    /**\n     * @param \\romanzipp\\Seo\\Schema\\Schema[] $schemas\n     */\n    public function set(array $schemas): void\n    {\n        $this->schemas = $schemas;\n    }\n}\n"
  },
  {
    "path": "src/Collections/StructCollection.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Collections;\n\nuse romanzipp\\Seo\\Collections\\Contracts\\CollectionContract;\nuse romanzipp\\Seo\\Structs\\Struct;\n\n/**\n * The Seo class functions as an intermediate layer between the laravel dependency container\n * and the SeoService singleton instance.\n *\n * This intermediate class has been introduced to support the sections feature.\n */\nclass StructCollection implements CollectionContract\n{\n    /**\n     * @var \\romanzipp\\Seo\\Structs\\Struct[]\n     */\n    protected $structs = [];\n\n    /**\n     * @return \\romanzipp\\Seo\\Structs\\Struct[]\n     */\n    public function all(): array\n    {\n        return $this->structs;\n    }\n\n    public function add(Struct $struct): void\n    {\n        $this->structs[] = $struct;\n    }\n\n    /**\n     * @param \\romanzipp\\Seo\\Structs\\Struct[] $structs\n     */\n    public function set(array $structs): void\n    {\n        $this->structs = $structs;\n    }\n\n    public function unset(int $index): void\n    {\n        unset($this->structs[$index]);\n    }\n\n    public function remove(Struct $struct): void\n    {\n        $this->structs = array_filter($this->structs, function (Struct $existing) use ($struct): bool {\n            return $existing !== $struct;\n        });\n    }\n}\n"
  },
  {
    "path": "src/Conductors/ArrayFormatConductor.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Conductors;\n\nuse romanzipp\\Seo\\Conductors\\ArrayStructures\\AbstractArraySchema;\nuse romanzipp\\Seo\\Conductors\\ArrayStructures\\AttributeArraySchema;\nuse romanzipp\\Seo\\Conductors\\ArrayStructures\\NestedArraySchema;\nuse romanzipp\\Seo\\Conductors\\ArrayStructures\\SingleArraySchema;\nuse romanzipp\\Seo\\Services\\SeoService;\nuse romanzipp\\Seo\\Structs;\n\nclass ArrayFormatConductor\n{\n    /**\n     * @var \\romanzipp\\Seo\\Services\\SeoService\n     */\n    private $seo;\n\n    public function __construct(SeoService $seo)\n    {\n        $this->seo = $seo;\n    }\n\n    /**\n     * Get the predefined schemas for array formatting.\n     *\n     * @return array<string, \\romanzipp\\Seo\\Conductors\\ArrayStructures\\AbstractArraySchema>\n     */\n    private function getSchemas(): array\n    {\n        return [\n            /*\n             * Single key-value pair.\n             *\n             *     $data = [\n             *         'title' => 'Foo'\n             *     ];\n             */\n\n            'title' => SingleArraySchema::make()->callback(function (string $value) {\n                $this->seo->title($value);\n            }),\n\n            'description' => SingleArraySchema::make()->callback(function (string $value) {\n                $this->seo->description($value);\n            }),\n\n            'charset' => SingleArraySchema::make()->callback(function (string $value) {\n                $this->seo->charset($value);\n            }),\n\n            'viewport' => SingleArraySchema::make()->callback(function (string $value) {\n                $this->seo->viewport($value);\n            }),\n\n            'canonical' => SingleArraySchema::make()->callback(function (string $value) {\n                $this->seo->canonical($value);\n            }),\n\n            'image' => SingleArraySchema::make()->callback(function (string $value) {\n                $this->seo->image($value);\n            }),\n\n            /*\n             * Nested item with key-value pairs.\n             *\n             *     $data = [\n             *         'twitter' => [\n             *             'card' => 'summary',\n             *             'creator' => '@romanzipp'\n             *         ]\n             *     ];\n             */\n\n            'twitter' => NestedArraySchema::make()->callback(function (string $attribute, string $value) {\n                $this->seo->twitter($attribute, $value);\n            }),\n\n            'og' => NestedArraySchema::make()->callback(function (string $attribute, string $value) {\n                $this->seo->og($attribute, $value);\n            }),\n\n            /*\n             * Item with attribute schema.\n             *\n             *     $data = [\n             *         'meta' => [\n             *             [\n             *                 'name' => 'copyright',\n             *                 'content' => 'Roman Zipp'\n             *             ],\n             *             [\n             *                 'name' => 'theme-color',\n             *                 'content' => 'red'\n             *             ]\n             *         ]\n             *     ];\n             */\n\n            'meta' => AttributeArraySchema::make(Structs\\Meta::class)->callback(function (Structs\\Meta $struct, array $attributes) {\n                $this->seo->add(\n                    $struct->attrs($attributes)\n                );\n            }),\n\n            'link' => AttributeArraySchema::make(Structs\\Link::class)->callback(function (Structs\\Link $struct, array $attributes) {\n                $this->seo->add(\n                    $struct->attrs($attributes)\n                );\n            }),\n        ];\n    }\n\n    /**\n     * Get a array schema based on index.\n     *\n     * @param string $index\n     *\n     * @return \\romanzipp\\Seo\\Conductors\\ArrayStructures\\AbstractArraySchema|null\n     */\n    private function getSchema(string $index): ?AbstractArraySchema\n    {\n        return $this->getSchemas()[$index] ?? null;\n    }\n\n    /**\n     * Set the array data and pass it to the seo service.\n     *\n     * @param array<string, mixed> $data\n     */\n    public function setData(array $data): void\n    {\n        foreach ($data as $key => $value) {\n            $schema = $this->getSchema($key);\n\n            if (null === $schema) {\n                throw new \\InvalidArgumentException(\"Unknown key {$key} provided for seo array format\");\n            }\n\n            $schema->apply($value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Conductors/ArrayStructures/AbstractArraySchema.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Conductors\\ArrayStructures;\n\nabstract class AbstractArraySchema\n{\n    /**\n     * @var string\n     */\n    protected $class;\n\n    /**\n     * @var \\Closure\n     */\n    protected $callback;\n\n    final public function __construct(?string $class = null)\n    {\n        $this->class = $class;\n    }\n\n    /**\n     * Create a new array schema instance.\n     *\n     * @param string|null $class\n     *\n     * @return static\n     */\n    public static function make(?string $class = null)\n    {\n        return new static($class);\n    }\n\n    /**\n     * Set the callback.\n     *\n     * @param \\Closure $callback\n     *\n     * @return static\n     */\n    public function callback(\\Closure $callback)\n    {\n        $this->callback = $callback;\n\n        return $this;\n    }\n\n    /**\n     * Get the callback.\n     *\n     * @return \\Closure\n     */\n    public function getCallback(): \\Closure\n    {\n        return $this->callback;\n    }\n\n    /**\n     * Call the callback with given parameters.\n     *\n     * @param mixed[] $parameters\n     */\n    protected function call(array $parameters): void\n    {\n        call_user_func(\n            $this->getCallback(),\n            ...$parameters\n        );\n    }\n\n    /**\n     * @param mixed $data\n     */\n    abstract public function apply($data): void;\n}\n"
  },
  {
    "path": "src/Conductors/ArrayStructures/AttributeArraySchema.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Conductors\\ArrayStructures;\n\nclass AttributeArraySchema extends AbstractArraySchema\n{\n    /**\n     * @param array<array<string>> $data\n     */\n    public function apply($data): void\n    {\n        if ( ! is_array($data)) {\n            throw new \\InvalidArgumentException('Invalid argument supplied for attribute array schema');\n        }\n\n        foreach ($data as $attributes) {\n            $this->call([\n                new $this->class(),\n                $attributes,\n            ]);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Conductors/ArrayStructures/NestedArraySchema.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Conductors\\ArrayStructures;\n\nclass NestedArraySchema extends AbstractArraySchema\n{\n    /**\n     * @param array<string, mixed> $data\n     */\n    public function apply($data): void\n    {\n        if ( ! is_array($data)) {\n            throw new \\InvalidArgumentException('Invalid argument supplied for nested array schema');\n        }\n\n        foreach ($data as $attribute => $value) {\n            $this->call([$attribute, $value]);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Conductors/ArrayStructures/SingleArraySchema.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Conductors\\ArrayStructures;\n\nclass SingleArraySchema extends AbstractArraySchema\n{\n    /**\n     * @param string $value\n     */\n    public function apply($value): void\n    {\n        if ( ! is_string($value)) {\n            throw new \\InvalidArgumentException('Invalid argument supplied for single array schema');\n        }\n\n        $this->call([\n            $value,\n        ]);\n    }\n}\n"
  },
  {
    "path": "src/Conductors/MixManifestConductor.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Conductors;\n\nuse romanzipp\\Seo\\Conductors\\Types\\ManifestAsset;\nuse romanzipp\\Seo\\Exceptions\\ManifestNotFoundException;\nuse romanzipp\\Seo\\Services\\SeoService;\nuse romanzipp\\Seo\\Structs\\Link;\n\nclass MixManifestConductor\n{\n    /**\n     * @var \\romanzipp\\Seo\\Services\\SeoService\n     */\n    private $seo;\n\n    /**\n     * @var string\n     */\n    private $path;\n\n    /**\n     * @var \\romanzipp\\Seo\\Conductors\\Types\\ManifestAsset[]\n     */\n    private $assets = [];\n\n    /**\n     * @var \\Closure|null\n     */\n    private $mapCallback;\n\n    /**\n     * @var bool\n     */\n    private $ignoreMissing = false;\n\n    /**\n     * MixManifestService constructor.\n     */\n    public function __construct(SeoService $seo)\n    {\n        $this->seo = $seo;\n        $this->path = public_path('mix-manifest.json');\n    }\n\n    /**\n     * @return string\n     */\n    public function getPath(): string\n    {\n        return $this->path;\n    }\n\n    /**\n     * @return \\romanzipp\\Seo\\Conductors\\Types\\ManifestAsset[]\n     */\n    public function getAssets(): array\n    {\n        return $this->assets;\n    }\n\n    /**\n     * Add a callback function which will be applied to every asset.\n     *\n     * @param \\Closure $callback\n     *\n     * @return \\romanzipp\\Seo\\Conductors\\MixManifestConductor\n     */\n    public function map(\\Closure $callback): self\n    {\n        $this->mapCallback = $callback;\n\n        return $this;\n    }\n\n    /**\n     * Do not throw exception if the mix manifest is not found.\n     *\n     * @return $this\n     */\n    public function ignoreMissing(): self\n    {\n        $this->ignoreMissing = true;\n\n        return $this;\n    }\n\n    /**\n     * Do not throw exception if the mix manifest is not found.\n     *\n     * @deprecated Use ignoreMissing() instead\n     *\n     * @return $this\n     */\n    public function ignore(): self\n    {\n        return $this->ignoreMissing();\n    }\n\n    /**\n     * @param string|null $path\n     *\n     * @throws \\romanzipp\\Seo\\Exceptions\\ManifestNotFoundException\n     *\n     * @return \\romanzipp\\Seo\\Conductors\\MixManifestConductor\n     */\n    public function load(?string $path = null): self\n    {\n        if (null !== $path) {\n            $this->path = $path;\n        }\n\n        $this->assets = $this->readContents();\n\n        if (null !== $this->mapCallback) {\n            $this->assets = array_map($this->mapCallback, $this->assets);\n        }\n\n        $this->assets = array_filter($this->assets);\n\n        foreach ($this->assets as $asset) {\n            $this->generateStruct($asset);\n        }\n\n        return $this;\n    }\n\n    /**\n     * @param \\romanzipp\\Seo\\Conductors\\Types\\ManifestAsset $asset\n     *\n     * @return void\n     */\n    private function generateStruct(ManifestAsset $asset): void\n    {\n        $link = Link::make()\n            ->rel($asset->rel)\n            ->href($asset->url);\n\n        if (null !== $asset->as) {\n            $link->as($asset->as);\n        }\n\n        if (null !== $asset->type) {\n            $link->type($asset->type);\n        }\n\n        $this->seo->add($link);\n    }\n\n    /**\n     * @throws \\romanzipp\\Seo\\Exceptions\\ManifestNotFoundException\n     *\n     * @return \\romanzipp\\Seo\\Conductors\\Types\\ManifestAsset[]\n     */\n    private function readContents(): array\n    {\n        $content = @file_get_contents($this->getPath());\n\n        if (false === $content) {\n            if ($this->ignoreMissing) {\n                return [];\n            }\n\n            throw new ManifestNotFoundException('The manifest file could not be found');\n        }\n\n        $data = @json_decode($content, true) ?? [];\n\n        return array_map(static function ($path, $url) {\n            return new ManifestAsset($path, $url);\n        }, array_keys($data), $data);\n    }\n}\n"
  },
  {
    "path": "src/Conductors/RenderConductor.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Conductors;\n\nuse Illuminate\\Contracts\\Support\\Arrayable;\nuse Illuminate\\Contracts\\Support\\Htmlable;\nuse Illuminate\\Contracts\\Support\\Renderable;\nuse Illuminate\\Support\\HtmlString;\nuse romanzipp\\Seo\\Builders\\StructBuilder;\nuse Spatie\\SchemaOrg\\Type;\n\nclass RenderConductor implements Htmlable, Renderable, Arrayable\n{\n    /**\n     * @var \\romanzipp\\Seo\\Structs\\Struct[]\n     */\n    private $structs;\n\n    /**\n     * @var \\Spatie\\SchemaOrg\\Type[]\n     */\n    private $schemes;\n\n    /**\n     * RenderConductor constructor.\n     *\n     * @param \\romanzipp\\Seo\\Structs\\Struct[] $structs\n     * @param \\Spatie\\SchemaOrg\\Type[] $schemes\n     */\n    public function __construct(array $structs, array $schemes)\n    {\n        $this->structs = $structs;\n        $this->schemes = $schemes;\n    }\n\n    /**\n     * Get all structs.\n     *\n     * @return \\romanzipp\\Seo\\Structs\\Struct[]\n     */\n    public function getStructs(): array\n    {\n        return $this->structs;\n    }\n\n    /**\n     * Get all structs.\n     *\n     * @return \\Spatie\\SchemaOrg\\Type[]\n     */\n    public function getSchemes(): array\n    {\n        return $this->schemes;\n    }\n\n    /**\n     * Build all applied structs.\n     *\n     * @return \\Illuminate\\Support\\HtmlString\n     */\n    public function build(): HtmlString\n    {\n        $contents = $this->toArray();\n\n        return new HtmlString(\n            implode(StructBuilder::$separator, $contents)\n        );\n    }\n\n    /**\n     * Get array of rendered html strings.\n     *\n     * @return string[]\n     */\n    public function toArray(): array\n    {\n        $structs = array_map(static function ($struct) {\n            return StructBuilder::build($struct)->toHtml();\n        }, $this->getStructs());\n\n        $schemas = array_map(static function (Type $schema) {\n            return $schema->toScript();\n        }, $this->getSchemes());\n\n        return array_values(\n            array_merge($structs, $schemas)\n        );\n    }\n\n    /**\n     * Get the evaluated contents of the object.\n     *\n     * @return string\n     */\n    public function render(): string\n    {\n        return (string) $this->build();\n    }\n\n    /**\n     * Get content as a string of HTML.\n     *\n     * @return string\n     */\n    public function toHtml(): string\n    {\n        return (string) $this->build();\n    }\n\n    /**\n     * Get content as a string of HTML.\n     *\n     * @return string\n     */\n    public function __toString(): string\n    {\n        return (string) $this->build();\n    }\n}\n"
  },
  {
    "path": "src/Conductors/Types/ManifestAsset.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Conductors\\Types;\n\nclass ManifestAsset\n{\n    /**\n     * @var string\n     */\n    public $path;\n\n    /**\n     * @var string\n     */\n    public $url;\n\n    /**\n     * @var string\n     */\n    public $rel = 'prefetch';\n\n    /**\n     * @var string|null\n     */\n    public $as;\n\n    /**\n     * @var mixed\n     */\n    public $type;\n\n    public function __construct(string $path, string $url)\n    {\n        $this->path = $path;\n        $this->url = $url;\n        $this->as = $this->guessResourceType($path);\n    }\n\n    private function guessResourceType(string $path): ?string\n    {\n        $extension = pathinfo($path, PATHINFO_EXTENSION);\n\n        if (empty($extension)) {\n            return null;\n        }\n\n        switch ($extension) {\n            case 'js':\n                return 'script';\n\n            case 'css':\n                return 'style';\n\n            case 'ttf':\n            case 'otf':\n                return 'font';\n\n            case 'jpg':\n            case 'jpeg':\n            case 'png':\n            case 'webp':\n                return 'image';\n\n            case 'mp4':\n                return 'video';\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/Enums/HookTarget.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Enums;\n\nclass HookTarget\n{\n    public const BODY = 0;\n    public const ATTRIBUTES = 1;\n    public const ATTRIBUTE = 2;\n}\n"
  },
  {
    "path": "src/Exceptions/ManifestNotFoundException.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Exceptions;\n\nclass ManifestNotFoundException extends \\Exception\n{\n}\n"
  },
  {
    "path": "src/Facades/Seo.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Facades;\n\nuse Illuminate\\Support\\Facades\\Facade;\nuse romanzipp\\Seo\\Services\\SeoService;\n\n/**\n * @method static void macro($name, $macro)\n * @method static void mixin($mixin, $replace = true)\n * @method static bool hasMacro($name)\n */\nclass Seo extends Facade\n{\n    protected static function getFacadeAccessor()\n    {\n        return SeoService::class;\n    }\n}\n"
  },
  {
    "path": "src/Helpers/Hook.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Helpers;\n\nuse romanzipp\\Seo\\Enums\\HookTarget;\n\nclass Hook\n{\n    /**\n     * Struct attribute to modify, defined in the\n     * \\romanzipp\\Seo\\Enums\\HookTarget enum.\n     *\n     * @var int\n     */\n    protected $target;\n\n    /**\n     * If HookTarget::ATTRIBUTE is used as target, this defines\n     * the attribute to be modified.\n     *\n     * @var mixed|null\n     */\n    protected $targetAttribute;\n\n    /**\n     * Filter the structs by certain attributes and values.\n     *\n     * @var array<string, mixed>\n     */\n    protected $filterAttributes = [];\n\n    /**\n     * Callback to be applied on the target.\n     *\n     * @var callable\n     */\n    protected $callback;\n\n    /**\n     * Weather the current hook callback has been executed.\n     *\n     * @var bool\n     */\n    protected $executed = false;\n\n    /**\n     * Create new Hook instance.\n     *\n     * @return self\n     */\n    public static function make(): self\n    {\n        return new self();\n    }\n\n    /*\n     *--------------------------------------------------------------------------\n     * Getters\n     *--------------------------------------------------------------------------\n     */\n\n    /**\n     * Get the specified hook target defined in \\romanzipp\\Seo\\Enums\\HookTarget.\n     *\n     * @return int \\romanzipp\\Seo\\Enums\\HookTarget enum value\n     */\n    public function getTarget(): int\n    {\n        return $this->target;\n    }\n\n    /**\n     * Get the specified hook target enum (attribute, attributes, body).\n     *\n     * @return mixed\n     */\n    public function getTargetAttribute()\n    {\n        return $this->targetAttribute;\n    }\n\n    /**\n     * Get specified attribute to filter for the hook.\n     *\n     * @return array<string, mixed>\n     */\n    public function getFilterAttributes(): array\n    {\n        return $this->filterAttributes;\n    }\n\n    /**\n     * Get the callback to be applied.\n     *\n     * @return callable\n     */\n    public function getCallback(): callable\n    {\n        return $this->callback;\n    }\n\n    /*\n     *--------------------------------------------------------------------------\n     * Setters\n     *--------------------------------------------------------------------------\n     */\n\n    /**\n     * Set hook target to body.\n     *\n     * @return $this\n     */\n    public function onBody(): self\n    {\n        $this->target = HookTarget::BODY;\n\n        return $this;\n    }\n\n    /**\n     * Set hook target on attributes.\n     *\n     * @return $this\n     */\n    public function onAttributes(): self\n    {\n        $this->target = HookTarget::ATTRIBUTES;\n\n        return $this;\n    }\n\n    /**\n     * Set hook target on specified attribute.\n     *\n     * @param string $attribute Struct attribute\n     *\n     * @return $this\n     */\n    public function onAttribute(string $attribute): self\n    {\n        $this->target = HookTarget::ATTRIBUTE;\n\n        $this->targetAttribute = $attribute;\n\n        return $this;\n    }\n\n    /**\n     * Add a hook attribute filter.\n     *\n     * @param string $attribute Attribute to search for\n     * @param mixed $value Attribute value to search for\n     *\n     * @return $this\n     */\n    public function whereAttribute(string $attribute, $value): self\n    {\n        $this->filterAttributes[$attribute] = $value;\n\n        return $this;\n    }\n\n    /**\n     * Set the callback to be applied.\n     *\n     * @param callable $callback Callback\n     *\n     * @return $this\n     */\n    public function callback(callable $callback): self\n    {\n        $this->callback = $callback;\n\n        return $this;\n    }\n\n    /**\n     * Set executed state.\n     *\n     * @param bool $status State\n     *\n     * @return \\romanzipp\\Seo\\Helpers\\Hook\n     */\n    public function setExecuted(bool $status): self\n    {\n        $this->executed = $status;\n\n        return $this;\n    }\n\n    /*\n     *--------------------------------------------------------------------------\n     * Methods\n     *--------------------------------------------------------------------------\n     */\n\n    /**\n     * Modify the data that will be handed over to the\n     * hook callback as parameter.\n     *\n     * @param mixed $data\n     *\n     * @return mixed\n     */\n    public function translateCallbackData($data)\n    {\n        switch ($this->target) {\n            case HookTarget::BODY:\n                return $data->data();\n\n            case HookTarget::ATTRIBUTE:\n                return array_values($data)[0]->data();\n\n            case HookTarget::ATTRIBUTES:\n                return array_map(static function ($value) {\n                    return $value->data();\n                }, $data);\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/Providers/SeoServiceProvider.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Providers;\n\nuse Illuminate\\Contracts\\Foundation\\Application;\nuse Illuminate\\Support\\ServiceProvider;\nuse romanzipp\\Seo\\Collections\\SchemaCollection;\nuse romanzipp\\Seo\\Collections\\StructCollection;\nuse romanzipp\\Seo\\Services\\SeoService;\n\nclass SeoServiceProvider extends ServiceProvider\n{\n    /**\n     * Bootstrap the application services.\n     *\n     * @return void\n     */\n    public function boot()\n    {\n        $this->publishes([\n            dirname(__DIR__) . '/../config/seo.php' => config_path('seo.php'),\n        ], 'config');\n    }\n\n    /**\n     * Register the application services.\n     *\n     * @return void\n     */\n    public function register()\n    {\n        $this->mergeConfigFrom(\n            dirname(__DIR__) . '/../config/seo.php',\n            'seo'\n        );\n\n        $this->app->singleton(StructCollection::class, function (Application $app) {\n            return new StructCollection();\n        });\n\n        $this->app->singleton(SchemaCollection::class, function (Application $app) {\n            return new SchemaCollection();\n        });\n\n        $this->app->bind(SeoService::class, function (Application $app) {\n            return new SeoService(\n                $app->make(StructCollection::class),\n                $app->make(SchemaCollection::class)\n            );\n        });\n    }\n\n    /**\n     * Get the services provided by the provider.\n     *\n     * @return string[]\n     */\n    public function provides()\n    {\n        return [SeoService::class];\n    }\n}\n"
  },
  {
    "path": "src/Schema/Schema.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Schema;\n\nuse romanzipp\\Seo\\Structs\\Struct;\nuse Spatie\\SchemaOrg\\Type;\n\nfinal class Schema\n{\n    /**\n     * @var \\Spatie\\SchemaOrg\\Type\n     */\n    private $type;\n\n    /**\n     * @var string\n     */\n    private $section;\n\n    public function __construct(Type $type)\n    {\n        $this->type = $type;\n    }\n\n    /**\n     * Get the schema type.\n     *\n     * @return \\Spatie\\SchemaOrg\\Type\n     */\n    public function getType(): Type\n    {\n        return $this->type;\n    }\n\n    /**\n     * Get the section in which the struct should rest. Default: \"default\".\n     *\n     * @return string\n     */\n    public function getSection(): string\n    {\n        return $this->section;\n    }\n\n    /**\n     * Set the section. This is mainly done in the SeoService class.\n     *\n     * @param string $section\n     *\n     * @return $this\n     */\n    public function setSection(string $section): self\n    {\n        $this->section = $section;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Services/SeoService.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Services;\n\nuse Illuminate\\Support\\Traits\\Macroable;\nuse romanzipp\\Seo\\Collections\\SchemaCollection;\nuse romanzipp\\Seo\\Collections\\StructCollection;\nuse romanzipp\\Seo\\Conductors\\ArrayFormatConductor;\nuse romanzipp\\Seo\\Conductors\\MixManifestConductor;\nuse romanzipp\\Seo\\Conductors\\RenderConductor;\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse romanzipp\\Seo\\Services\\Traits\\CollisionTrait;\nuse romanzipp\\Seo\\Services\\Traits\\SchemaOrgTrait;\nuse romanzipp\\Seo\\Services\\Traits\\ShorthandSetterTrait;\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass SeoService\n{\n    use CollisionTrait;\n    use Macroable;\n    use SchemaOrgTrait;\n    use ShorthandSetterTrait;\n\n    /**\n     * Config.\n     *\n     * @var array<string, mixed>\n     */\n    protected $config;\n\n    /**\n     * The section used to add new structs and retrieve existing structs.\n     * All structs for all sections will be added to the same service instance.\n     *\n     * @var string\n     */\n    protected $section = 'default';\n\n    /**\n     * Applied schema.org schemes.\n     *\n     * @var \\romanzipp\\Seo\\Collections\\SchemaCollection\n     */\n    protected $schemaCollection;\n\n    /**\n     * @var \\romanzipp\\Seo\\Collections\\StructCollection\n     */\n    protected $structCollection;\n\n    /**\n     * Constructor.\n     *\n     * @param \\romanzipp\\Seo\\Collections\\StructCollection $structCollection\n     * @param \\romanzipp\\Seo\\Collections\\SchemaCollection $schemaCollection\n     */\n    public function __construct(StructCollection $structCollection, SchemaCollection $schemaCollection)\n    {\n        $this->structCollection = $structCollection;\n        $this->schemaCollection = $schemaCollection;\n        $this->config = config('seo');\n    }\n\n    /**\n     * Create service instance.\n     *\n     * @return self\n     */\n    public static function make(): self\n    {\n        return app(self::class);\n    }\n\n    /**\n     * Get config.\n     *\n     * @return array<string, mixed>\n     */\n    public function getConfig(): array\n    {\n        return $this->config;\n    }\n\n    /**\n     * Fluent section setter.\n     *\n     * @param string $section\n     *\n     * @return $this\n     */\n    public function section(string $section): self\n    {\n        $this->section = $section;\n\n        return $this;\n    }\n\n    /**\n     * Get structs.\n     *\n     * @return \\romanzipp\\Seo\\Structs\\Struct[]\n     */\n    public function getStructs(): array\n    {\n        return array_filter($this->structCollection->all(), function (Struct $struct): bool {\n            return $struct->getSection() === $this->section;\n        });\n    }\n\n    /**\n     * Get Struct by class.\n     *\n     * @param string $class\n     *\n     * @return \\romanzipp\\Seo\\Structs\\Struct|null\n     */\n    public function getStruct(string $class): ?Struct\n    {\n        foreach ($this->getStructs() as $struct) {\n            if (get_class($struct) !== $class) {\n                continue;\n            }\n\n            return $struct;\n        }\n\n        return null;\n    }\n\n    /**\n     * Set structs.\n     *\n     * @param \\romanzipp\\Seo\\Structs\\Struct[] $structCollection\n     */\n    public function setStructCollection(array $structCollection): void\n    {\n        $this->clearStructs();\n\n        foreach ($structCollection as $struct) {\n            $this->appendStruct($struct);\n        }\n    }\n\n    /**\n     * Remove a struct from the collection by given array index.\n     *\n     * @param int $index\n     */\n    public function unsetStruct(int $index): void\n    {\n        $this->structCollection->unset($index);\n    }\n\n    /**\n     * Removes all structs from service instance.\n     *\n     * @return void\n     */\n    public function clearStructs(): void\n    {\n        $this->structCollection->set([]);\n    }\n\n    /**\n     * Append a given struct. This is an internal method called by all add/set public methods\n     * which also sets the current section to the struct.\n     *\n     * @param \\romanzipp\\Seo\\Structs\\Struct $struct\n     */\n    public function appendStruct(Struct $struct): void\n    {\n        $struct->setSection($this->section);\n\n        $this->structCollection->add($struct);\n    }\n\n    /**\n     * Add struct.\n     *\n     * @param Struct $struct\n     *\n     * @return $this\n     */\n    public function add(Struct $struct): self\n    {\n        $this->removeDuplicateStruct($struct);\n\n        $this->appendStruct($struct);\n\n        return $this;\n    }\n\n    /**\n     * Add a given Struct if the given condition is true.\n     *\n     * @param bool $boolean\n     * @param Struct $struct\n     *\n     * @return $this\n     */\n    public function addIf(bool $boolean, Struct $struct): self\n    {\n        if ($boolean) {\n            $this->add($struct);\n        }\n\n        return $this;\n    }\n\n    /**\n     * Add many structs.\n     *\n     * @param \\romanzipp\\Seo\\Structs\\Struct[] $structs\n     *\n     * @return $this\n     */\n    public function addMany(array $structs): self\n    {\n        foreach ($structs as $struct) {\n            $this->add($struct);\n        }\n\n        return $this;\n    }\n\n    /**\n     * Add structs from array format.\n     *\n     * @param array<string, mixed> $data\n     *\n     * @return $this\n     */\n    public function addFromArray(array $data): self\n    {\n        $this->arrayFormat()->setData($data);\n\n        return $this;\n    }\n\n    /**\n     * Add hook to given struct class. This is just an\n     * alias for the Struct::hook() method.\n     *\n     * @param string $structClass\n     * @param \\romanzipp\\Seo\\Helpers\\Hook $hook\n     *\n     * @return void\n     */\n    public function hook(string $structClass, Hook $hook): void\n    {\n        app($structClass)::hook($hook);\n    }\n\n    /**\n     * @return \\romanzipp\\Seo\\Conductors\\MixManifestConductor\n     */\n    public function mix(): MixManifestConductor\n    {\n        return new MixManifestConductor($this);\n    }\n\n    /**\n     * @return \\romanzipp\\Seo\\Conductors\\RenderConductor\n     */\n    public function render(): RenderConductor\n    {\n        return new RenderConductor(\n            $this->getStructs(),\n            $this->getSchemes()\n        );\n    }\n\n    /**\n     * @return \\romanzipp\\Seo\\Conductors\\ArrayFormatConductor\n     */\n    public function arrayFormat(): ArrayFormatConductor\n    {\n        return new ArrayFormatConductor($this);\n    }\n}\n"
  },
  {
    "path": "src/Services/Traits/CollisionTrait.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Services\\Traits;\n\nuse romanzipp\\Seo\\Structs\\Struct;\n\ntrait CollisionTrait\n{\n    abstract public function getStructs(): array;\n\n    abstract public function unsetStruct(int $index): void;\n\n    /**\n     * Remove struct from existing structs.\n     *\n     * @param \\romanzipp\\Seo\\Structs\\Struct $struct\n     *\n     * @return void\n     */\n    public function removeDuplicateStruct(Struct $struct): void\n    {\n        if ( ! $result = $this->getDuplicateStruct($struct)) {\n            return;\n        }\n\n        [$existing, $key] = $result;\n\n        if (null === $existing || null === $key) {\n            return;\n        }\n\n        $this->unsetStruct($key);\n    }\n\n    /**\n     * Get matching struct duplicate.\n     *\n     * @param \\romanzipp\\Seo\\Structs\\Struct $struct\n     *\n     * @return (\\romanzipp\\Seo\\Structs\\Struct|int|null)[]|null\n     */\n    public function getDuplicateStruct(Struct $struct): ?array\n    {\n        if (false === $struct->isUnique()) {\n            return null;\n        }\n\n        foreach ($this->getStructs() as $key => $existing) {\n            /** @var \\romanzipp\\Seo\\Structs\\Struct $existing */\n            if (get_class($existing) !== get_class($struct)) {\n                continue;\n            }\n\n            if (empty($existing->getUniqueAttributes())) {\n                return [$existing, $key];\n            }\n\n            $diff = array_diff(\n                $existing->getComputedUniqueAttributes(),\n                $struct->getComputedUniqueAttributes()\n            );\n\n            if (empty($diff)) {\n                return [$existing, $key];\n            }\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/Services/Traits/SchemaOrgTrait.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Services\\Traits;\n\nuse Illuminate\\Support\\Arr;\nuse romanzipp\\Seo\\Schema\\Schema as SchemaContainer;\nuse Spatie\\SchemaOrg\\Schema;\nuse Spatie\\SchemaOrg\\Type;\n\ntrait SchemaOrgTrait\n{\n    /**\n     * Get spatie/schema-org types.\n     *\n     * @return \\Spatie\\SchemaOrg\\Type[]\n     */\n    public function getSchemes(): array\n    {\n        return array_values(\n            array_map(\n                static function (SchemaContainer $container): Type {\n                    return $container->getType();\n                },\n                array_filter(\n                    $this->schemaCollection->all(),\n                    function (SchemaContainer $container): bool {\n                        return $container->getSection() === $this->section;\n                    }\n                )\n            )\n        );\n    }\n\n    /**\n     * Add spatie/schema-org object.\n     *\n     * @param Type $schema schema.org Type\n     *\n     * @return $this\n     */\n    public function addSchema(Type $schema): self\n    {\n        $container = new SchemaContainer($schema);\n        $container->setSection($this->section);\n\n        $this->schemaCollection->add($container);\n\n        return $this;\n    }\n\n    /**\n     * Set array of spatie/schema-org objects.\n     *\n     * @param \\Spatie\\SchemaOrg\\Type[] $schemes\n     *\n     * @return $this\n     */\n    public function setSchemes(array $schemes): self\n    {\n        $containers = [];\n\n        foreach ($schemes as $schema) {\n            $container = new SchemaContainer($schema);\n            $container->setSection($this->section);\n\n            $containers[] = $container;\n        }\n\n        $this->schemaCollection->set($containers);\n\n        return $this;\n    }\n\n    /**\n     * Add a list of breadcrumbs.\n     *\n     * @param array<array<string, string>> $crumbs\n     *\n     * @return $this\n     */\n    public function addSchemaBreadcrumbs(array $crumbs): self\n    {\n        $itemListElement = [];\n\n        foreach ($crumbs as $key => $crumb) {\n            $itemListElement[] = Schema::listItem()\n                ->position($key + 1)\n                ->name(\n                    Arr::get($crumb, 'name')\n                )\n                ->item(\n                    Arr::get($crumb, 'item')\n                );\n        }\n\n        $this->addSchema(\n            Schema::breadcrumbList()\n                ->itemListElement($itemListElement)\n        );\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Services/Traits/ShorthandSetterTrait.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Services\\Traits;\n\nuse Illuminate\\Support\\Arr;\nuse romanzipp\\Seo\\Services\\SeoService;\nuse romanzipp\\Seo\\Structs\\Link\\Canonical;\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Meta\\Charset;\nuse romanzipp\\Seo\\Structs\\Meta\\CsrfToken;\nuse romanzipp\\Seo\\Structs\\Meta\\Description;\nuse romanzipp\\Seo\\Structs\\Meta\\EmbedX;\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\nuse romanzipp\\Seo\\Structs\\Meta\\Twitter;\nuse romanzipp\\Seo\\Structs\\Meta\\Viewport;\nuse romanzipp\\Seo\\Structs\\Struct;\nuse romanzipp\\Seo\\Structs\\Title;\n\ntrait ShorthandSetterTrait\n{\n    /**\n     * Add title.\n     *\n     * @param string|null $title\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function title(?string $title = null, bool $escape = true): self\n    {\n        $config = Arr::get($this->config, 'shorthand.title');\n\n        $this->addIf(\n            $config['tag'] ?? true,\n            Title::make()->body($title, $escape)\n        );\n\n        $this->addIf(\n            $config['opengraph'] ?? true,\n            OpenGraph::make()->property('title')->content($title, $escape)\n        );\n\n        $this->addIf(\n            $config['twitter'] ?? true,\n            Twitter::make()->name('title')->content($title, $escape)\n        );\n\n        $this->addIf(\n            $config['embedx'] ?? true,\n            EmbedX::make()->name('title')->content($title, $escape)\n        );\n\n        return $this;\n    }\n\n    /**\n     * Add description.\n     *\n     * @param string|null $description\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function description(?string $description = null, bool $escape = true): self\n    {\n        $config = Arr::get($this->config, 'shorthand.description');\n\n        $this->addIf(\n            $config['meta'] ?? true,\n            Description::make()->content($description, $escape)\n        );\n\n        $this->addIf(\n            $config['opengraph'] ?? true,\n            OpenGraph::make()->property('description')->content($description, $escape)\n        );\n\n        $this->addIf(\n            $config['twitter'] ?? true,\n            Twitter::make()->name('description')->content($description, $escape)\n        );\n\n        $this->addIf(\n            $config['embedx'] ?? true,\n            EmbedX::make()->name('description')->content($description, $escape)\n        );\n\n        return $this;\n    }\n\n    /**\n     * Add image.\n     *\n     * @param string|null $image\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function image(?string $image = null, bool $escape = true): self\n    {\n        $config = Arr::get($this->config, 'shorthand.image');\n\n        $this->addIf(\n            $config['meta'] ?? true,\n            Meta::make()->name('image')->content($image, $escape)\n        );\n\n        $this->addIf(\n            $config['opengraph'] ?? true,\n            OpenGraph::make()->property('image')->content($image, $escape)\n        );\n\n        $this->addIf(\n            $config['twitter'] ?? true,\n            Twitter::make()->name('image')->content($image, $escape)\n        );\n\n        $this->addIf(\n            $config['embedx'] ?? true,\n            EmbedX::make()->name('image')->content($image, $escape)\n        );\n\n        return $this;\n    }\n\n    /**\n     * Add name-content Meta struct.\n     *\n     * @param string $name\n     * @param mixed|null $content\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function meta(string $name, $content = null, bool $escape = true): self\n    {\n        return $this->add(\n            Meta::make()->name($name)->content($content, $escape)\n        );\n    }\n\n    /**\n     * Add Twitter struct.\n     *\n     * @param string $name\n     * @param mixed|null $content\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function twitter(string $name, $content = null, bool $escape = true): self\n    {\n        return $this->add(\n            Twitter::make()->name($name)->content($content, $escape)\n        );\n    }\n\n    /**\n     * Add OpenGraph struct.\n     *\n     * @param string $property\n     * @param mixed|null $content\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function og(string $property, $content = null, bool $escape = true): self\n    {\n        return $this->add(\n            OpenGraph::make()->property($property)->content($content, $escape)\n        );\n    }\n\n    /**\n     * Add EmbedX struct.\n     *\n     * @see https://embedx.app\n     *\n     * @param string $property\n     * @param mixed|null $content\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function embedx(string $property, $content = null, bool $escape = true): self\n    {\n        return $this->add(\n            EmbedX::make()->name($property)->content($content, $escape)\n        );\n    }\n\n    /**\n     * Add the meta charset struct.\n     *\n     * @param string $charset\n     *\n     * @return $this\n     */\n    public function charset(string $charset = 'utf-8'): self\n    {\n        return $this->add(\n            Charset::make()->charset($charset)\n        );\n    }\n\n    /**\n     * Add the meta viewport struct.\n     *\n     * @param string $viewport\n     *\n     * @return $this\n     */\n    public function viewport(string $viewport = 'width=device-width, initial-scale=1'): self\n    {\n        return $this->add(\n            Viewport::make()->content($viewport)\n        );\n    }\n\n    /**\n     * Add the canonical struct.\n     *\n     * @param string $canonical\n     *\n     * @return $this\n     */\n    public function canonical(string $canonical): self\n    {\n        return $this->add(\n            Canonical::make()->href($canonical)\n        );\n    }\n\n    /**\n     * Add the CSRF token meta struct.\n     *\n     * @param string|null $token\n     *\n     * @return $this\n     */\n    public function csrfToken(?string $token = null): self\n    {\n        return $this->add(\n            CsrfToken::make()->token($token ?? csrf_token())\n        );\n    }\n\n    abstract public function add(Struct $struct): SeoService;\n\n    abstract public function addIf(bool $boolean, Struct $struct): SeoService;\n}\n"
  },
  {
    "path": "src/Structs/Base.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#elements\n */\nclass Base extends Struct\n{\n    protected $unique = true;\n\n    protected function tag(): string\n    {\n        return 'base';\n    }\n}\n"
  },
  {
    "path": "src/Structs/Link/Canonical.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Link;\n\nuse romanzipp\\Seo\\Structs\\Link;\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass Canonical extends Link\n{\n    protected $unique = true;\n\n    public static function defaults(Struct $struct): void\n    {\n        $struct->addAttribute('rel', 'canonical');\n    }\n}\n"
  },
  {
    "path": "src/Structs/Link.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#link\n */\nclass Link extends Struct\n{\n    protected $unique = false;\n\n    protected function tag(): string\n    {\n        return 'link';\n    }\n\n    /**\n     * @param mixed|null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function rel($value = null, bool $escape = true)\n    {\n        $this->addAttribute('rel', $value, $escape);\n\n        return $this;\n    }\n\n    /**\n     * @param mixed|null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function href($value = null, bool $escape = true)\n    {\n        $this->addAttribute('href', $value, $escape);\n\n        return $this;\n    }\n\n    /**\n     * @param mixed|null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function as($value = null, bool $escape = true)\n    {\n        $this->addAttribute('as', $value, $escape);\n\n        return $this;\n    }\n\n    /**\n     * @param mixed|null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function type($value = null, bool $escape = true)\n    {\n        $this->addAttribute('type', $value, $escape);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Structs/Meta/AppLink.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#app-links\n */\nclass AppLink extends Meta\n{\n    /**\n     * @param mixed $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function property($value, bool $escape = true)\n    {\n        $this->addAttribute('property', \"al:{$value}\", $escape);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Structs/Meta/Article.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#facebook-open-graph\n */\nclass Article extends Meta\n{\n    protected $unique = true;\n\n    protected $uniqueAttributes = ['property'];\n\n    /**\n     * @param mixed|null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function property($value = null, bool $escape = true)\n    {\n        $this->addAttribute('property', \"article:{$value}\", $escape);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Structs/Meta/Charset.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Struct;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#recommended-minimum\n */\nclass Charset extends Meta\n{\n    protected $unique = true;\n\n    public static function defaults(Struct $struct): void\n    {\n        $struct->addAttribute('charset', 'utf-8');\n    }\n\n    /**\n     * @param mixed|null $charset\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function charset($charset = null, bool $escape = true)\n    {\n        $this->addAttribute('charset', $charset, $escape);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Structs/Meta/CsrfToken.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Struct;\n\n/**\n * @see  https://laravel.com/docs/csrf#csrf-x-csrf-token\n */\nclass CsrfToken extends Meta\n{\n    protected $unique = true;\n\n    public static function defaults(Struct $struct): void\n    {\n        $struct->addAttribute('name', 'csrf-token');\n    }\n\n    /**\n     * @param mixed|null $token\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function token($token = null, bool $escape = true)\n    {\n        $this->addAttribute('content', $token, $escape);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Structs/Meta/Description.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Struct;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#recommended-minimum\n */\nclass Description extends Meta\n{\n    protected $unique = true;\n\n    public static function defaults(Struct $struct): void\n    {\n        $struct->addAttribute('name', 'description');\n    }\n}\n"
  },
  {
    "path": "src/Structs/Meta/EmbedX.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#twitter-card\n * @see https://embedx.app\n */\nclass EmbedX extends Meta\n{\n    protected $unique = true;\n\n    protected $uniqueAttributes = ['name'];\n\n    /**\n     * @param mixed|null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function name($value = null, bool $escape = true)\n    {\n        $this->addAttribute('name', \"embedx:{$value}\", $escape);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Structs/Meta/OpenGraph.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#facebook-open-graph\n */\nclass OpenGraph extends Meta\n{\n    protected $unique = true;\n\n    protected $uniqueAttributes = ['property'];\n\n    /**\n     * @param mixed|null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function property($value = null, bool $escape = true)\n    {\n        $this->addAttribute('property', \"og:{$value}\", $escape);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Structs/Meta/Robots.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass Robots extends Meta\n{\n    protected $unique = true;\n\n    public static function defaults(Struct $struct): void\n    {\n        $struct->addAttribute('name', 'robots');\n    }\n}\n"
  },
  {
    "path": "src/Structs/Meta/Twitter.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#twitter-card\n */\nclass Twitter extends Meta\n{\n    protected $unique = true;\n\n    protected $uniqueAttributes = ['name'];\n\n    /**\n     * @param mixed|null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function name($value = null, bool $escape = true)\n    {\n        $this->addAttribute('name', \"twitter:{$value}\", $escape);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Structs/Meta/Viewport.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Meta;\n\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Struct;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#recommended-minimum\n */\nclass Viewport extends Meta\n{\n    protected $unique = true;\n\n    public static function defaults(Struct $struct): void\n    {\n        $struct->addAttribute('name', 'viewport');\n    }\n}\n"
  },
  {
    "path": "src/Structs/Meta.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#meta\n */\nclass Meta extends Struct\n{\n    protected function tag(): string\n    {\n        return 'meta';\n    }\n\n    /**\n     * @param mixed|null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function name($value = null, bool $escape = true)\n    {\n        $this->addAttribute('name', $value, $escape);\n\n        return $this;\n    }\n\n    /**\n     * @param mixed|null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function httpEquiv($value = null, bool $escape = true)\n    {\n        $this->addAttribute('http-equiv', $value, $escape);\n\n        return $this;\n    }\n\n    /**\n     * @param mixed|null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function content($value = null, bool $escape = true)\n    {\n        $this->addAttribute('content', $value, $escape);\n\n        return $this;\n    }\n\n    /**\n     * @param mixed $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function value($value, bool $escape = true)\n    {\n        $this->addAttribute('value', $value, $escape);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Structs/Noscript.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#elements\n */\nclass Noscript extends Struct\n{\n    protected function tag(): string\n    {\n        return 'noscript';\n    }\n}\n"
  },
  {
    "path": "src/Structs/Script.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#elements\n */\nclass Script extends Struct\n{\n    protected function tag(): string\n    {\n        return 'script';\n    }\n\n    /**\n     * @param null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function src($value = null, bool $escape = true)\n    {\n        $this->addAttribute('src', $value, $escape);\n\n        return $this;\n    }\n\n    /**\n     * @param null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function type($value = null, bool $escape = true)\n    {\n        $this->addAttribute('type', $value, $escape);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Structs/Struct.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\nuse romanzipp\\Seo\\Enums\\HookTarget;\nuse romanzipp\\Seo\\Structs\\Traits\\HookableTrait;\nuse romanzipp\\Seo\\Values\\Attribute;\nuse romanzipp\\Seo\\Values\\Body;\n\nabstract class Struct\n{\n    use HookableTrait;\n\n    /**\n     * Can the website <head> contain more\n     * than one element of this type.\n     *\n     * @var bool\n     */\n    protected $unique = false;\n\n    /**\n     * Attribute names which should be unique across\n     * all existing elements combined with the struct tag.\n     *\n     * @var string[]\n     */\n    protected $uniqueAttributes = [];\n\n    /**\n     * Attributes.\n     *\n     * @var array<string, \\romanzipp\\Seo\\Values\\Attribute>\n     */\n    protected $attributes = [];\n\n    /**\n     * Struct body.\n     *\n     * @var \\romanzipp\\Seo\\Values\\Body|null\n     */\n    protected $body;\n\n    /**\n     * @var string\n     */\n    protected $section;\n\n    /**\n     * Constructor.\n     */\n    final public function __construct()\n    {\n        static::defaults($this);\n    }\n\n    /**\n     * Create struct instance.\n     *\n     * @return static\n     */\n    public static function make()\n    {\n        return new static();\n    }\n\n    /**\n     * Modify struct after creation.\n     *\n     * @param self $struct\n     */\n    public static function defaults(self $struct): void\n    {\n    }\n\n    /*\n     *--------------------------------------------------------------------------\n     * Getters\n     *--------------------------------------------------------------------------\n     */\n\n    /**\n     * Get struct tag.\n     *\n     * @return string\n     */\n    public function getTag(): string\n    {\n        return $this->tag();\n    }\n\n    /**\n     * Get struct body.\n     *\n     * @return \\romanzipp\\Seo\\Values\\Body|null\n     */\n    public function getBody(): ?Body\n    {\n        return $this->body;\n    }\n\n    /**\n     * Get struct attributes.\n     *\n     * @return array<string, \\romanzipp\\Seo\\Values\\Attribute>\n     */\n    public function getAttributes(): array\n    {\n        return $this->attributes;\n    }\n\n    /**\n     * Get computed attributes. Converting objects to string values.\n     *\n     * @return array<string, \\romanzipp\\Seo\\Values\\Attribute>\n     */\n    public function getComputedAttributes(): array\n    {\n        return $this->getAttributes();\n    }\n\n    /**\n     * Get computed single attribute.\n     *\n     * @param string $attribute\n     *\n     * @return \\romanzipp\\Seo\\Values\\Attribute|null\n     */\n    public function getComputedAttribute(string $attribute): ?Attribute\n    {\n        return $this->getComputedAttributes()[$attribute] ?? null;\n    }\n\n    /**\n     * Get struct unique attributes for collision detection.\n     *\n     * @return string[]\n     */\n    public function getUniqueAttributes(): array\n    {\n        return $this->uniqueAttributes;\n    }\n\n    /**\n     * Get all attributes with values that have been declared as unique.\n     *\n     * @return \\romanzipp\\Seo\\Values\\Attribute[]\n     */\n    public function getComputedUniqueAttributes(): array\n    {\n        return array_filter($this->getAttributes(), function ($value, $key) {\n            return in_array($key, $this->getUniqueAttributes(), false);\n        }, ARRAY_FILTER_USE_BOTH);\n    }\n\n    /**\n     * Is struct unique.\n     *\n     * @return bool\n     */\n    public function isUnique(): bool\n    {\n        return $this->unique;\n    }\n\n    /**\n     * Set the unique-flag.\n     *\n     * @param bool $unique\n     *\n     * @return $this\n     */\n    public function setUnique(bool $unique = true)\n    {\n        $this->unique = $unique;\n\n        return $this;\n    }\n\n    /**\n     * Get the section in which the struct should rest. Default: \"default\".\n     *\n     * @return string\n     */\n    public function getSection(): string\n    {\n        return $this->section;\n    }\n\n    /**\n     * Set the section. This is mainly done in the SeoService class.\n     *\n     * @param string $section\n     *\n     * @return static\n     */\n    public function setSection(string $section)\n    {\n        $this->section = $section;\n\n        return $this;\n    }\n\n    /**\n     * Determines if struct is void element.\n     *\n     * @see  https://www.w3.org/TR/html/syntax.html#void-elements\n     *\n     * @return bool\n     */\n    public function isVoidElement(): bool\n    {\n        return in_array($this->getTag(), [\n            'area',\n            'base',\n            'br',\n            'col',\n            'embed',\n            'hr',\n            'img',\n            'input',\n            'link',\n            'meta',\n            'param',\n            'source',\n            'track',\n            'wbr',\n        ]);\n    }\n\n    /*\n     *--------------------------------------------------------------------------\n     * Setters\n     *--------------------------------------------------------------------------\n     */\n\n    /**\n     * Fluid body setter.\n     *\n     * @param mixed $body\n     * @param bool $escape Escape body\n     *\n     * @return $this\n     */\n    public function body($body, bool $escape = true)\n    {\n        if ($escape) {\n            $body = $this->escapeValue($body);\n        }\n\n        $this->setBody($body);\n\n        return $this;\n    }\n\n    /**\n     * Fluid attributes setter.\n     *\n     * @param string $attribute\n     * @param mixed|null $value\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function attr(string $attribute, $value = null, bool $escape = true)\n    {\n        $this->addAttribute($attribute, $value, $escape);\n\n        return $this;\n    }\n\n    /**\n     * Fluid setter for multiple attributes.\n     *\n     * @param array<string, mixed> $attributes\n     * @param bool $escape\n     *\n     * @return $this\n     */\n    public function attrs(array $attributes, bool $escape = true)\n    {\n        foreach ($attributes as $attribute => $value) {\n            $this->attr($attribute, $value, $escape);\n        }\n\n        return $this;\n    }\n\n    /**\n     * Set body.\n     *\n     * @param mixed $body\n     */\n    protected function setBody($body): void\n    {\n        $this->body = new Body($body);\n\n        $this->triggerHook(HookTarget::BODY, $this->body);\n    }\n\n    /**\n     * Add attribute.\n     *\n     * @param string $key\n     * @param mixed $value\n     * @param bool $escape\n     */\n    protected function addAttribute(string $key, $value, bool $escape = true): void\n    {\n        if ($escape) {\n            $value = $this->escapeValue($value);\n        }\n\n        $this->attributes[$key] = new Attribute($value);\n\n        $this->triggerHook(HookTarget::ATTRIBUTE, [$key => $this->attributes[$key]]);\n\n        $this->triggerHook(HookTarget::ATTRIBUTES, $this->attributes);\n    }\n\n    /**\n     * Set attributes.\n     *\n     * @param array<string, mixed> $attributes\n     */\n    protected function setAttributes(array $attributes): void\n    {\n        foreach ($attributes as $key => $value) {\n            $this->addAttribute($key, $value);\n        }\n    }\n\n    /**\n     * Escape attribute value.\n     *\n     * @param mixed $value\n     *\n     * @return string|null\n     */\n    protected function escapeValue($value): ?string\n    {\n        switch (gettype($value)) {\n            case 'NULL':\n                return null;\n\n            case 'integer':\n                return (string) $value;\n\n            case 'boolean':\n                return true === $value ? '1' : '0';\n        }\n\n        $value = trim($value);\n\n        if ('' === $value) {\n            return null;\n        }\n\n        return e($value);\n    }\n\n    abstract protected function tag(): string;\n}\n"
  },
  {
    "path": "src/Structs/Title.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs;\n\n/**\n * @see https://github.com/joshbuchea/HEAD#elements\n */\nclass Title extends Struct\n{\n    protected $unique = true;\n\n    protected function tag(): string\n    {\n        return 'title';\n    }\n}\n"
  },
  {
    "path": "src/Structs/Traits/HookableTrait.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Structs\\Traits;\n\nuse romanzipp\\Seo\\Enums\\HookTarget;\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse romanzipp\\Seo\\Values\\Attribute;\n\ntrait HookableTrait\n{\n    /**\n     * Applied hooks.\n     *\n     * @var \\romanzipp\\Seo\\Helpers\\Hook[]\n     */\n    protected static $hooks = [];\n\n    /**\n     * Add given Hook to the struct.\n     *\n     * @param \\romanzipp\\Seo\\Helpers\\Hook $hook\n     *\n     * @return void\n     */\n    public static function hook(Hook $hook): void\n    {\n        self::$hooks[] = $hook;\n    }\n\n    /**\n     * Remove all hooks.\n     *\n     * @return void\n     */\n    public static function clearHooks(): void\n    {\n        self::$hooks = [];\n    }\n\n    /**\n     * Trigger all possible hooks by given target.\n     * This is getting called if struct values are changed.\n     *\n     * @param int $target\n     * @param mixed $data\n     *\n     * @return void\n     */\n    public function triggerHook(int $target, $data): void\n    {\n        foreach ($this->getMatchingHooks($target, $data) as $hook) {\n            $callback = $hook->getCallback();\n\n            // We can pass body or multiple attributes to the user callback without\n            // worring about data pre-formating.\n            // In case of single-attribute manipulation we only need to pass the\n            // attribute value to the callback.\n\n            $callbackData = $hook->translateCallbackData($data);\n\n            $this->setModifiedHookData(\n                $hook,\n                $callback($callbackData)\n            );\n        }\n    }\n\n    /**\n     * Get all matching hooks applied to the struct\n     * given by a target.\n     *\n     * @param int $target\n     * @param mixed $data\n     *\n     * @return \\romanzipp\\Seo\\Helpers\\Hook[]\n     */\n    public function getMatchingHooks(int $target, $data): array\n    {\n        $hooks = [];\n\n        foreach (self::$hooks as $key => $hook) {\n            // Continue, if applied hook target does not match\n            // the intendet target.\n\n            if ($hook->getTarget() !== $target) {\n                continue;\n            }\n\n            // Filter by attributes applied to the hook with the\n            // whereAttribute() method.\n\n            $filterAttributes = $hook->getFilterAttributes();\n\n            foreach ($filterAttributes as $fAttribute => $fValue) {\n                if ($this->getComputedAttribute($fAttribute) != $fValue) {\n                    continue 2;\n                }\n            }\n\n            // Dont't make any more processing if we are targeting the\n            // Struct body or attributes array.\n\n            if (HookTarget::BODY == $target || HookTarget::ATTRIBUTES == $target) {\n                $hooks[] = $hook;\n\n                continue;\n            }\n\n            // $data = ['attribute' => 'value']\n\n            $attribute = array_keys($data)[0];\n\n            if ($attribute != $hook->getTargetAttribute()) {\n                continue;\n            }\n\n            $hooks[] = $hook;\n        }\n\n        return $hooks;\n    }\n\n    /**\n     * Set the modified struct data from hook\n     * as struct value.\n     *\n     * @param \\romanzipp\\Seo\\Helpers\\Hook $hook\n     * @param mixed $data\n     *\n     * @return void\n     */\n    public function setModifiedHookData(Hook $hook, $data): void\n    {\n        switch ($hook->getTarget()) {\n            case HookTarget::BODY:\n                $this->body->setData($data);\n\n                break;\n\n            case HookTarget::ATTRIBUTES:\n                $this->setModifiedHookAttributes($data);\n\n                break;\n\n            case HookTarget::ATTRIBUTE:\n                $this->attributes[$hook->getTargetAttribute()]->setData($data);\n\n                break;\n        }\n    }\n\n    /**\n     * Set HookTarget::ATTRIBUTES data from hook callback.\n     *\n     * @param mixed $data\n     */\n    protected function setModifiedHookAttributes($data): void\n    {\n        $attributes = $this->getAttributes();\n\n        foreach ($data as $modifiedAttribute => $modifiedAttributeValue) {\n            if (array_key_exists($modifiedAttribute, $attributes)) {\n                $attributes[$modifiedAttribute]->setData($modifiedAttributeValue);\n            } else {\n                // Set the attribute directly to avoid triggering\n                // further Hooks\n\n                $this->attributes[$modifiedAttribute] = new Attribute($modifiedAttributeValue);\n            }\n        }\n    }\n\n    abstract public function getComputedAttribute(string $attribute);\n\n    abstract public function getAttributes(): array;\n}\n"
  },
  {
    "path": "src/Values/Attribute.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Values;\n\nclass Attribute extends Value\n{\n}\n"
  },
  {
    "path": "src/Values/Body.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Values;\n\nclass Body extends Value\n{\n}\n"
  },
  {
    "path": "src/Values/Value.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Values;\n\nclass Value\n{\n    /**\n     * Value object original data.\n     *\n     * @var mixed|null\n     */\n    protected $originalData;\n\n    /**\n     * Value object data.\n     *\n     * @var mixed|null\n     */\n    protected $data;\n\n    /**\n     * Constructor.\n     *\n     * @param mixed|null $data\n     */\n    public function __construct($data = null)\n    {\n        $this->originalData = $data;\n    }\n\n    /**\n     * Get value data.\n     *\n     * @return mixed|null\n     */\n    public function data()\n    {\n        if (null !== $this->data) {\n            return $this->data;\n        }\n\n        return $this->originalData;\n    }\n\n    /**\n     * Get original value data.\n     *\n     * @return mixed|null\n     */\n    public function getOriginalData()\n    {\n        return $this->originalData;\n    }\n\n    /**\n     * Set modified data.\n     *\n     * @param mixed $data\n     */\n    public function setData($data): void\n    {\n        $this->data = $data;\n    }\n\n    /**\n     * Get data string representation.\n     *\n     * @return string\n     */\n    public function __toString()\n    {\n        return (string) $this->data();\n    }\n}\n"
  },
  {
    "path": "src/helpers.php",
    "content": "<?php\n\nuse romanzipp\\Seo\\Services\\SeoService;\n\nif ( ! function_exists('seo')) {\n    /**\n     * Create SeoService instance.\n     *\n     * @param string|null $section\n     *\n     * @return \\romanzipp\\Seo\\Services\\SeoService\n     */\n    function seo(?string $section = null): SeoService\n    {\n        if (null === $section) {\n            return app(SeoService::class);\n        }\n\n        return app(SeoService::class)->section($section);\n    }\n}\n"
  },
  {
    "path": "tests/ArrayFormatTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Structs\\Link;\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Meta\\Description;\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\nuse romanzipp\\Seo\\Structs\\Meta\\Twitter;\nuse romanzipp\\Seo\\Structs\\Title;\n\nclass ArrayFormatTest extends TestCase\n{\n    // Title\n\n    public function testTitleIndex()\n    {\n        seo()->addFromArray([\n            'title' => 'Foo',\n        ]);\n\n        $this->assertCount(4, seo()->getStructs());\n\n        $this->assertInstanceOf(Title::class, $struct = seo()->getStruct(Title::class));\n        $this->assertEquals('Foo', (string) $struct->getBody());\n\n        $this->assertInstanceOf(Twitter::class, $struct = seo()->getStruct(Twitter::class));\n        $this->assertEquals('Foo', (string) $struct->getComputedAttribute('content'));\n\n        $this->assertInstanceOf(OpenGraph::class, $struct = seo()->getStruct(OpenGraph::class));\n        $this->assertEquals('Foo', (string) $struct->getComputedAttribute('content'));\n    }\n\n    public function testTitleIndexTagOnly()\n    {\n        config([\n            'seo.shorthand.title.tag' => true,\n            'seo.shorthand.title.opengraph' => false,\n            'seo.shorthand.title.twitter' => false,\n            'seo.shorthand.title.embedx' => false,\n        ]);\n\n        seo()->addFromArray([\n            'title' => 'Foo',\n        ]);\n\n        $this->assertCount(1, seo()->getStructs());\n\n        $this->assertInstanceOf(Title::class, $struct = seo()->getStruct(Title::class));\n        $this->assertEquals('Foo', (string) $struct->getBody());\n    }\n\n    // Description\n\n    public function testDescriptionIndex()\n    {\n        seo()->addFromArray([\n            'description' => 'Foo',\n        ]);\n\n        $this->assertCount(4, seo()->getStructs());\n\n        $this->assertInstanceOf(Description::class, $struct = seo()->getStruct(Description::class));\n        $this->assertEquals('Foo', (string) $struct->getComputedAttribute('content'));\n\n        $this->assertInstanceOf(Twitter::class, $struct = seo()->getStruct(Twitter::class));\n        $this->assertEquals('Foo', (string) $struct->getComputedAttribute('content'));\n\n        $this->assertInstanceOf(OpenGraph::class, $struct = seo()->getStruct(OpenGraph::class));\n        $this->assertEquals('Foo', (string) $struct->getComputedAttribute('content'));\n    }\n\n    public function testDescriptionIndexTagOnly()\n    {\n        config([\n            'seo.shorthand.description.tag' => true,\n            'seo.shorthand.description.opengraph' => false,\n            'seo.shorthand.description.twitter' => false,\n            'seo.shorthand.description.embedx' => false,\n        ]);\n\n        seo()->addFromArray([\n            'description' => 'Foo',\n        ]);\n\n        $this->assertCount(1, seo()->getStructs());\n\n        $this->assertInstanceOf(Description::class, $struct = seo()->getStruct(Description::class));\n        $this->assertEquals('Foo', (string) $struct->getComputedAttribute('content'));\n    }\n\n    // Twitter\n\n    public function testTwitterIndex()\n    {\n        seo()->addFromArray([\n            'twitter' => [\n                'card' => 'summary',\n            ],\n        ]);\n\n        $this->assertCount(1, seo()->getStructs());\n\n        $this->assertInstanceOf(Twitter::class, $struct = seo()->getStruct(Twitter::class));\n        $this->assertEquals('twitter:card', (string) $struct->getComputedAttribute('name'));\n        $this->assertEquals('summary', (string) $struct->getComputedAttribute('content'));\n    }\n\n    // OG\n\n    public function testOpenGraphIndex()\n    {\n        seo()->addFromArray([\n            'og' => [\n                'locale' => 'de',\n            ],\n        ]);\n\n        $this->assertCount(1, seo()->getStructs());\n\n        $this->assertInstanceOf(OpenGraph::class, $struct = seo()->getStruct(OpenGraph::class));\n        $this->assertEquals('og:locale', (string) $struct->getComputedAttribute('property'));\n        $this->assertEquals('de', (string) $struct->getComputedAttribute('content'));\n    }\n\n    // Meta\n\n    public function testMetaIndex()\n    {\n        seo()->addFromArray([\n            'meta' => [\n                [\n                    'name' => 'copyright',\n                    'content' => 'Roman Zipp',\n                ],\n            ],\n        ]);\n\n        $this->assertCount(1, seo()->getStructs());\n\n        $this->assertInstanceOf(Meta::class, $struct = seo()->getStruct(Meta::class));\n        $this->assertEquals('copyright', (string) $struct->getComputedAttribute('name'));\n        $this->assertEquals('Roman Zipp', (string) $struct->getComputedAttribute('content'));\n    }\n\n    public function testMetaIndexMultiple()\n    {\n        seo()->addFromArray([\n            'meta' => [\n                [\n                    'name' => 'copyright',\n                    'content' => 'Roman Zipp',\n                ],\n                [\n                    'name' => 'theme-color',\n                    'content' => '#3053C6',\n                ],\n            ],\n        ]);\n\n        $this->assertCount(2, seo()->getStructs());\n\n        $this->assertInstanceOf(Meta::class, $struct = seo()->getStructs()[0]);\n        $this->assertEquals('copyright', (string) $struct->getComputedAttribute('name'));\n        $this->assertEquals('Roman Zipp', (string) $struct->getComputedAttribute('content'));\n\n        $this->assertInstanceOf(Meta::class, $struct = seo()->getStructs()[1]);\n        $this->assertEquals('theme-color', (string) $struct->getComputedAttribute('name'));\n        $this->assertEquals('#3053C6', (string) $struct->getComputedAttribute('content'));\n    }\n\n    // Link\n\n    public function testLinkIndex()\n    {\n        seo()->addFromArray([\n            'link' => [\n                [\n                    'rel' => 'icon',\n                    'href' => '/favicon.ico',\n                ],\n            ],\n        ]);\n\n        $this->assertCount(1, seo()->getStructs());\n\n        $this->assertInstanceOf(Link::class, $struct = seo()->getStruct(Link::class));\n        $this->assertEquals('icon', (string) $struct->getComputedAttribute('rel'));\n        $this->assertEquals('/favicon.ico', (string) $struct->getComputedAttribute('href'));\n    }\n\n    public function testLinkIndexMultiple()\n    {\n        seo()->addFromArray([\n            'link' => [\n                [\n                    'rel' => 'icon',\n                    'href' => '/favicon.ico',\n                ],\n                [\n                    'rel' => 'preload',\n                    'href' => '/font.woff2',\n                ],\n            ],\n        ]);\n\n        $this->assertCount(2, seo()->getStructs());\n\n        $this->assertInstanceOf(Link::class, $struct = seo()->getStructs()[0]);\n        $this->assertEquals('icon', (string) $struct->getComputedAttribute('rel'));\n        $this->assertEquals('/favicon.ico', (string) $struct->getComputedAttribute('href'));\n\n        $this->assertInstanceOf(Link::class, $struct = seo()->getStructs()[1]);\n        $this->assertEquals('preload', (string) $struct->getComputedAttribute('rel'));\n        $this->assertEquals('/font.woff2', (string) $struct->getComputedAttribute('href'));\n    }\n}\n"
  },
  {
    "path": "tests/CollisionTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Structs\\Meta\\Charset;\nuse romanzipp\\Seo\\Structs\\Meta\\Robots;\nuse romanzipp\\Seo\\Structs\\Meta\\Viewport;\nuse romanzipp\\Seo\\Structs\\Title;\nuse romanzipp\\Seo\\Test\\Structs\\UniqueMultiAttributeStruct;\nuse romanzipp\\Seo\\Test\\Structs\\UniqueSingleAttributeStruct;\n\nclass CollisionTest extends TestCase\n{\n    public function testShouldNotCollide()\n    {\n        seo()->add(\n            Charset::make()\n        );\n\n        seo()->add(\n            Viewport::make()->content('width=device-width, initial-scale=1')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(2, $contents);\n    }\n\n    public function testNormalElementCollisions()\n    {\n        seo()->add(\n            Title::make()->body('My Title')\n        );\n\n        seo()->add(\n            Title::make()->body('My Second Title')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        $this->assertEquals('<title>My Second Title</title>', $contents[0]);\n    }\n\n    public function testRobotsElementCollisions()\n    {\n        seo()->add(\n            Robots::make()->content('index')\n        );\n\n        seo()->add(\n            Robots::make()->content('noindex')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        $this->assertEquals('<meta name=\"robots\" content=\"noindex\" />', $contents[0]);\n    }\n\n    public function testVoidElementSingleAttributeCollisions()\n    {\n        seo()->add(\n            UniqueSingleAttributeStruct::make()\n                ->attr('first', 'unique')\n                ->attr('content', 'My Site Name')\n        );\n\n        seo()->add(\n            UniqueSingleAttributeStruct::make()\n                ->attr('first', 'unique')\n                ->attr('content', 'My Second Site Name')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        $this->assertMatchesRegularExpressionCustom('/content\\=\\\"My Second Site Name\\\"/', $contents[0]);\n    }\n\n    public function testVoidElementSingleOptionalAttributeCollisions()\n    {\n        seo()->add(\n            UniqueSingleAttributeStruct::make()\n                ->attr('first', 'unique')\n                ->attr('content', 'My Site Name')\n        );\n\n        seo()->add(\n            UniqueSingleAttributeStruct::make()\n                ->attr('second', 'unique')\n                ->attr('content', 'My Second Site Name')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(2, $contents);\n\n        $this->assertMatchesRegularExpressionCustom('/content\\=\\\"My Site Name\\\"/', $contents[0]);\n        $this->assertMatchesRegularExpressionCustom('/content\\=\\\"My Second Site Name\\\"/', $contents[1]);\n    }\n\n    public function testVoidElementMultipleAttributesCollisions()\n    {\n        seo()->add(\n            UniqueMultiAttributeStruct::make()\n                ->attr('first', 'unique')\n                ->attr('second', 'also unique')\n                ->attr('content', 'My Value')\n        );\n\n        seo()->add(\n            UniqueMultiAttributeStruct::make()\n                ->attr('first', 'unique')\n                ->attr('second', 'also unique')\n                ->attr('content', 'My Second Value')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        $this->assertMatchesRegularExpressionCustom('/content\\=\\\"My Second Value\\\"/', $contents[0]);\n    }\n}\n"
  },
  {
    "path": "tests/EscapingTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Title;\n\nclass EscapingTest extends TestCase\n{\n    public function testBodyEscaping()\n    {\n        $malicious = '<script>alert(\"malicious\");</script>';\n\n        seo()->add(\n            Title::make()->body($malicious)\n        );\n\n        $title = seo()->render()->toArray()[0];\n\n        $this->assertEquals('<title>' . e($malicious) . '</title>', $title);\n    }\n\n    public function testAttributeEscaping()\n    {\n        $malicious = '<script>alert(\"malicious\");</script>';\n\n        seo()->add(\n            Meta::make()->attr('content', $malicious)\n        );\n\n        $meta = seo()->render()->toArray()[0];\n\n        $this->assertEquals('<meta content=\"' . e($malicious) . '\" />', $meta);\n    }\n\n    public function testSkipEscaping()\n    {\n        $url = 'http://example.com/something?param1=123&param2=456';\n\n        $expected = '<meta name=\"url\" content=\"' . $url . '\" />';\n\n        seo()->add(\n            Meta::make()->attr('name', 'url')->attr('content', $url, false)\n        );\n\n        $meta = seo()->render()->toArray()[0];\n\n        $this->assertEquals($expected, $meta);\n    }\n\n    public function testShorthandSkipEscaping()\n    {\n        $url = 'http://example.com/something?param1=123&param2=456';\n\n        $expected = '<meta name=\"twitter:player\" content=\"' . $url . '\" />';\n\n        seo()->twitter('player', $url, false);\n\n        $meta = seo()->render()->toArray()[0];\n\n        $this->assertEquals($expected, $meta);\n    }\n}\n"
  },
  {
    "path": "tests/HooksTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\nuse romanzipp\\Seo\\Structs\\Title;\n\nclass HooksTest extends TestCase\n{\n    public function testBodyHooks()\n    {\n        Title::hook(\n            Hook::make()\n                ->onBody()\n                ->callback(function ($body) {\n                    return $body . ' 1';\n                })\n        );\n\n        seo()->add(\n            Title::make()->body('My Title')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        $this->assertEquals('<title>My Title 1</title>', $contents[0]);\n\n        Title::clearHooks();\n    }\n\n    public function testMultipleBodyHooks()\n    {\n        Title::hook(\n            Hook::make()\n                ->onBody()\n                ->callback(function ($body) {\n                    return $body . ' 1';\n                })\n        );\n\n        Title::hook(\n            Hook::make()\n                ->onBody()\n                ->callback(function ($body) {\n                    return $body . ' 2';\n                })\n        );\n\n        seo()->add(\n            Title::make()->body('My Title')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertEquals('<title>My Title 1 2</title>', $contents[0]);\n\n        Title::clearHooks();\n    }\n\n    public function testBodyHookMultipleExecutions()\n    {\n        Title::hook(\n            Hook::make()\n                ->onBody()\n                ->callback(function ($body) {\n                    return $body . ' 1';\n                })\n        );\n\n        seo()->add(\n            Title::make()->body('My Title')\n        );\n\n        seo()->add(\n            Title::make()->body('Some Title')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertEquals('<title>Some Title 1</title>', $contents[0]);\n\n        Title::clearHooks();\n    }\n\n    public function testExistingAttributesHooks()\n    {\n        OpenGraph::hook(\n            Hook::make()\n                ->onAttributes()\n                ->whereAttribute('property', 'og:title')\n                ->callback(function ($attributes) {\n                    return array_merge($attributes, ['content' => 'My Second Title']);\n                })\n        );\n\n        seo()->add(\n            OpenGraph::make()->property('title')->content('My Title')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertMatchesRegularExpressionCustom('/content\\=\\\"My Second Title\\\"/', $contents[0]);\n\n        OpenGraph::clearHooks();\n    }\n\n    public function testAppendingAttributesHooks()\n    {\n        OpenGraph::hook(\n            Hook::make()\n                ->onAttributes()\n                ->whereAttribute('property', 'og:title')\n                ->callback(function ($attributes) {\n                    return array_merge(['should' => 'exist'], $attributes);\n                })\n        );\n\n        seo()->add(\n            OpenGraph::make()->property('title')->content('My Title')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertMatchesRegularExpressionCustom('/should\\=\\\"exist\\\"/', $contents[0]);\n\n        OpenGraph::clearHooks();\n    }\n\n    public function testAttributeHooks()\n    {\n        OpenGraph::hook(\n            Hook::make()\n                ->onAttribute('content')\n                ->whereAttribute('property', 'og:title')\n                ->callback(function ($content) {\n                    return $content . ' 1';\n                })\n        );\n\n        seo()->add(\n            OpenGraph::make()->property('title')->content('My Title')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertMatchesRegularExpressionCustom('/content\\=\\\"My Title 1\\\"/', $contents[0]);\n\n        OpenGraph::clearHooks();\n    }\n\n    public function testEmptyStructTargetHooks()\n    {\n        Title::hook(\n            Hook::make()\n                ->onBody()\n                ->callback(function ($body) {\n                    return $body . ' 1';\n                })\n        );\n\n        seo()->add(\n            Title::make()->attr('ignore', 'me')\n        );\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        Title::clearHooks();\n    }\n}\n"
  },
  {
    "path": "tests/InstantiationTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Facades\\Seo;\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse romanzipp\\Seo\\Services\\SeoService;\nuse romanzipp\\Seo\\Structs\\Meta;\n\nclass InstantiationTest extends TestCase\n{\n    public function testServiceInstance()\n    {\n        $this->assertInstanceOf(SeoService::class, app(SeoService::class));\n\n        $this->assertInstanceOf(SeoService::class, Seo::make());\n\n        $this->assertInstanceOf(SeoService::class, seo());\n    }\n\n    public function testHookInstance()\n    {\n        $this->assertInstanceOf(Hook::class, Hook::make());\n    }\n\n    public function testStructInstance()\n    {\n        $this->assertInstanceOf(Meta::class, Meta::make());\n    }\n}\n"
  },
  {
    "path": "tests/MixManifestAssetAttributesTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Conductors\\Types\\ManifestAsset;\n\nclass MixManifestAssetAttributesTest extends TestCase\n{\n    public function testGuessScriptType()\n    {\n        $this->assertEquals('script', (new ManifestAsset('/js/app.js', '/js/app.123456.js'))->as);\n        $this->assertEquals('script', (new ManifestAsset('/js/app.js', '/js/app.js?id=123456'))->as);\n    }\n\n    public function testGuessStyleType()\n    {\n        $this->assertEquals('style', (new ManifestAsset('/js/app.css', '/js/app.123456.css'))->as);\n        $this->assertEquals('style', (new ManifestAsset('/js/app.css', '/js/app.css?id=123456'))->as);\n    }\n\n    public function testGuessFontType()\n    {\n        $this->assertEquals('font', (new ManifestAsset('/fonts/Comic-Sans.otf', '/fonts/Comic-Sans.123456.otf'))->as);\n        $this->assertEquals('font', (new ManifestAsset('/fonts/Comic-Sans.otf', '/fonts/Comic-Sans.otf?id=123456'))->as);\n\n        $this->assertEquals('font', (new ManifestAsset('/fonts/Comic-Sans.ttf', '/fonts/Comic-Sans.123456.ttf'))->as);\n        $this->assertEquals('font', (new ManifestAsset('/fonts/Comic-Sans.ttf', '/fonts/Comic-Sans.ttf?id=123456'))->as);\n    }\n\n    public function testUnsupportedExtension()\n    {\n        $this->assertNull((new ManifestAsset('/totally-not-a-virus/app.exe', '/totally-not-a-virus/app.123456.exe'))->as);\n        $this->assertNull((new ManifestAsset('/totally-not-a-virus/app.exe', '/totally-not-a-virus/app.exe?id=123456'))->as);\n    }\n\n    public function testInvalidExtension()\n    {\n        $this->assertNull((new ManifestAsset('/totally-not-a-virus/app', '/totally-not-a-virus/app'))->as);\n    }\n}\n"
  },
  {
    "path": "tests/MixManifestTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Conductors\\MixManifestConductor;\nuse romanzipp\\Seo\\Conductors\\Types\\ManifestAsset;\nuse romanzipp\\Seo\\Exceptions\\ManifestNotFoundException;\nuse romanzipp\\Seo\\Structs\\Link;\n\nclass MixManifestTest extends TestCase\n{\n    public function testInstance()\n    {\n        $mix = seo()->mix();\n\n        $this->assertInstanceOf(MixManifestConductor::class, $mix);\n    }\n\n    public function testLoadingOk()\n    {\n        $path = $this->path('mix-manifest.json');\n\n        $mix = seo()\n            ->mix()\n            ->load($path);\n\n        $assets = json_decode(\n            file_get_contents($path),\n            true\n        );\n\n        $this->assertEquals([\n            new ManifestAsset(array_keys($assets)[0], array_values($assets)[0]),\n            new ManifestAsset(array_keys($assets)[1], array_values($assets)[1]),\n        ], $mix->getAssets());\n    }\n\n    public function testLoadingInvalidPath()\n    {\n        $this->expectException(ManifestNotFoundException::class);\n\n        $path = $this->path('mix-manifest.not-found.json');\n\n        seo()\n            ->mix()\n            ->load($path);\n    }\n\n    public function testLoadInvalidPathIgnoredException()\n    {\n        $path = $this->path('mix-manifest.not-found.json');\n\n        $mix = seo()\n            ->mix()\n            ->ignore()\n            ->load($path);\n\n        $this->assertEquals([], $mix->getAssets());\n    }\n\n    public function testLoadingInvalidJson()\n    {\n        $path = $this->path('mix-manifest.empty.json');\n\n        $mix = seo()\n            ->mix()\n            ->load($path);\n\n        $this->assertEquals([], $mix->getAssets());\n    }\n\n    public function testLoadingEmptyFile()\n    {\n        $path = $this->path('mix-manifest.empty.json');\n\n        $mix = seo()\n            ->mix()\n            ->load($path);\n\n        $this->assertEquals([], $mix->getAssets());\n    }\n\n    public function testDefaultRel()\n    {\n        $path = $this->path('mix-manifest.json');\n\n        $mix = seo()\n            ->mix()\n            ->load($path);\n\n        $this->assertEquals(\n            ['prefetch', 'prefetch'],\n            [\n                $mix->getAssets()[0]->rel,\n                $mix->getAssets()[1]->rel,\n            ]\n        );\n    }\n\n    public function testMapCallbackNoChanges()\n    {\n        $path = $this->path('mix-manifest.json');\n\n        $mix = seo()\n            ->mix()\n            ->map(function (ManifestAsset $asset): ?ManifestAsset {\n                return $asset;\n            })\n            ->load($path);\n\n        $assets = json_decode(\n            file_get_contents($path),\n            true\n        );\n\n        $this->assertEquals([\n            new ManifestAsset(array_keys($assets)[0], array_values($assets)[0]),\n            new ManifestAsset(array_keys($assets)[1], array_values($assets)[1]),\n        ], $mix->getAssets());\n    }\n\n    public function testMapCallbackRejectAll()\n    {\n        $path = $this->path('mix-manifest.json');\n\n        $mix = seo()\n            ->mix()\n            ->map(function (ManifestAsset $asset): ?ManifestAsset {\n                return null;\n            })\n            ->load($path);\n\n        $this->assertCount(0, $mix->getAssets());\n    }\n\n    public function testMapCallbackModifyUrl()\n    {\n        $path = $this->path('mix-manifest.json');\n\n        $mix = seo()\n            ->mix()\n            ->map(function (ManifestAsset $asset): ?ManifestAsset {\n                $asset->url = 'http://localhost' . $asset->url;\n\n                return $asset;\n            })\n            ->load($path);\n\n        $assets = json_decode(\n            file_get_contents($path),\n            true\n        );\n\n        $this->assertEquals(\n            [\n                'http://localhost' . array_values($assets)[0],\n                'http://localhost' . array_values($assets)[1],\n            ],\n            [\n                $mix->getAssets()[0]->url,\n                $mix->getAssets()[1]->url,\n            ]\n        );\n    }\n\n    public function testMapCallbackModifyPath()\n    {\n        $path = $this->path('mix-manifest.json');\n\n        $mix = seo()\n            ->mix()\n            ->map(function (ManifestAsset $asset): ?ManifestAsset {\n                $asset->path = '/somewhere' . $asset->path;\n\n                return $asset;\n            })\n            ->load($path);\n\n        $assets = json_decode(\n            file_get_contents($path),\n            true\n        );\n\n        $this->assertEquals(\n            [\n                '/somewhere' . array_keys($assets)[0],\n                '/somewhere' . array_keys($assets)[1],\n            ],\n            [\n                $mix->getAssets()[0]->path,\n                $mix->getAssets()[1]->path,\n            ]\n        );\n    }\n\n    public function testMapCallbackModifyRel()\n    {\n        $path = $this->path('mix-manifest.json');\n\n        $mix = seo()\n            ->mix()\n            ->map(function (ManifestAsset $asset): ?ManifestAsset {\n                $asset->rel = 'preload';\n\n                return $asset;\n            })\n            ->load($path);\n\n        $this->assertEquals(\n            ['preload', 'preload'],\n            [\n                $mix->getAssets()[0]->rel,\n                $mix->getAssets()[1]->rel,\n            ]\n        );\n    }\n\n    public function testBasicStructs()\n    {\n        $path = __DIR__ . '/Support/mix-manifest.json';\n\n        seo()\n            ->mix()\n            ->load($path);\n\n        $this->assertCount(2, seo()->getStructs());\n\n        $this->assertInstanceOf(Link::class, seo()->getStructs()[0]);\n        $this->assertInstanceOf(Link::class, seo()->getStructs()[1]);\n    }\n\n    private function path(string $file): string\n    {\n        return sprintf('%s/Support/%s', __DIR__, $file);\n    }\n}\n"
  },
  {
    "path": "tests/RenderTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse Illuminate\\Support\\HtmlString;\nuse romanzipp\\Seo\\Builders\\StructBuilder;\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Title;\n\nclass RenderTest extends TestCase\n{\n    public function testRenderAll()\n    {\n        seo()->title('My Title');\n\n        $this->assertInstanceOf(HtmlString::class, seo()->render()->build());\n    }\n\n    public function testRenderSingleStruct()\n    {\n        $struct = Title::make()->body('My Title');\n\n        $this->assertInstanceOf(HtmlString::class, StructBuilder::build($struct));\n    }\n\n    public function testAttributeRenderResult()\n    {\n        seo()->add(\n            Title::make()->attr('attribute', 'value')\n        );\n\n        $this->assertEquals('<title attribute=\"value\"></title>', seo()->render()->toHtml());\n    }\n\n    public function testSpacedAttributeRenderResult()\n    {\n        seo()->add(\n            Title::make()->attr('attribute', 'value ')\n        );\n\n        $this->assertEquals('<title attribute=\"value\"></title>', seo()->render()->toHtml());\n    }\n\n    public function testWrongSpacedAttributeRenderResult()\n    {\n        seo()->add(\n            Title::make()->attr('   attribute ', 'value')\n        );\n\n        $this->assertEquals('<title attribute=\"value\"></title>', seo()->render()->toHtml());\n    }\n\n    public function testBodyRenderResult()\n    {\n        seo()->add(\n            Title::make()->body('My Body')\n        );\n\n        $this->assertEquals('<title>My Body</title>', seo()->render()->toHtml());\n    }\n\n    public function testSpacedBodyRenderResult()\n    {\n        seo()->add(\n            Title::make()->body('My Body ')\n        );\n\n        $this->assertEquals('<title>My Body</title>', seo()->render()->toHtml());\n    }\n\n    public function testNullStringAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', '0')\n        );\n\n        $this->assertEquals('<meta name=\"0\" />', seo()->render()->toHtml());\n    }\n\n    public function testZeroIntegerAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', 0)\n        );\n\n        $this->assertEquals('<meta name=\"0\" />', seo()->render()->toHtml());\n    }\n\n    public function testEmptyStringAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', '')\n        );\n\n        $this->assertEquals('<meta name />', seo()->render()->toHtml());\n    }\n\n    public function testEmptySpaceStringAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', ' ')\n        );\n\n        $this->assertEquals('<meta name />', seo()->render()->toHtml());\n    }\n\n    public function testTrueBooleanAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', true)\n        );\n\n        $this->assertEquals('<meta name=\"1\" />', seo()->render()->toHtml());\n    }\n\n    public function testFalseBooleanAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', false)\n        );\n\n        $this->assertEquals('<meta name=\"0\" />', seo()->render()->toHtml());\n    }\n\n    public function testSeparator()\n    {\n        StructBuilder::$separator = '  ';\n\n        seo()->add(\n            Meta::make()->attr('name', 'first')\n        );\n\n        seo()->add(\n            Meta::make()->attr('name', 'second')\n        );\n\n        $this->assertEquals('<meta name=\"first\" />  <meta name=\"second\" />', seo()->render()->toHtml());\n    }\n\n    public function testIndent()\n    {\n        StructBuilder::$separator = PHP_EOL;\n        StructBuilder::$indent = '  ';\n\n        seo()->add(\n            Meta::make()->attr('name', 'first')\n        );\n\n        seo()->add(\n            Meta::make()->attr('name', 'second')\n        );\n\n        $this->assertEquals('  <meta name=\"first\" />' . PHP_EOL . '  <meta name=\"second\" />', seo()->render()->toHtml());\n    }\n\n    public function testTagSyntaxHtml5()\n    {\n        config(['seo.tag_syntax' => StructBuilder::TAG_SYNTAX_HTML5]);\n\n        seo()->add(\n            Meta::make()->attr('name', true)\n        );\n\n        $this->assertEquals('<meta name=\"1\">', seo()->render()->toHtml());\n    }\n\n    public function testTagSyntaxXhtml()\n    {\n        config(['seo.tag_syntax' => StructBuilder::TAG_SYNTAX_XHTML]);\n\n        seo()->add(\n            Meta::make()->attr('name', true)\n        );\n\n        $this->assertEquals('<meta name=\"1\" />', seo()->render()->toHtml());\n    }\n\n    public function testTagSyntaxXhtmlStrict()\n    {\n        config(['seo.tag_syntax' => StructBuilder::TAG_SYNTAX_XHTML_STRICT]);\n\n        seo()->add(\n            Meta::make()->attr('name', true)\n        );\n\n        $this->assertEquals('<meta name=\"1\"></meta>', seo()->render()->toHtml());\n    }\n\n    public function testTagSyntaxUnset()\n    {\n        config(['seo.tag_syntax' => null]);\n\n        seo()->add(\n            Meta::make()->attr('name', true)\n        );\n\n        $this->assertEquals('<meta name=\"1\" />', seo()->render()->toHtml());\n    }\n\n    public function testTagSyntaxUnknown()\n    {\n        config(['seo.tag_syntax' => 'invalid']);\n\n        seo()->add(\n            Meta::make()->attr('name', true)\n        );\n\n        $this->assertEquals('<meta name=\"1\" />', seo()->render()->toHtml());\n    }\n}\n"
  },
  {
    "path": "tests/SchemaOrgTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse Spatie\\SchemaOrg\\BreadcrumbList;\nuse Spatie\\SchemaOrg\\Schema;\n\nclass SchemaOrgTest extends TestCase\n{\n    public function testAppending()\n    {\n        seo()->addSchema(\n            Schema::localBusiness()->name('Spatie')\n        );\n\n        $this->assertCount(\n            1,\n            seo()->render()->toArray()\n        );\n    }\n\n    public function testSetter()\n    {\n        seo()->addSchema(\n            Schema::localBusiness()->name('Spatie')\n        );\n\n        seo()->setSchemes([\n            Schema::airline()->name('Spatie'),\n        ]);\n\n        $this->assertCount(\n            1,\n            seo()->render()->toArray()\n        );\n    }\n\n    public function testBasicRender()\n    {\n        seo()->addSchema(\n            Schema::localBusiness()->name('Spatie')\n        );\n\n        $this->assertStringStartsWith(\n            '<script type=\"application/ld+json\">',\n            seo()->render()->toHtml()\n        );\n    }\n\n    public function testBreadcrumbs()\n    {\n        seo()->addSchemaBreadcrumbs([\n            ['name' => 'First', 'item' => 'https://example.com/first'],\n            ['name' => 'Second', 'item' => 'https://example.com/second'],\n        ]);\n\n        $breadcrumbList = seo()->getSchemes()[0];\n\n        $this->assertInstanceOf(BreadcrumbList::class, $breadcrumbList);\n\n        $itemListElement = $breadcrumbList->getProperty('itemListElement');\n\n        $this->assertTrue(\n            is_array($itemListElement)\n        );\n\n        $this->assertCount(2, $itemListElement);\n\n        $this->assertEquals(\n            1,\n            $itemListElement[0]->getProperty('position')\n        );\n\n        $this->assertEquals(\n            'First',\n            $itemListElement[0]->getProperty('name')\n        );\n    }\n}\n"
  },
  {
    "path": "tests/SectionsTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Services\\SeoService;\nuse romanzipp\\Seo\\Structs\\Meta;\nuse Spatie\\SchemaOrg\\NightClub;\nuse Spatie\\SchemaOrg\\PetStore;\n\nclass SectionsTest extends TestCase\n{\n    public function testDefaultSection()\n    {\n        seo()->twitter('card', 'default');\n\n        self::assertSame(\n            seo()->section('default')->getStructs(),\n            seo()->getStructs()\n        );\n    }\n\n    public function testDefaultSectionExplicitlyDeclared()\n    {\n        seo()->section('default')->twitter('card', 'default');\n\n        self::assertSame(\n            seo()->section('default')->getStructs(),\n            seo()->getStructs()\n        );\n    }\n\n    public function testDefaultSectionUntouched()\n    {\n        seo()->section('secondary')->twitter('card', 'default');\n\n        self::assertSame(\n            seo()->section('default')->getStructs(),\n            seo()->getStructs()\n        );\n    }\n\n    public function testSectionsDoNotMatch()\n    {\n        seo()\n            ->add(Meta::make()->attr('section', 'default'));\n\n        seo()\n            ->section('secondary')\n            ->add(Meta::make()->attr('section', 'secondary'));\n\n        self::assertCount(1, seo()->getStructs());\n        self::assertSame('default', (string) seo()->getStruct(Meta::class)->getComputedAttribute('section'));\n\n        self::assertCount(1, seo()->section('default')->getStructs());\n        self::assertSame('default', (string) seo()->section('default')->getStruct(Meta::class)->getComputedAttribute('section'));\n\n        self::assertCount(1, seo()->section('secondary')->getStructs());\n        self::assertSame('secondary', (string) seo()->section('secondary')->getStruct(Meta::class)->getComputedAttribute('section'));\n    }\n\n    public function testSectionPassedAsParameterToHelper()\n    {\n        seo('secondary')->twitter('card', 'default');\n\n        self::assertSame(\n            seo()->section('secondary')->getStructs(),\n            seo('secondary')->getStructs()\n        );\n    }\n\n    public function testSectionSetterOnMutableInstance()\n    {\n        $seo = app(SeoService::class);\n\n        $seo->section('secondary');\n\n        $seo->twitter('card', 'default');\n        $seo->twitter('author', 'Roman');\n\n        self::assertCount(2, $seo->getStructs());\n\n        self::assertSame(\n            $seo->getStructs(),\n            seo('secondary')->getStructs()\n        );\n    }\n\n    public function testSectionRender()\n    {\n        seo()->twitter('card', 'default');\n\n        seo()->section('secondary')->twitter('card', 'secondary');\n\n        self::assertEquals(\n            '<meta name=\"twitter:card\" content=\"default\" />',\n            seo()->render()->toHtml()\n        );\n\n        self::assertEquals(\n            '<meta name=\"twitter:card\" content=\"secondary\" />',\n            seo()->section('secondary')->render()->toHtml()\n        );\n\n        self::assertEquals(\n            '<meta name=\"twitter:card\" content=\"secondary\" />',\n            seo('secondary')->render()->toHtml()\n        );\n    }\n\n    public function testSchemes()\n    {\n        seo()->addSchema(new NightClub());\n        seo('secondary')->addSchema(new PetStore());\n\n        self::assertCount(1, seo()->getSchemes());\n        self::assertInstanceOf(NightClub::class, seo()->getSchemes()[0]);\n\n        self::assertCount(1, seo('secondary')->getSchemes());\n        self::assertInstanceOf(PetStore::class, seo('secondary')->getSchemes()[0]);\n    }\n}\n"
  },
  {
    "path": "tests/SetterTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\nuse romanzipp\\Seo\\Structs\\Meta\\Twitter;\n\nclass SetterTest extends TestCase\n{\n    public function testClear()\n    {\n        seo()->twitter('card', 'image');\n\n        self::assertCount(1, seo()->getStructs());\n\n        seo()->clearStructs();\n\n        self::assertCount(0, seo()->getStructs());\n    }\n\n    public function testSetOverride()\n    {\n        seo()->twitter('card', 'image');\n\n        self::assertCount(1, seo()->getStructs());\n\n        seo()->setStructCollection([\n            OpenGraph::make(),\n        ]);\n\n        self::assertCount(1, seo()->getStructs());\n        self::assertInstanceOf(OpenGraph::class, seo()->getStruct(OpenGraph::class));\n    }\n\n    public function testAdd()\n    {\n        seo()->add(Twitter::make());\n\n        self::assertCount(1, seo()->getStructs());\n\n        seo()->add(OpenGraph::make());\n\n        self::assertCount(2, seo()->getStructs());\n    }\n\n    public function testAddIf()\n    {\n        seo()->addIf(false, Twitter::make());\n\n        self::assertCount(0, seo()->getStructs());\n\n        seo()->addIf(true, Twitter::make());\n\n        self::assertCount(1, seo()->getStructs());\n    }\n}\n"
  },
  {
    "path": "tests/ShorthandSettersTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Structs\\Link\\Canonical;\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Meta\\Charset;\nuse romanzipp\\Seo\\Structs\\Meta\\OpenGraph;\nuse romanzipp\\Seo\\Structs\\Meta\\Twitter;\nuse romanzipp\\Seo\\Structs\\Meta\\Viewport;\n\nclass ShorthandSettersTest extends TestCase\n{\n    public function testTitleSingleSetter()\n    {\n        config([\n            'seo.shorthand.title.tag' => true,\n            'seo.shorthand.title.twitter' => false,\n            'seo.shorthand.title.opengraph' => false,\n            'seo.shorthand.title.embedx' => false,\n        ]);\n\n        seo()->title('My Title');\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n    }\n\n    public function testTitleMultipleSetter()\n    {\n        config([\n            'seo.shorthand.title.tag' => true,\n            'seo.shorthand.title.twitter' => true,\n            'seo.shorthand.title.opengraph' => true,\n            'seo.shorthand.title.embedx' => true,\n        ]);\n\n        seo()->title('My Title');\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(4, $contents);\n    }\n\n    public function testDescriptionSingleSetter()\n    {\n        config([\n            'seo.shorthand.description.meta' => true,\n            'seo.shorthand.description.twitter' => false,\n            'seo.shorthand.description.opengraph' => false,\n            'seo.shorthand.description.embedx' => false,\n        ]);\n\n        seo()->description('My Description');\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n    }\n\n    public function testDescriptionMultipleSetter()\n    {\n        config([\n            'seo.shorthand.description.meta' => true,\n            'seo.shorthand.description.twitter' => true,\n            'seo.shorthand.description.opengraph' => true,\n            'seo.shorthand.description.embedx' => true,\n        ]);\n\n        seo()->description('My Description');\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(4, $contents);\n    }\n\n    public function testTwitterSetter()\n    {\n        seo()->twitter('card', 'summary');\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        $this->assertInstanceOf(Twitter::class, seo()->getStructs()[0]);\n    }\n\n    public function testOpenGraphSetter()\n    {\n        seo()->og('site_name', 'My Site Name');\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        $this->assertInstanceOf(OpenGraph::class, seo()->getStructs()[0]);\n    }\n\n    public function testEmbedXSetter()\n    {\n        seo()->embedx('title', 'My Site Name');\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        $this->assertInstanceOf(Meta\\EmbedX::class, seo()->getStructs()[0]);\n    }\n\n    public function testMetaSetter()\n    {\n        seo()->meta('author', 'My Little Pony');\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        $this->assertInstanceOf(Meta::class, seo()->getStructs()[0]);\n    }\n\n    public function testCharsetSetter()\n    {\n        seo()->charset('utf-8');\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        $this->assertInstanceOf(Charset::class, seo()->getStructs()[0]);\n    }\n\n    public function testViewportSetter()\n    {\n        seo()->viewport('width=device-width, initial-scale=1');\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        $this->assertInstanceOf(Viewport::class, seo()->getStructs()[0]);\n    }\n\n    public function testCanonicalSetter()\n    {\n        seo()->canonical('https://test.com/example');\n\n        $contents = seo()->render()->toArray();\n\n        $this->assertCount(1, $contents);\n\n        $this->assertInstanceOf(Canonical::class, seo()->getStructs()[0]);\n    }\n}\n"
  },
  {
    "path": "tests/Structs/UniqueMultiAttributeStruct.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test\\Structs;\n\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass UniqueMultiAttributeStruct extends Struct\n{\n    protected $unique = true;\n\n    protected $uniqueAttributes = ['first', 'second'];\n\n    protected function tag(): string\n    {\n        return 'unique-multi-attr';\n    }\n}\n"
  },
  {
    "path": "tests/Structs/UniqueSingleAttributeStruct.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test\\Structs;\n\nuse romanzipp\\Seo\\Structs\\Struct;\n\nclass UniqueSingleAttributeStruct extends Struct\n{\n    protected $unique = true;\n\n    protected $uniqueAttributes = ['first'];\n\n    protected function tag(): string\n    {\n        return 'unique-single-attr';\n    }\n}\n"
  },
  {
    "path": "tests/Support/mix-manifest.empty.json",
    "content": "{}\n"
  },
  {
    "path": "tests/Support/mix-manifest.json",
    "content": "{\n  \"/js/app.js\": \"/js/app.0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33.js\",\n  \"/css/app.css\": \"/css/app.62cdb7020ff920e5aa642c3d4066950dd1f01f4d.css\"\n}\n"
  },
  {
    "path": "tests/Support/mix-manifest.null.json",
    "content": "null\n"
  },
  {
    "path": "tests/Support/mix-manifest.versioned.json",
    "content": "{\n  \"/js/app.js\": \"/js/app.js?id=4c8b94c7a94dd6137b79\",\n  \"/css/app.css\": \"/css/app.css?id=35f9f53a2e3a7804169d\"\n}\n"
  },
  {
    "path": "tests/TestCase.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse PHPUnit\\Framework\\Constraint\\RegularExpression;\nuse romanzipp\\Seo\\Builders\\StructBuilder;\nuse romanzipp\\Seo\\Facades\\Seo;\nuse romanzipp\\Seo\\Providers\\SeoServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    public function setUp(): void\n    {\n        parent::setUp();\n\n        StructBuilder::$separator = PHP_EOL;\n        StructBuilder::$indent = null;\n    }\n\n    protected function getPackageProviders($app)\n    {\n        return [\n            SeoServiceProvider::class,\n        ];\n    }\n\n    protected function getPackageAliases($app)\n    {\n        return [\n            'Seo' => Seo::class,\n        ];\n    }\n\n    public static function assertMatchesRegularExpressionCustom(string $pattern, string $string, string $message = ''): void\n    {\n        // If parent has method assertMatchesRegularExpression, call it\n        if (method_exists(BaseTestCase::class, 'assertMatchesRegularExpression')) {\n            parent::assertMatchesRegularExpression($pattern, $string, $message);\n\n            return;\n        }\n\n        static::assertThat($string, new RegularExpression($pattern), $message);\n    }\n}\n"
  },
  {
    "path": "tests/ValueTypesTest.php",
    "content": "<?php\n\nnamespace romanzipp\\Seo\\Test;\n\nuse romanzipp\\Seo\\Helpers\\Hook;\nuse romanzipp\\Seo\\Structs\\Meta;\nuse romanzipp\\Seo\\Structs\\Title;\n\nclass ValueTypesTest extends TestCase\n{\n    public function testBodyNullValue()\n    {\n        seo()->add(\n            Title::make()->body(null)\n        );\n\n        $this->assertNull(seo()->getStructs()[0]->getBody()->data());\n    }\n\n    public function testBodyEmptyStringValue()\n    {\n        seo()->add(\n            Title::make()->body('')\n        );\n\n        $this->assertNull(seo()->getStructs()[0]->getBody()->data());\n    }\n\n    public function testZeroStringAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', '0')\n        );\n\n        $this->assertTrue('0' === seo()->getStructs()[0]->getAttributes()['name']->data());\n    }\n\n    // --- legacy\n\n    public function testZeroIntegerAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', 0)\n        );\n\n        $this->assertTrue('0' === seo()->getStructs()[0]->getAttributes()['name']->data());\n    }\n\n    public function testNullAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', null)\n        );\n\n        $this->assertNull(seo()->getStructs()[0]->getAttributes()['name']->data());\n    }\n\n    public function testEmptyStringAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', '')\n        );\n\n        $this->assertNull(seo()->getStructs()[0]->getAttributes()['name']->data());\n    }\n\n    public function testEmptySpaceStringAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', ' ')\n        );\n\n        $this->assertNull(seo()->getStructs()[0]->getAttributes()['name']->data());\n    }\n\n    public function testTrueBooleanAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', true)\n        );\n\n        $this->assertSame('1', seo()->getStructs()[0]->getAttributes()['name']->data());\n    }\n\n    public function testFalseBooleanAttributeValue()\n    {\n        seo()->add(\n            Meta::make()->attr('name', false)\n        );\n\n        $this->assertSame('0', seo()->getStructs()[0]->getAttributes()['name']->data());\n    }\n\n    public function testHookCallbackBodyType()\n    {\n        Title::hook(\n            Hook::make()\n                ->onBody()\n                ->callback(function ($body) {\n                    $this->assertTrue(is_string($body));\n\n                    return $body;\n                })\n        );\n\n        seo()->add(\n            Title::make()->body('My Title')\n        );\n\n        Title::clearHooks();\n    }\n\n    public function testHookCallbackNullableBodyType()\n    {\n        Title::hook(\n            Hook::make()\n                ->onBody()\n                ->callback(function ($body) {\n                    $this->assertNull($body);\n\n                    return $body;\n                })\n        );\n\n        seo()->add(\n            Title::make()->body(null)\n        );\n\n        Title::clearHooks();\n    }\n}\n"
  }
]