PHPackages                             backstage/laravel-seo-scanner - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. backstage/laravel-seo-scanner

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

backstage/laravel-seo-scanner
=============================

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

v5.21.3(2w ago)2695.9k↑72.2%29MITPHPPHP ^8.2|^8.3|^8.4|^8.5CI passing

Since Jan 6Pushed 1w ago4 watchersCompare

[ Source](https://github.com/backstagephp/laravel-seo-scanner)[ Packagist](https://packagist.org/packages/backstage/laravel-seo-scanner)[ Docs](https://github.com/backstagephp/laravel-seo-scanner)[ GitHub Sponsors](https://github.com/vormkracht10)[ RSS](/packages/backstage-laravel-seo-scanner/feed)WikiDiscussions main Synced 3d ago

READMEChangelog (10)Dependencies (38)Versions (110)Used By (0)

Laravel SEO Scanner
===================

[](#laravel-seo-scanner)

[![Total Downloads](https://camo.githubusercontent.com/bd356f7f30eccde7eddc76001bc0a715187e9dc21d43e06c8cd7b2a059cd37a0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6261636b73746167652f6c61726176656c2d73656f2d7363616e6e65722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/backstage/laravel-seo-scanner)[![Tests](https://github.com/backstagephp/laravel-seo-scanner/actions/workflows/run-tests.yml/badge.svg?branch=main)](https://github.com/backstagephp/laravel-seo-scanner/actions/workflows/run-tests.yml)[![PHPStan](https://github.com/backstagephp/laravel-seo-scanner/actions/workflows/phpstan.yml/badge.svg?branch=main)](https://github.com/backstagephp/laravel-seo-scanner/actions/workflows/phpstan.yml)[![GitHub release (latest by date)](https://camo.githubusercontent.com/8ead4bfbdcf15312c56f3d623753c8cfb1cddcaf3094039d0c67756b8566b17d/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f6261636b73746167657068702f6c61726176656c2d73656f2d7363616e6e6572)](https://camo.githubusercontent.com/8ead4bfbdcf15312c56f3d623753c8cfb1cddcaf3094039d0c67756b8566b17d/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f6261636b73746167657068702f6c61726176656c2d73656f2d7363616e6e6572)[![Packagist PHP Version Support](https://camo.githubusercontent.com/92c6bcec6e10b3f51b4056c9da7ace02aaaddf753ec3f99437fa80a3e0b577df/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6261636b73746167652f6c61726176656c2d73656f2d7363616e6e6572)](https://camo.githubusercontent.com/92c6bcec6e10b3f51b4056c9da7ace02aaaddf753ec3f99437fa80a3e0b577df/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6261636b73746167652f6c61726176656c2d73656f2d7363616e6e6572)[![Latest Version on Packagist](https://camo.githubusercontent.com/517622298e70da25eb216dc3474b422800233dfcbdf30972dbe2c9c6c1f28720/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6261636b73746167652f6c61726176656c2d73656f2d7363616e6e65722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/backstage/laravel-seo-scanner)

Nice to meet you, we're [Backstage](https://backstagephp.com)
-------------------------------------------------------------

[](#nice-to-meet-you-were-backstage)

Hi! We are a web development agency from Nijmegen in the Netherlands and we use Laravel for everything: advanced websites with a lot of bells and whitles and large web applications.

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

[](#the-laravel-tool-to-boost-the-seo-score-of-your-web-pages)

[![Screenshot 2023-01-05 at 15 02 31](https://user-images.githubusercontent.com/10845460/210797960-d65e260e-d543-4aec-aca8-1d9cca3aee96.png)](https://user-images.githubusercontent.com/10845460/210797960-d65e260e-d543-4aec-aca8-1d9cca3aee96.png)

Introduction
------------

[](#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 24 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](#minimum-requirements)
- [Installation](#installation)
- [Available checks](#available-checks)
    - [Configuration](#configuration)
    - [Content](#content)
    - [Meta](#meta)
    - [Performance](#performance)
- [Usage](#usage)
    - [Running the scanner in a local environment](#running-the-scanner-in-a-local-environment)
    - [Scanning routes](#scanning-routes)
    - [Scanning a single route](#scanning-a-single-route)
    - [Scanning routes in an SPA application](#scanning-routes-in-an-spa-application)
    - [Throttling](#throttling)
    - [Scanning large sites](#scanning-large-sites)
    - [Scan model urls](#scan-model-urls)
    - [Saving scans into the database](#saving-scans-into-the-database)
    - [Listening to events](#listening-to-events)
    - [Retrieving scans](#retrieving-scans)
    - [Retrieving scores](#retrieving-scores)
    - [Adding your own checks](#adding-your-own-checks)
- [Testing](#testing)
- [Changelog](#changelog)
- [Contributing](#contributing)
- [Security Vulnerabilities](#security-vulnerabilities)
- [Credits](#credits)
- [License](#license)

Minimum requirements
--------------------

[](#minimum-requirements)

- PHP 8.2 or higher (8.2, 8.3, 8.4, 8.5)
- Laravel 12.0 or higher (12.x, 13.x)

Installation
------------

[](#installation)

You can install the package via composer:

```
composer require backstage/laravel-seo-scanner
```

If you want to scan pages that are rendered using Javascript, for example Vue or React, you need to install Puppeteer. You can install it using the following command:

> If you want to know how to scan Javascript rendered pages, check out [Scanning routes in an SPA application](#scanning-routes-in-an-spa-application). Want to know more about Puppeteer? Check out the [Puppeteer documentation](https://pptr.dev/).

```
npm install puppeteer
```

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"
```

Click here to see the [config file](https://github.com/backstagephp/laravel-seo-scanner/blob/too-long-sentences-check/config/seo.php).

Available checks
----------------

[](#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

[](#configuration)

✅ The page does not have 'noindex' set.
✅ The page does not have 'nofollow' set.
✅ Robots.txt allows indexing.
✅ The site has a valid XML sitemap.

### AI

[](#ai)

✅ Known AI crawlers (GPTBot, ClaudeBot, PerplexityBot, ...) are not blocked in robots.txt.
✅ The site provides an llms.txt file.

### Content

[](#content)

✅ The page has an H1 tag and if it is used only once per page.
✅ The page has a logical heading structure (no skipped heading levels).
✅ All links redirect to an url using HTTPS.
✅ Every image has an alt attribute.
✅ Every image has explicit width and height attributes (prevents layout shift).
✅ Images below the fold use lazy loading.
✅ The page contains no broken links.
✅ The page contains no broken images.
✅ Length of the content is at least 2100 characters.
✅ No more than 20% of the content contains too long sentences (more than 20 words).
✅ A minimum of 30% of the sentences contain a transition word or phrase.

> Note: To change the locale of the transition words, you can publish the config file and change the locale in the config file. The default locale is `null` which uses the language of your `app` config. If set to `nl` or `en`, the transition words will be in Dutch or English. If you want to add more locales, you can create a pull request.

### Meta

[](#meta)

✅ The page has a meta description.
✅ The page title is not longer than 60 characters.
✅ The page has an Open Graph image.
✅ The page has a canonical URL.
✅ The page has complete Open Graph tags (og:title, og:description, og:url, og:type).
✅ The page has a Twitter card.
✅ The lang attribute is set on the html tag.
✅ The page has a valid viewport meta tag.
✅ The page has a character encoding declared.
✅ The page has a favicon.
✅ The page has valid hreflang annotations.
✅ The title contains one or more keywords.
✅ One or more keywords are present in the first paragraph.
✅ The page does not contain invalid HTML elements in the head section.
✅ The page contains structured data (JSON-LD).

### Performance

[](#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.
✅ Images use modern formats (WebP/AVIF).
✅ JavaScript files are not larger than 1 MB.
✅ CSS files are not larger than 15 KB.
✅ HTML is GZIP compressed.

### Security

[](#security)

✅ The page does not use a long redirect chain.
✅ The page sets recommended security headers (HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy).
✅ The page is served over HTTPS.

Usage
-----

[](#usage)

### Running the scanner in a local environment

[](#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

[](#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 Backstage\LaravelSeo\Jobs\Scan;

Scan::dispatch();
```

### Scanning a single route

[](#scanning-a-single-route)

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

```
php artisan seo:scan-url https://backstagephp.com
```

> 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.

### Output formats

[](#output-formats)

Both `seo:scan` and `seo:scan-url` support a `--format` option. The default is the human-readable console output. Use `--format=json` to get structured JSON instead, which is ideal for CI pipelines or AI agents:

```
php artisan seo:scan-url https://backstagephp.com --format=json
```

For `seo:scan-url` this outputs a single object; for `seo:scan` it outputs an array with one object per scanned URL. Each object looks like:

```
{
    "url": "https://backstagephp.com",
    "score": 87,
    "passed": 24,
    "failed": 3,
    "checks": {
        "passed": [
            {
                "check": "Backstage\\Seo\\Checks\\Meta\\TitleLengthCheck",
                "category": "Meta",
                "title": "The page title is not longer than 60 characters",
                "priority": "medium",
                "scoreWeight": 5
            }
        ],
        "failed": [
            {
                "check": "Backstage\\Seo\\Checks\\Meta\\CanonicalCheck",
                "category": "Meta",
                "title": "The page has a canonical URL",
                "priority": "medium",
                "scoreWeight": 3,
                "timeToFix": 5,
                "failureReason": "The page does not contain a canonical URL, while it should.",
                "actualValue": null,
                "expectedValue": null
            }
        ]
    }
}
```

### Scanning routes in an SPA application

[](#scanning-routes-in-an-spa-application)

If you have an SPA application, you can enable javascript rendering. This will use a headless browser to render the content. To enable javascript rendering, set the `javascript` option to `true` in the config file. You can also enable javascript rendering for a single route by adding the `--javascript` option to the command:

```
php artisan seo:scan-url https://backstagephp.com --javascript
```

> Note: This command will use Puppeteer to render the page. Make sure that you have Puppeteer installed on your system. You can install Puppeteer by running the following command: `npm install puppeteer`. **At this moment it's only available when scanning single routes.**

### PageSpeed Insights (Core Web Vitals)

[](#pagespeed-insights-core-web-vitals)

The package can pull the Google PageSpeed (Lighthouse) performance score and Core Web Vitals straight from the [PageSpeed Insights API](https://developers.google.com/speed/docs/insights/v5/get-started) — no third-party package required. These checks are **opt-in** because they call an external API and are slower and rate-limited.

To enable them:

1. Request a free API key and set it (for example via `.env`):

```
SEO_PAGESPEED_API_KEY=your-api-key
```

2. Remove the PageSpeed checks you want to run from the `exclude_checks` array in `config/seo.php`:

```
'exclude_checks' => [
    // \Backstage\Seo\Checks\PageSpeed\PerformanceScoreCheck::class,
    // \Backstage\Seo\Checks\PageSpeed\LcpCheck::class,
    // \Backstage\Seo\Checks\PageSpeed\ClsCheck::class,
],
```

You can configure the strategy (`mobile` or `desktop`) and request timeout under the `pagespeed` key in the config file. The three checks share a single API call per URL.

### Throttling

[](#throttling)

If you want to throttle the requests, you can set the `throttle` option to `true` in the config file. You can also set the amount of requests per minute by setting the `requests_per_minute` option in the config file.

```
'throttle' => [
    'enabled' => false,
    'requests_per_minute' => 10,
],
```

### Scanning large sites

[](#scanning-large-sites)

For large sites (think a webshop with thousands of product pages) a single synchronous run is slow and can hit the queue job timeout. The scanner can instead split the work into batched queue jobs and process them with multiple workers.

Run the scan as a batch of queued jobs:

```
php artisan seo:scan --queue
```

This creates one scan record, splits the routes and model records into chunks, and dispatches them as a `Bus::batch` of `ScanChunk` jobs. When the batch finishes, the scan record is finalized (page count, failed checks, duration) and the `ScanCompleted` event is fired.

Memory stays flat regardless of how many model records you have: records are read in batches using `lazyById()` rather than loaded all at once. The batch size is controlled by `chunk_size`, which is also the number of pages each queue job scans:

```
// config/seo.php
'chunk_size' => 100,
```

Dispatch the jobs onto a dedicated queue so you can scale its workers independently:

```
// config/seo.php
'queue' => 'seo',
```

```
# Run several workers against the seo queue to scan in parallel
php artisan queue:work --queue=seo
php artisan queue:work --queue=seo
php artisan queue:work --queue=seo
```

> Use a real queue connection (Redis, database, …) for parallel workers — the `sync` driver runs jobs inline and cannot run them in parallel.

**Throttling across parallel workers.** The `throttle` setting (see above) is enforced across all workers when scanning with `--queue`, using a cache-backed rate limiter. Because each job scans `chunk_size` pages, the limiter allows roughly `requests_per_minute / chunk_size` jobs per minute. For this to be shared between workers, use a shared cache store (Redis, Memcached or database) — not the `array` driver.

### Scan model urls

[](#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 Backstage\Seo\Traits\HasSeoScore;
use Backstage\Seo\SeoInterface;

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

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

    public function getUrlAttribute(): string
    {
        return 'https://backstagephp.com/' . $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();
```

2. 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

[](#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](#check-the-seo-score-of-a-model) section.

#### Pruning the database

[](#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](https://laravel.com/docs/10.x/eloquent#pruning-models) for more information about pruning the database.

### Listening to events

[](#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

[](#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 Backstage\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

[](#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 Backstage\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

[](#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 `Backstage\Seo\Interfaces\Check` interface.
2. Add the `Backstage\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

[](#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.

```
