/laravel-seo-scanner

Scan your Laravel application routes for SEO improvements suggestions.

Primary LanguagePHPMIT LicenseMIT

Laravel SEO Scanner

Total Downloads Tests PHPStan GitHub release (latest by date) Packagist PHP Version Support Latest Version on Packagist

The Laravel tool to boost the SEO score of your web pages

Screenshot 2023-01-05 at 15 02 31

Introduction

This package is your guidance to get a better SEO score on search engines. Laravel SEO Scanner scans your code and crawls the routes from your app. The package has 21 checks that will check on performance, configurations, use of meta tags and content quality.

Easily configure which routes to scan, exclude or include specific checks or even add your own checks! Completing checks will further improve the SEO score and thus increase the chance of ranking higher at the search engines.

Minimum requirements

  • PHP 8.1 or higher
  • Laravel 9.0 or higher

Installation

You can install the package via composer:

composer require vormkracht10/laravel-seo-scanner

Run the install command to publish the config file and run the migrations:

php artisan seo:install

Or you can publish the config file and run the migrations manually:

php artisan vendor:publish --tag="seo-migrations"
php artisan migrate
php artisan vendor:publish --tag="seo-config"

This will be the contents of the published config file:

return [
    /*
    |--------------------------------------------------------------------------
    | Cache
    |--------------------------------------------------------------------------
    |
    | The following array lists the cache options for the application.
    |
    */
    'cache' => [
        // Only drivers that support tags are supported.
        // These are: array, memcached and redis.
        'driver' => 'array',
    ],

    /*
    |--------------------------------------------------------------------------
    | Check classes
    |--------------------------------------------------------------------------
    |
    | The following array lists the "check" classes that will be registered
    | with Laravel Seo. These checks run an check on the application via
    | various methods. Feel free to customize it.
    |
    | An example of a check class:
    | \Vormkracht10\Seo\Checks\Content\BrokenLinkCheck::class
    |
    */
    'checks' => ['*'],

    // If you wish to skip running some checks, list the classes in the array below.
    'exclude_checks' => [],

    /*
    |--------------------------------------------------------------------------
    | Check paths
    |--------------------------------------------------------------------------
    |
    | The following array lists the "checks" paths that will be searched
    | recursively to find check classes. This option will only be used
    | if the checks option above is set to the asterisk wildcard. The
    | key is the base namespace to resolve the class name.
    |
    */
    'check_paths' => [
        'Vormkracht10\\Seo\\Checks' => base_path('vendor/vormkracht10/laravel-seo-scanner-scanner/src/Checks'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Routes
    |--------------------------------------------------------------------------
    |
    | The following array lists the "checkable" routes that will be registered
    | with Laravel Seo. These routes will be checked for SEO. Feel free to
    | customize it. To check for specific routes, use the route name.
    |
    | An example of a checkable route:
    | 'blog.index'
    |
    */
    'check_routes' => true,
    'routes' => ['*'],

    // If you wish to skip running some checks on some routes, list the routes
    // in the array below by using the route name. For example:
    // 'blog.index'
    'exclude_routes' => [],

    // If you wish to skip running some checks on some paths, list the paths
    // in the array below.
    'exclude_paths' => [
        'admin/*',
        'nova/*',
        'horizon/*',
        'vapor-ui/*',
    ],

    /*
    |--------------------------------------------------------------------------
    | Database
    |--------------------------------------------------------------------------
    |
    | Here you can specify database related configurations like the connection 
    | that will be used to save the SEO scores. When you set the save 
    | option to true, the SEO score will be saved to the database. 
    |
    */
    'database' => [
        'connection' => 'mysql',
        'save' => true,
        'prune' => [
            'older_than_days' => 30,
        ]
    ],

    /*
    |--------------------------------------------------------------------------
    | Models
    |--------------------------------------------------------------------------
    |
    | Here you can specify which models you want to check. When you specify a
    | model, the SEO score will be saved to the database. This way you can
    | check the SEO score of a specific page.
    |
    | An example of a model:
    | \App\Models\BlogPost::class
    |
    */
    'models' => [],

    'http' => [
        /*
        |--------------------------------------------------------------------------
        | Http client options
        |--------------------------------------------------------------------------
        |
        | Here you can specify the options of the http client. For example, in a
        | local development environment you may want to disable the SSL
        | certificate integrity check.
        |
        | An example of a http option:
        | 'verify' => false
        |
        */
        'options' => [],

        /*
        |--------------------------------------------------------------------------
        | Http headers
        |--------------------------------------------------------------------------
        |
        | Here you can specify custom headers of the http client.
        |
        */
        'headers' => [
            'User-Agent' => 'Laravel SEO Scanner/1.0',
        ],
    ],
];

Available checks

These checks are available in the package. You can add or remove checks in the config file. These checks are based on SEO best practices and if all checks are green, your website will have a good SEO score. If you want to add more checks, you can create a pull request.

Configuration

✅ The page does not have 'noindex' set.
✅ The page does not have 'nofollow' set.
✅ Robots.txt allows indexing.

Content

✅ The page has an H1 tag and if it is used only once per page.
✅ All links redirect to an url using HTTPS.
✅ Every image has an alt attribute.
✅ The page contains no broken links.
✅ The page contains no broken images.
✅ Length of the content is at least 2100 characters.

Meta

✅ The page has a meta description.
✅ The page title is not longer than 60 characters.
✅ The page title does not contain 'home' or 'homepage'.
✅ The page has an Open Graph image.
✅ The lang attribute is set on the html tag.

Performance

✅ Time To First Byte (TTFB) is below 600ms.
✅ The page response returns a 200 status code.
✅ HTML is not larger than 100 KB.
✅ Images are not larger than 1 MB.
✅ JavaScript files are not larger than 1 MB.
✅ CSS files are not larger than 15 KB.
✅ HTML is GZIP compressed.

Usage

Running the scanner in a local environment

If you are using auto signed SSL certificates in your local development environment, you may want to disable the SSL certificate integrity check. You can do this by adding the following option to the http.options array in the config file:

'http' => [
    'options' => [
        'verify' => false,
    ],
],

It's also possible to pass custom headers to the http client. For example, if you want to set a custom user agent, you can add the following option to the http.headers array in the config file:

'http' => [
    'headers' => [
        'User-Agent' => 'My custom user agent',
    ],
],

Scanning routes

By default, all GET routes will be checked for SEO. If you want to check the SEO score of a specific route, you can add the route name to the routes array in the config file. If you want to skip a route, you can add the route name to the exclude_routes array in the config file. If you don't want to check the SEO score of routes at all, you can set the check_routes option to false in the config file.

To check the SEO score of your routes, run the following command:

php artisan seo:scan

If you want to queue the scan and trigger it manually you can dispatch the 'Scan' job:

use Vormkracht10\LaravelSeo\Jobs\Scan;

Scan::dispatch();

Scanning a single route

Want to get the score of a specific url? Run the following command:

php artisan seo:scan-url https://vormkracht10.nl

Note: The command will only check the SEO score of the url and output the score in the CLI. It will not save the score to the database.

Scan model urls

When you have an application where you have a lot of pages which are related to a model, you can save the SEO score to the model. This way you can check the SEO score of a specific page and show it in your application.

For example, you have a BlogPost model which has a page for each content item:

  1. Add the model to the models array in the config file.
  2. Implement the SeoInterface in your model.
  3. Add the HasSeoScore trait to your model.

Note: Please make sure that the model has a url attribute. This attribute will be used to check the SEO score of the model. Also check that the migrations are run. Otherwise the command will fail.

use Vormkracht10\Seo\Traits\HasSeoScore;
use Vormkracht10\Seo\SeoInterface;

class BlogPost extends Model implements SeoInterface
{
    use HasFactory,
        HasSeoScore;

    protected $fillable = [
        'title',
        'description',
        'slug',
        // ...
    ];

    public function getUrlAttribute(): string
    {
        return 'https://vormkracht10.nl/' . $this->slug;
    }
}

You can get the SEO score of a model by calling the seoScore() or seoScoreDetails() methods on the model. These methods are defined in the HasSeoScore trait and can be overridden by adding the modified method in your model.

To fill the database with the scores of all models, run the following command:

php artisan seo:scan

To get the SEO score(s) of a model, you have the following options:

  1. Get the SEO scores of a single model from the database:
$scores = Model::withSeoScores()->get();
  1. Run a SEO score check on a single model:
$model = Model::first();

// Get just the score
$score = $model->getCurrentScore();

// Get the score including the details
$scoreDetails = $model->getCurrentScoreDetails();

Saving scans into the database

When you want to save the SEO score to the database, you need to set the save option to true in the config file.

'database' => [
    'connection' => 'mysql',
    'save' => true,
    'prune' => [
        'older_than_days' => 30,
    ],
],

Optionally you can specify the database connection in the config file. If you want to save the SEO score to a model, you need to add the model to the models array in the config file. More information about this can be found in the Check the SEO score of a model section.

Pruning the database

Per default the package will prune the database from old scans. You can specify the number of days you want to keep the scans in the database. The default is 30 days.

If you want to prune the database, you need to add the prune command to your App\Console\Kernel:

protected function schedule(Schedule $schedule)
{
    // ...
    $schedule->command('model:prune')->daily();
}

Please refer to the Laravel documentation for more information about pruning the database.

Listening to events

When you run the seo:scan command, the package will fire an event to let you know it's finished. You can listen to this events and do something with the data. For example, you can send an email to the administrator when the SEO score of a page is below a certain threshold. Add the following code to your EventServiceProvider:

protected $listen = [
    // ...
    ScanCompleted::class => [
        // Add your listener here
    ],
];

Retrieving scans

You can retrieve the scans from the database by using the SeoScan model. This model is used to save the scans to the database. You can use the SeoScan model to retrieve the scans from the database. For example:

use Vormkracht10\Seo\Models\SeoScan;

// Get the latest scan
$scan = SeoScan::latest()->first();

// Get the failed checks
$failedChecks = $scan->failedChecks;

// Get the total amount of pages scanned
$totalPages = $scan->pages;

Retrieving scores

You can retrieve the scores from the database by using the SeoScore model. This model is used to save the scores to the database. You can use the SeoScore model to retrieve the scores from the database. For example:

use Vormkracht10\Seo\Models\SeoScore;

// Get the latest score
$score = SeoScore::latest()->first();

// Or get all scores for a specific scan
$scan = SeoScan::latest()->with('scores')->first();

Adding your own checks

You can add your own checks to the package. To do this, you need to create a check class in your application.

  1. Create a new class in your application which implements the Vormkracht10\Seo\Interfaces\Check interface.
  2. Add the Vormkracht10\Seo\Traits\PerformCheck trait to your class.
  3. Add the base path of your check classes to the check_paths array in the config file.

Example

In this example I make use of the symfony/dom-crawler package to crawl the HTML of a page as this is far more reliable than using preg_match for example. Feel free to use anything you want. The crawler is always passed to the check method, so you still need to define the $crawler parameter in your check method.

<?php

namespace App\Support\Seo\Checks;

use Illuminate\Http\Client\Response;
use Symfony\Component\DomCrawler\Crawler;
use Vormkracht10\Seo\Interfaces\Check;
use Vormkracht10\Seo\Traits\PerformCheck;

class CanonicalCheck implements Check
{
    use PerformCheck;

    /**
     * The name of the check.
     */
    public string $title = "The page has a canonical meta tag";

    /**
     * The priority of the check (in terms of SEO).
     */
    public string $priority = 'low';

    /**
     * The time it takes to fix the issue.
     */
    public int $timeToFix = 1;

    /**
     * The weight of the check. This will be used to calculate the score.
     */
    public int $scoreWeight = 2;

    /**
     * If this check should continue after a failure. You don't
     * want to continue after a failure if the page is not
     * accessible, for example.
     */
    public bool $continueAfterFailure = true;

    public string|null $failureReason;

    /* If you want to check the actual value later on make sure
     * to set the actualValue property. This will be used
     * when saving the results.
     */
    public mixed $actualValue = null;

    /* If you want to check the expected value later on make sure
     * to set the expectedValue property. This will be used
     * when saving the results.
     */
    public mixed $expectedValue = null;

    public function check(Response $response, Crawler $crawler): bool
    {
        // Feel free to use any validation you want here.
        if (! $this->validateContent($crawler)) {
            return false;
        }

        return true;
    }

    public function validateContent(Crawler $crawler): bool
    {
        // Get the canonical meta tag
        $node = $crawler->filterXPath('//link[@rel="canonical"]')->getNode(0);

        if (! $node) {
            // We set the failure reason here so this will be showed in the CLI and saved in the database.
            $this->failureReason = 'The canonical meta tag does not exist';
            return false;
        }

        // Get the href attribute
        $this->actualValue = $node->getAttribute('href');

        if (! $this->actualValue) {
            // The failure reason is different here because the canonical tag exists, but it does not have a href attribute.
            $this->failureReason = 'The canonical meta tag does not have a href attribute';

            return false;
        }

        // The canonical meta tag exists and has a href attribute, so the check is successful.
        return true;
    }
}

The config file:

return [
    // ...
    'check_paths' => [
        'Vormkracht10\\Seo\\Checks' => base_path('vendor/vormkracht10/laravel-seo-scanner/src/Checks'),
        'App\\Support\\Seo\\Checks' => base_path('app/Support/Seo/Checks'),
    ],
];

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.