├── .github └── workflows │ └── run-tests.yml ├── .styleci.yml ├── LICENSE.md ├── README.md ├── UPGRADING.md ├── composer.json ├── config └── f9web-laravel-meta.php └── src ├── Exceptions └── GuessorException.php ├── GuessesTitles.php ├── Meta.php ├── MetaFacade.php ├── MetaServiceProvider.php ├── Tags ├── Canonical.php ├── Description.php ├── Name.php ├── Property.php ├── Tag.php └── Title.php ├── TitleGuessor.php └── helpers.php /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: "Run Tests - Current" 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | php: [8.3, 8.2, 8.1, 8.0] 13 | laravel: ["^12.0", "^11.0", "^10.0", "^9.0", "^8.12"] 14 | dependency-version: [prefer-lowest, prefer-stable] 15 | include: 16 | - laravel: ^12.0 17 | testbench: 10.* 18 | - laravel: ^11.0 19 | testbench: 9.* 20 | - laravel: ^10.0 21 | testbench: 8.* 22 | - laravel: ^9.0 23 | testbench: 7.* 24 | - laravel: ^8.12 25 | testbench: ^6.23 26 | exclude: 27 | - laravel: ^8.12 28 | php: 8.3 29 | - laravel: ^10.0 30 | php: 8.0 31 | - laravel: ^11.0 32 | php: 8.0 33 | - laravel: ^11.0 34 | php: 8.1 35 | - laravel: ^12.0 36 | php: 8.0 37 | - laravel: ^12.0 38 | php: 8.1 39 | 40 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} 41 | 42 | steps: 43 | - name: Checkout code 44 | uses: actions/checkout@v4 45 | 46 | - name: Get Composer Cache Directory 47 | id: composer-cache 48 | run: | 49 | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 50 | 51 | - uses: actions/cache@v4 52 | with: 53 | path: ${{ steps.composer-cache.outputs.dir }} 54 | key: ${{ runner.os }}-composer-${{ matrix.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} 55 | restore-keys: | 56 | ${{ runner.os }}-composer- 57 | 58 | # - name: Cache dependencies 59 | # uses: actions/cache@v4 60 | # with: 61 | # path: ~/.composer/cache/files 62 | # key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 63 | 64 | - name: Setup PHP 65 | uses: shivammathur/setup-php@v2 66 | with: 67 | php-version: ${{ matrix.php }} 68 | extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv 69 | coverage: none 70 | 71 | - name: Install dependencies 72 | run: | 73 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" --no-interaction --no-update 74 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 75 | 76 | - name: Display PHP version 77 | run: php -v | grep ^PHP | cut -d' ' -f2 78 | 79 | - name: Execute tests 80 | run: vendor/bin/phpunit 81 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) F9WebLtd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://banners.beyondco.de/Laravel%20Meta.png?theme=light&packageManager=composer+require&packageName=f9webltd%2Flaravel-meta&pattern=brickWall&style=style_1&description=Render+meta+tags+within+your+Laravel+application%2C+using+a+fluent+API&md=1&showWatermark=0&fontSize=100px&images=code) 2 | 3 | [![Packagist Version](https://img.shields.io/packagist/v/f9webltd/laravel-meta?style=flat-square)](https://packagist.org/packages/f9webltd/laravel-meta) 4 | [![Run Tests - Current](https://github.com/f9webltd/laravel-meta/actions/workflows/run-tests.yml/badge.svg)](https://github.com/f9webltd/laravel-meta/actions/workflows/run-tests.yml) 5 | [![StyleCI Status](https://github.styleci.io/repos/264978205/shield)](https://github.styleci.io/repos/264978205) 6 | [![License](https://poser.pugx.org/f9webltd/laravel-meta/license)](https://packagist.org/packages/f9webltd/laravel-meta) 7 | 8 | # Laravel Meta Tags 9 | 10 | Easily render meta tags within your Laravel application, using a fluent API 11 | 12 | ## Features 13 | 14 | - Simple API 15 | - Render named, property, raw, Twitter card and OpenGraph type meta tags 16 | - [Optionally, render default tags on every request](#default-tags) 17 | - [Conditionally set tags](#conditionally-setting-tags) 18 | - [Macroable](#macroable-support) 19 | - There is no need to set meta titles for every controller method. The package can [optionally guess titles based on uri](#meta-title) segments or the current named route 20 | - Well documented 21 | - Tested, with 100% code coverage 22 | 23 | ## Requirements 24 | 25 | - PHP `^8.0` 26 | - Laravel `^8.12`, `^9.0`, `^10.0`, `^11.0` or `^12.0` 27 | 28 | ### Legacy Support / Upgrading 29 | 30 | For PHP `<8.0` and Laravel `<8.12` / support, use package version [`^1.7.7`](https://github.com/f9webltd/laravel-meta/tree/1.7.7) 31 | 32 | If upgrading from `^1.0`, see [UPGRADING](UPGRADING.md) for details. 33 | 34 | ## Installation 35 | 36 | ``` bash 37 | composer require f9webltd/laravel-meta 38 | ``` 39 | 40 | The package will automatically register itself. 41 | 42 | Optionally publish the configuration file by running: 43 | 44 | ```bash 45 | php artisan vendor:publish --provider="F9Web\Meta\MetaServiceProvider" --tag="config" 46 | ``` 47 | 48 | ## Documentation 49 | 50 | This package aims to make adding common meta tags to your Laravel application a breeze. 51 | 52 | ### Usage 53 | 54 | Within a controller: 55 | 56 | ```php 57 | meta() 58 | ->set('title', 'Buy widgets today') 59 | ->set('canonical', '/users/name') 60 | ->set('description', 'My meta description') 61 | ->set('theme-color', '#fafafa') 62 | ->noIndex(); 63 | ``` 64 | 65 | To output metadata add the following within a Blade layout file: 66 | 67 | ```php 68 | {!! meta()->toHtml() !!} 69 | ``` 70 | 71 | ```html 72 | Buy widgets today - Meta Title Append 73 | 74 | 75 | 76 | 77 | ``` 78 | 79 | Optionally, the `Meta` facade can be used as an alternative to `meta()` helper, generating the same output: 80 | 81 | ```php 82 | Meta::set('title', 'Buy widgets today') 83 | ->set('canonical', '/users/name') 84 | ->set('description', 'My meta description') 85 | ->set('theme-color', '#fafafa') 86 | ->noIndex(); 87 | ``` 88 | 89 | #### Quotes 90 | 91 | This package with handle double and single quotations within meta tag values as per Google recommendations. 92 | 93 | The follwog code: 94 | 95 | ```php 96 | Meta::set('description', 'We sell 20" industrial nails'); 97 | ``` 98 | 99 | Actual output: 100 | 101 | ```html 102 | 103 | ``` 104 | 105 | ### Conditionally Setting Tags 106 | 107 | The `when()` method can be used to conditionally set tags. A boolean condition (indicating if the closure should be executed) and a closure. The closure parameter is full instance of the meta class, meaning all methods are callable. 108 | 109 | ```php 110 | 111 | $noIndex = true; 112 | 113 | meta()->when($noIndex, function ($meta) { 114 | $meta->noIndex(); 115 | }); 116 | ``` 117 | 118 | The `when()` is fluent and can be called multiple times: 119 | 120 | ```php 121 | meta() 122 | ->set('title', 'the title') 123 | -when(true, fn ($meta) => $meta->set('og:description', 'og description')) 124 | -when(false, fn ($meta) => $meta->set('referrer', 'no-referrer-when-downgrade')) 125 | ->noIndex(); 126 | ``` 127 | 128 | ### Blade Directives 129 | 130 | Blade directives are available, as an alternative to using PHP function within templates. 131 | 132 | To render all metadata: 133 | 134 | ```html 135 | @meta 136 | ``` 137 | 138 | Render a specific meta tag by name: 139 | 140 | ```html 141 | @meta('title') 142 | ``` 143 | 144 | ### Additional tag types 145 | 146 | The package supports multiple tag types. 147 | 148 | #### Property type tags 149 | 150 | To create property type tags, append `property:` before the tag name. 151 | 152 | ```php 153 | meta()->set('property:fb:app_id', '1234567890'); 154 | ``` 155 | 156 | ```html 157 | 158 | ``` 159 | 160 | #### Twitter card tags 161 | 162 | To create twitter card tags, append `twitter:` before the tag name. 163 | 164 | ```php 165 | meta()->set('twitter:site', '@twitter_user'); 166 | ``` 167 | 168 | ```html 169 | 170 | ``` 171 | 172 | #### Open Graph tags 173 | 174 | To create Open Graph (or Facebook) tags, append `og:` before the tag name: 175 | 176 | ```php 177 | meta() 178 | ->set('og:title', 'My new site') 179 | ->set('og:url', 'http://site.co.uk/posts/hello.html'); 180 | ``` 181 | 182 | ```html 183 | 184 | 185 | ``` 186 | 187 | #### Other tag types 188 | 189 | To create other tag types, use the `raw()` method: 190 | 191 | ```php 192 | meta() 193 | ->setRawTag('') 194 | ->setRawTag(''); 195 | ``` 196 | 197 | ```html 198 | 199 | 200 | ``` 201 | 202 | ### Default tags 203 | 204 | It may be desirable to render static meta tags application wide. Optionally define common tags within `f9web-laravel-meta.defaults`. 205 | 206 | For example, defining the below defaults 207 | 208 | ```php 209 | 'defaults' => [ 210 | 'robots' => 'all', 211 | 'referrer' => 'no-referrer-when-downgrade', 212 | '', 213 | ], 214 | ``` 215 | 216 | will render the following on every page: 217 | 218 | ```html 219 | 220 | 221 | 222 | ``` 223 | 224 | ### Helper methods 225 | 226 | #### `get()` 227 | 228 | Fetch a specific tag value by name. 229 | 230 | ```php 231 | meta()->set('title', 'meta title'); 232 | 233 | meta()->get('title'); // meta title 234 | ``` 235 | 236 | `null` is returned for none existent tags. 237 | 238 | #### `render()` 239 | 240 | Render all defined tags. `render()` is called when rendering tags within Blade files. 241 | 242 | The below calls are identical. 243 | 244 | ```php 245 | {!! meta()->toHtml() !!} 246 | {!! meta()->render() !!} 247 | ``` 248 | 249 | Passing a tag title to `render()` will render that tag. 250 | 251 | ```php 252 | meta()->set('title', 'meta title'); 253 | 254 | meta()->render('title'); // meta title 255 | ``` 256 | 257 | #### `fromArray()` 258 | 259 | Generate multiple tags from an array of tags. 260 | 261 | ```php 262 | meta() 263 | ->fromArray([ 264 | 'viewport' => 'width=device-width, initial-scale=1.0', 265 | 'author' => 'John Joe', 266 | 'og:title' => 'When Great Minds Dont Think Alike', 267 | 'twitter:title' => 'Using Laravel 7', 268 | ]); 269 | ``` 270 | 271 | ```html 272 | 273 | 274 | 275 | 276 | Users - Edit - Meta Title Append 277 | ``` 278 | 279 | #### `setRawTags()` 280 | 281 | Generate multiple raw tags from an array. 282 | 283 | ```php 284 | meta() 285 | ->setRawTags([ 286 | '', 287 | '', 288 | '' 289 | ]); 290 | ``` 291 | 292 | ```html 293 | 294 | 295 | 296 | ``` 297 | 298 | #### `tags()` 299 | 300 | Fetch all tags as an array. 301 | 302 | ```php 303 | meta() 304 | ->set('title', 'meta title') 305 | ->set('og:title', 'og title'); 306 | 307 | $tags = meta()->tags(); 308 | 309 | /* 310 | [ 311 | "title" => "meta title" 312 | "og:title" => "og title" 313 | ]; 314 | */ 315 | ``` 316 | 317 | #### `purge()` 318 | 319 | Remove all previously set tags. 320 | 321 | #### `forget()` 322 | 323 | Remove a previously set tag by title. 324 | 325 | ```php 326 | meta() 327 | ->set('title', 'meta title') 328 | ->set('og:title', 'og title'); 329 | 330 | meta()->forget('title'); 331 | 332 | $tags = meta()->tags(); 333 | 334 | // ["og:title" => "og title"]; 335 | ``` 336 | 337 | #### `noIndex()` 338 | 339 | Generate the necessary tags to exclude the url from search engines. 340 | 341 | ```php 342 | meta()->noIndex(); 343 | ``` 344 | 345 | ```html 346 | 347 | ``` 348 | 349 | #### `favIcon()` 350 | 351 | Generate the necessary tags for a basic favicon. The favicon path can be specified within the `f9web-laravel-meta.favicon-path` configuration value. 352 | 353 | ```php 354 | meta()->favIcon(); 355 | ``` 356 | 357 | ```html 358 | 359 | 360 | ``` 361 | 362 | ### Dynamic Calls 363 | 364 | For improved readability, it is possible to make dynamic method calls. The below codes blocks would render identical HTML: 365 | 366 | ```php 367 | meta() 368 | ->title('meta title') 369 | ->description('meta description') 370 | ->canonical('/users/me'); 371 | ``` 372 | 373 | ```php 374 | meta() 375 | ->set('title', 'meta title') 376 | ->set('description', 'meta description') 377 | ->set('canonical', '/users/me'); 378 | ``` 379 | 380 | ### Macroable Support 381 | 382 | The package implements Laravel's `Macroable` trait, meaning additional methods can be added the main Meta service class at run time. For example, [Laravel's collection class is macroable](For furtherinformatioin see the following samples 383 | ). 384 | 385 | The `noIndex` and `favIcon` helpers are defined as macros within the [package service provider](src/MetaServiceProvider.php). 386 | 387 | Sample macro to set arbitrary defaults tags for SEO: 388 | 389 | ```php 390 | // within a service provider 391 | Meta::macro('seoDefaults', function () { 392 | return Meta::favIcon() 393 | ->set('title', $title = 'Widgets, Best Widgets') 394 | ->set('og:title', $title) 395 | ->set('description', $description = 'Buy the best widgets from Acme Co.') 396 | ->set('og:description', $description) 397 | ->fromarray([ 398 | 'twitter:card' => 'summary', 399 | 'twitter:site' => '@roballport', 400 | ]); 401 | }); 402 | ``` 403 | 404 | To call the newly defined macro: 405 | 406 | ```php 407 | meta()->seoDefaults(); 408 | ``` 409 | 410 | Macros can also accept arguments. 411 | 412 | ```php 413 | Meta::macro('setPaginationTags', function (array $data) { 414 | $page = $data['page'] ?? 1; 415 | 416 | if ($page > 1) { 417 | Meta::setRawTag(''); 418 | } 419 | 420 | if (!empty($data['next'])) { 421 | return Meta::setRawTag(''); 422 | } 423 | 424 | return Meta::instance(); 425 | }); 426 | ``` 427 | 428 | ```php 429 | meta()->setPaginationTags([ 430 | 'page' => 7, 431 | 'next' => '/users/page/8', 432 | 'prev' => '/users/page/6', 433 | ]); 434 | ``` 435 | 436 | To allow for fluent method calls ensure the macro returns an instance of the class. 437 | 438 | 439 | ### Special tags 440 | 441 | #### Meta title 442 | 443 | The package ensures a meta tag is always present. Omitting a title will force the package to guess one based upon the current named route or uri. 444 | 445 | The set the preferred method, edit the `f9web-laravel-meta.title-guessor.method` configuration value. 446 | 447 | ##### `uri` method sample 448 | 449 | - if the uri is `/orders/create` thr guessed title is "Orders - Create" 450 | - if the uri is `/orders/9999/edit` thr guessed title is "Orders - 9999 - Edit" 451 | 452 | ##### `route` method sample 453 | 454 | - current named route is `users.create`, guessed title 'Users - Create' 455 | - current named route is `users.index`, guessed title 'Users' 456 | 457 | This behaviour can be disabled via editing the `f9web-laravel-meta.title-guessor.enabled` configuration value. 458 | 459 | This automatic resolution is useful in large applications, where it would be otherwise cumbersome to set metadata for every controller method. 460 | 461 | ##### Appending text to the meta title 462 | 463 | Typically, common data such as the company name is appended to meta titles. 464 | 465 | The `f9web-laravel-meta.meta-title-append` configuration value can be set to append the given string automatically to every meta title. 466 | 467 | To disable this behaviour set `f9web-laravel-meta.meta-title-append` to `null`. 468 | 469 | ##### Limiting the meta title length 470 | 471 | For SEO reasons, the meta title length should be restricted. This package, by default, limits the title to 60 characters. 472 | 473 | To change this behaviour update the configuration value of `f9web-laravel-meta.title-limit`. Set to `null` to stop limiting. 474 | 475 | #### Meta description 476 | 477 | For SEO reasons, the meta description should typically remain less than ~160 characters. This package, by default, does not limit the length. 478 | 479 | To change the limit adjust the configuration value `f9web-laravel-meta.description-limit`. Set to `null` to stop limiting. 480 | 481 | #### Canonical 482 | 483 | It is important to set a sensible [canonical](https://ahrefs.com/blog/canonical-tags/). Optionally, the package can automatically replace user defined strings when generating a canonical. 484 | 485 | Due to incorrect setup some Laravel installations allow `public` and/or `index.php` within the url. 486 | 487 | For instance, `/users/create`, `/public/users/create` and `/public/index.php/users/create` would both be visitable, crawlable and ultimately indexable urls. 488 | 489 | By editing the array of removable url strings within `f9web-laravel-meta.removable-uri-segments`, this behaviour can be controlled. 490 | 491 | The package will strip `public` and `index.php` from canonical urls automatically, as a default. 492 | 493 | ## Contribution 494 | 495 | Any ideas are welcome. Feel free to submit any issues or pull requests. 496 | 497 | ## Testing 498 | 499 | ``` bash 500 | composer test 501 | ``` 502 | 503 | ## Security 504 | 505 | If you discover any security related issues, please email rob@f9web.co.uk instead of using the issue tracker. 506 | 507 | ## Credits 508 | 509 | - [Rob Allport](https://github.com/ultrono) 510 | 511 | ## License 512 | 513 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 514 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | ## From 2.x to 3.x 4 | 5 | - This package now **automatically** encoded strings using `htmlentities`. This is a breaking change as already encoded strings may be passed to this package. See the [package readme](https://github.com/f9webltd/laravel-meta#quotes) for an updated usage example 6 | - No further changes are required 7 | 8 | ## From 1.x to 2.x 9 | 10 | - Run the following command to fetch the latets version of the package: `composer require f9webltd/laravel-meta:^2.0` 11 | - The package now requires PHP `^8.0` and Laravel `^8.12` / `^9.0`, `^10.0` or `^11.0` 12 | - The `meta()` helper function now **only** returns an instance of the `Meta` class, this is breaking change. Previously the helper acted as a short cut to fetch a specific tag. For example, `meta('title')` was previously the same as `meta()->get('title')`. This behaviour was removed for simplicity and to help with autocompletion. If using this function calls in the format `meta('title')` shoulod be adjusted to `meta()->get('title')` 13 | - No further changes are required 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "f9webltd/laravel-meta", 3 | "description": "Render meta tags in your Laravel application", 4 | "keywords": [ 5 | "laravel", 6 | "laravel meta", 7 | "laravel meta tags", 8 | "meta", 9 | "meta tags", 10 | "laravel seo", 11 | "laravel open graph tags", 12 | "laravel header meta tags" 13 | ], 14 | "homepage": "https://github.com/f9webltd/laravel-meta", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Rob Allport", 19 | "email": "rob@f9web.co.uk", 20 | "homepage": "https://www.f9web.co.uk", 21 | "role": "Developer" 22 | } 23 | ], 24 | "require": { 25 | "php": "^8.0", 26 | "illuminate/config": "^8.12|^9.0|^10.0|^11.0|^12.0", 27 | "illuminate/support": "^8.12|^9.0|^10.0|^11.0|^12.0" 28 | }, 29 | "require-dev": { 30 | "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0", 31 | "phpunit/phpunit": "^9.4|^10.1|^11.5.3" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "F9Web\\Meta\\": "src" 36 | }, 37 | "files": [ 38 | "src/helpers.php" 39 | ] 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "F9Web\\Meta\\Tests\\": "tests" 44 | } 45 | }, 46 | "scripts": { 47 | "test": "vendor/bin/phpunit" 48 | }, 49 | "config": { 50 | "sort-packages": true 51 | }, 52 | "extra": { 53 | "laravel": { 54 | "providers": [ 55 | "F9Web\\Meta\\MetaServiceProvider" 56 | ], 57 | "aliases": { 58 | "Meta": "F9Web\\Meta\\MetaFacade" 59 | } 60 | } 61 | }, 62 | "minimum-stability": "dev", 63 | "prefer-stable": true 64 | } 65 | -------------------------------------------------------------------------------- /config/f9web-laravel-meta.php: -------------------------------------------------------------------------------- 1 | [ 18 | // '', 19 | // 'robots' => 'noindex nofollow', 20 | // 'viewport' => 'width=device-width, initial-scale=1, shrink-to-fit=no', 21 | // 'referrer' => 'no-referrer-when-downgrade', 22 | // 'twitter:site' => '@user', 23 | // 'og:url' => 'http://site.co.uk/posts/hello.html', 24 | ], 25 | 26 | 'title-guessor' => [ 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Meta title guessing 31 | |-------------------------------------------------------------------------- 32 | | 33 | | Enable or disable meta title guessing. This is useful for large systems 34 | | with lots of controllers with consistent restful urls, where setting 35 | | a meta title within every controller method is undesirable. 36 | | 37 | */ 38 | 39 | 'enabled' => true, 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Guessing method 44 | |-------------------------------------------------------------------------- 45 | | 46 | | Can be "route" or "uri". "route" will guess based on current route name. 47 | | "uri" will guess based on the current uri. For example, if the uri is 48 | | "users/99/edit" the title would be "Users - 23 - Edit". If the route 49 | | name "users.profile", the title would be "Users - Profile". 50 | | 51 | */ 52 | 53 | 'method' => 'uri', 54 | ], 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Url segments to always remove from the canonical url 59 | |-------------------------------------------------------------------------- 60 | | 61 | | An array of uri components to automatically remove when rendering 62 | | the canonical tag 63 | | 64 | */ 65 | 66 | 'removable-uri-segments' => [ 67 | '/public', 68 | '/index.php', 69 | ], 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Limit title meta tag length 74 | |-------------------------------------------------------------------------- 75 | | 76 | | Google typically displays the first 50–60 characters of a title tag. To 77 | | avoid limiting the title set this value to null. 78 | | 79 | */ 80 | 81 | 'meta-title-append' => env('APP_NAME'), 82 | 83 | /* 84 | |-------------------------------------------------------------------------- 85 | | Title tag replacements 86 | |-------------------------------------------------------------------------- 87 | | 88 | | Optionally replace specific title characters. By default, no characters 89 | | are replaced. 90 | | 91 | */ 92 | 'meta-title-replacements' => [ 93 | 'enabled' => true, 94 | 95 | 'search' => [ 96 | '', 97 | ], 98 | 99 | 'replace' => [ 100 | ' ', 101 | ] 102 | ], 103 | 104 | /* 105 | |-------------------------------------------------------------------------- 106 | | Fallback meta title 107 | |-------------------------------------------------------------------------- 108 | | 109 | | Value used when a meta title has not been set and automatic 110 | | guessing is disabled 111 | | 112 | */ 113 | 114 | 'fallback-meta-title' => env('APP_NAME'), 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | Favicon path 119 | |-------------------------------------------------------------------------- 120 | | 121 | | Optional, used when the "favIcon()" helper is called 122 | | 123 | */ 124 | 125 | 'favicon-path' => '/favicon.ico', 126 | 127 | /* 128 | |-------------------------------------------------------------------------- 129 | | Limit title meta tag length 130 | |-------------------------------------------------------------------------- 131 | | 132 | | Google typically displays the first 50–60 characters of a title tag. To 133 | | avoid limiting the title set this value to null. 134 | | 135 | */ 136 | 137 | 'title-limit' => 60, 138 | 139 | /* 140 | |-------------------------------------------------------------------------- 141 | | Limit description meta tag length 142 | |-------------------------------------------------------------------------- 143 | | 144 | | Meta descriptions can be any length, but Google generally truncates 145 | | snippets to ~155–160 characters. Keep in mind that the "optimal" 146 | | length will vary depending on the situation, and your primary 147 | | goal should be to provide value and drive clicks. Set to null 148 | | to remove any limiting 149 | | 150 | */ 151 | 152 | 'description-limit' => null, 153 | ]; 154 | -------------------------------------------------------------------------------- /src/Exceptions/GuessorException.php: -------------------------------------------------------------------------------- 1 | withUri(Request::path()) 23 | ->withRoute(optional(Route::current())->getName()) 24 | ->render(); 25 | 26 | if ($title === '' && ($fallback = config('f9web-laravel-meta.fallback-meta-title'))) { 27 | return $fallback; 28 | } 29 | 30 | return $title; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Meta.php: -------------------------------------------------------------------------------- 1 | push($value); 59 | 60 | return self::instance(); 61 | } 62 | 63 | public static function instance(): ?self 64 | { 65 | if (self::$_instance === null) { 66 | self::$_instance = new self(); 67 | } 68 | 69 | return self::$_instance; 70 | } 71 | 72 | public static function forget(string $tag): self 73 | { 74 | if (null !== self::$tags && self::$tags->has($tag)) { 75 | self::$tags->pull($tag); 76 | } 77 | 78 | return self::instance(); 79 | } 80 | 81 | public static function purge(): self 82 | { 83 | self::$tags = new Collection(); 84 | self::$rawTags = new Collection(); 85 | 86 | return self::instance(); 87 | } 88 | 89 | /** 90 | * A helper method used within a testing context only. 91 | * To reset tags use the purge() method. 92 | * 93 | * @return \F9Web\Meta\Meta 94 | * @see Meta::purge() To reset all tags 95 | */ 96 | public static function resetTags(): self 97 | { 98 | self::$tags = null; 99 | self::$rawTags = []; 100 | 101 | return self::instance(); 102 | } 103 | 104 | public static function fromArray(array $items = []): self 105 | { 106 | foreach ($items as $key => $value) { 107 | self::set($key, (string)$value); 108 | } 109 | 110 | return self::instance(); 111 | } 112 | 113 | public static function set(string $key, string $value): self 114 | { 115 | if (self::$tags === null) { 116 | self::$tags = new Collection(); 117 | } 118 | 119 | if (str_contains($value, '"') || str_contains($value, '\'')) { 120 | $value = htmlentities($value, ENT_QUOTES, 'UTF-8'); 121 | } 122 | 123 | self::$tags->put($key, $value); 124 | 125 | return self::instance(); 126 | } 127 | 128 | public static function when(bool $condition, Closure $callback): self 129 | { 130 | $instance = self::instance(); 131 | 132 | return tap( 133 | $instance, 134 | function ($instance) use ($callback, $condition) { 135 | return $condition ? $callback($instance) : $instance; 136 | } 137 | ); 138 | } 139 | 140 | public function tags(): array 141 | { 142 | if (null === self::$tags) { 143 | self::$tags = new Collection(); 144 | } 145 | 146 | return self::$tags->concat(self::$rawTags)->toArray(); 147 | } 148 | 149 | public function toHtml(): string 150 | { 151 | return self::render(); 152 | } 153 | 154 | public static function render(?string $tag = null): string 155 | { 156 | // ensure a meta title is always set 157 | if (!Arr::get(self::$tags ?? [], 'title')) { 158 | self::setDefaultTitle(); 159 | } 160 | 161 | // register default tags on each request 162 | foreach (config('f9web-laravel-meta.defaults') as $key => $value) { 163 | if (!Arr::has(self::$tags, $key)) { 164 | if (is_int($key)) { 165 | self::setRawTag($value); 166 | } else { 167 | self::set($key, $value); 168 | } 169 | } 170 | } 171 | 172 | $tags = self::$tags; 173 | 174 | // render a specific tag if provided 175 | if (null !== $tag && isset($tags[$tag])) { 176 | return (self::getContent($tags[$tag] ?? '', $tag))->toHtml(); 177 | } 178 | 179 | return implode( 180 | PHP_EOL, 181 | $tags 182 | ->map( 183 | function ($content, $name) { 184 | return self::getContent($content ?? '', $name); 185 | } 186 | ) 187 | ->concat(self::$rawTags) 188 | ->toArray() 189 | ); 190 | } 191 | 192 | public static function getContent(string $value, string $tag): HtmlString 193 | { 194 | $tags = self::$tags; 195 | 196 | // a dedicated tag class with same name as the key exists 197 | $class = __NAMESPACE__ . '\\Tags\\' . ucwords($tag); 198 | 199 | if (class_exists($class)) { 200 | return (new $class())->render($tag, $value, $tags); 201 | } 202 | 203 | // the key starts with "property:" or "og:" - register a property type tag 204 | if (Str::startsWith($tag, ['property:', 'og:'])) { 205 | return (new Property())->render($tag, $value, $tags); 206 | } 207 | 208 | // render a default meta name/content tag 209 | return (new Name())->render($tag, $value, $tags); 210 | } 211 | 212 | /** 213 | * @param $name 214 | * @return \Illuminate\Support\Collection|string|null 215 | */ 216 | public function __get($name) 217 | { 218 | return self::get($name); 219 | } 220 | 221 | /** 222 | * @param string|null $name 223 | * @return \Illuminate\Support\Collection|string|null 224 | */ 225 | public static function get(?string $name = null) 226 | { 227 | if (null === $name) { 228 | return self::$tags; 229 | } 230 | 231 | return self::$tags->get($name); 232 | } 233 | 234 | /** 235 | * @param string $method 236 | * @param mixed $parameters 237 | * @return \F9Web\Meta\Meta|null 238 | */ 239 | public function __call(string $method, $parameters): ?self 240 | { 241 | if (static::hasMacro($method)) { 242 | return $this->macroCall($method, $parameters); 243 | } 244 | 245 | if ($method === 'raw') { 246 | return self::setRawTag($parameters[0]); 247 | } 248 | 249 | return self::set($method, $parameters[0]); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/MetaFacade.php: -------------------------------------------------------------------------------- 1 | publishes( 16 | [ 17 | __DIR__ . '/../config/f9web-laravel-meta.php' => config_path('f9web-laravel-meta.php'), 18 | ], 19 | 'config' 20 | ); 21 | 22 | $this->mergeConfigFrom(__DIR__ . '/../config/f9web-laravel-meta.php', 'f9web-laravel-meta'); 23 | 24 | $this->registerBladeDirectives(); 25 | 26 | $this->registerMacros(); 27 | } 28 | 29 | public function register() 30 | { 31 | $this->app->singleton(Meta::class); 32 | 33 | $this->app->alias(Meta::class, 'meta'); 34 | } 35 | 36 | private function registerMacros(): void 37 | { 38 | Meta::macro( 39 | 'noIndex', 40 | function () { 41 | Meta::forget('robots'); 42 | return Meta::set('robots', 'noindex nofollow'); 43 | } 44 | ); 45 | 46 | Meta::macro( 47 | 'favIcon', 48 | function (?string $src = null) { 49 | Meta::set('shortcut icon', ($icon = $src ?? config('f9web-laravel-meta.favicon-path'))); 50 | 51 | return Meta::setRawTag(''); 52 | } 53 | ); 54 | } 55 | 56 | private function registerBladeDirectives(): void 57 | { 58 | Blade::directive( 59 | 'meta', 60 | function ($expression) { 61 | return sprintf('render(%s); ?>', $expression); 62 | } 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Tags/Canonical.php: -------------------------------------------------------------------------------- 1 | ', $key, $url) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Tags/Description.php: -------------------------------------------------------------------------------- 1 | ', 25 | $key, 26 | Str::limit($value, config('f9web-laravel-meta.description-limit') ?? 9999, null) 27 | ) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Tags/Name.php: -------------------------------------------------------------------------------- 1 | ', $key, $value) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Tags/Property.php: -------------------------------------------------------------------------------- 1 | ', 23 | str_replace('property:', '', $key), 24 | $value 25 | ) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Tags/Tag.php: -------------------------------------------------------------------------------- 1 | %s', $config['fallback-meta-title']) 28 | ); 29 | } 30 | 31 | if ( 32 | isset($config['meta-title-replacements']['enabled']) && 33 | $config['meta-title-replacements']['enabled'] === true 34 | ) { 35 | $title = str_replace( 36 | $config['meta-title-replacements']['search'] ?? [], 37 | $config['meta-title-replacements']['replace'] ?? [], 38 | $title 39 | ); 40 | } 41 | 42 | if ($append = $config['meta-title-append']) { 43 | $title .= ' - ' . $append; 44 | } 45 | 46 | $title = Str::limit($title, $config['title-limit'] ?? 999, null); 47 | 48 | return new HtmlString(sprintf('%s', $title)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/TitleGuessor.php: -------------------------------------------------------------------------------- 1 | setMethod($config['method']); 41 | 42 | if (!in_array($method = $this->getMethod(), ['route', 'uri'])) { 43 | throw new GuessorException(); 44 | } 45 | 46 | if ($method === 'uri' && $this->uri !== null) { 47 | return $this->getFromUri(); 48 | } 49 | 50 | return $this->getFromRoute(); 51 | } 52 | 53 | /** 54 | * @return string|null 55 | */ 56 | private function getFromRoute(): ?string 57 | { 58 | $routeName = str_replace('.index', '', $this->route ?? ''); 59 | 60 | return ucwords(str_replace('.', ' - ', $routeName)); 61 | } 62 | 63 | /** 64 | * @return string|null 65 | */ 66 | private function getFromUri(): ?string 67 | { 68 | return $this->getUriSegments()->map( 69 | function ($segment) { 70 | return ucwords($segment); 71 | } 72 | )->implode(' - '); 73 | } 74 | 75 | /** 76 | * @return \Illuminate\Support\Collection 77 | */ 78 | private function getUriSegments(): Collection 79 | { 80 | $url = parse_url($this->uri)['path'] ?? '/'; 81 | 82 | return collect(explode('/', $url))->filter(); 83 | } 84 | 85 | /** 86 | * @param string $uri 87 | * @return $this 88 | */ 89 | public function withUri(string $uri): self 90 | { 91 | $this->uri = $uri; 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * @param string|null $route 98 | * @return $this 99 | */ 100 | public function withRoute(?string $route = null): self 101 | { 102 | $this->route = $route; 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * @return string|null 109 | */ 110 | public function getRoute(): ?string 111 | { 112 | return $this->route; 113 | } 114 | 115 | /** 116 | * @param string|null $method 117 | * @return $this 118 | */ 119 | public function setMethod(?string $method = null): self 120 | { 121 | $this->method = $method; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * @return string 128 | */ 129 | public function getMethod(): string 130 | { 131 | return $this->method; 132 | } 133 | 134 | /** 135 | * @return $this 136 | */ 137 | public function reset(): self 138 | { 139 | $this->method = 'uri'; 140 | $this->route = null; 141 | 142 | return $this; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 |