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