HummingbirdUK main logo

Coding solutions to business problems

About us

We use code to create solutions to business challenges, bottle-necks and headaches.

If you think your business has a problem that can be solved through code, we are happy to chat things through without any obligation.

Get in touch

Automate Lighthouse testing in Laravel 10

Home / Blog / Automate Lighthouse testing in Laravel 10

Written by Giles Bennett

As part of the relaunch of our site to celebrate 10 years of HummingbirdUK we wanted to be sure that the site was as smooth and fast as possible.

Lighthouse is a great in-browser tool for checking how a page fares in five different categories, with scores out of 100 given for performance, accessibility, best practices, SEO and progressive web app (although our site does not operate as a PWA - yet - so the most we can aim for in that category is 60 out of 100).

Whilst it's quick and easy to run a Lighthouse audit from the browser, we wanted a way of doing it automatically. So a bit of digging.

Our site is relatively straightforward - we have a BlogPost model, which is associated with the database table which holds information about each of our blog posts. So the first step was to create a new table to hold information on each post's Lighthouse score.

CREATE TABLE `lighthouse_desktop_results` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `blog_post_id` bigint unsigned NOT NULL,
  `total` int GENERATED ALWAYS AS (((((`performance` + `accessibility`) + `best_practices`) + `seo`) + `pwa`)) STORED,
  `performance` int DEFAULT NULL,
  `accessibility` int DEFAULT NULL,
  `best_practices` int DEFAULT NULL,
  `seo` int DEFAULT NULL,
  `pwa` int DEFAULT NULL,
  `last_checked_at` timestamp NULL DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `lighthouse_results_blog_post_id_foreign` (`blog_post_id`),
  CONSTRAINT `lighthouse_results_blog_post_id_foreign` FOREIGN KEY (`blog_post_id`) REFERENCES `blog_post` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=145 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

You could, alterrnatively, do this as a Laravel migration. One thing to note is that the total table is a generated table which automatically adds together the scores for the five totals whenever they change.

We then also need a model for the LighthousDesktopResult, which is relatively straightfowrard :

protected $table="lighthouse_desktop_results";

protected $fillable = ['blog_post_id', 'performance', 'accessibility', 'best_practices', 'seo', 'pwa', 'last_checked_at'];

protected $casts = ['last_checked_at' => 'date'];

public function post() {
    return $this->belongsTo(BlogPost::class);
}

And finally we need to set up the relationship within our BlogPost model :

public function lighthouseDesktop() {
    return $this->hasOne(LighthouseDesktopResult::class);
}

With those in place, the next thing we need is Spatie's excellent Lighthouse package :

composer require spatie/lighthouse-php

Now we're ready to go. We've set ours up as a Laravel Command so that it can be run on-demand, or as a scheduled job via app/Console/Commands/Kernel.php :

protected $signature = 'lighthouse:desktop {id?}';

Within the handle method of the command first we need to check what, if any, ID was passed to the command. If there's no ID passed (it's an optional parameter, indicated by the presence of the question mark) then we want to get. Alternatively we can pass the ID of a specific blog post, or we can pass 'poor' as the parameter to get all blog posts which currently score less than the highest possible score (remember, for us this is 460 at the moment).

$id = $this->argument('id');
if($id) {
    switch($id) {
        case "poor" :
            $posts = BlogPost::whereHas('lighthouseDesktop', function($query) {
                $query->where('lighthouse_desktop_results.total', '<', 460);
            })->get();
            break;
        default :
            $posts = BlogPost::where('id', $id)->get();
            break;
    }
} else {
    $posts = BlogPost::all();
}

Assuming we end up with any posts in the $posts Collection at this point, we then want to loop through them, run the Lighthouse test for that page, extract the results and either create the entry in the lighthouse_desktop_results table, or update it if it already exists.

if($posts) {
    foreach($posts as $post) {
        $entry = LighthouseDesktopResult::firstOrCreate(['blog_post_id' => $post->id]);
        $result = Lighthouse::url('https://hummingbirduk.com/' . $post->slug)->run();
        $scores = $result->scores();
        $entry->performance = $scores['performance'];
        $entry->accessibility = $scores['accessibility'];
        $entry->best_practices = $scores['best-practices'];
        $entry->seo = $scores['seo'];
        $entry->pwa = $scores['pwa'];
        $entry->last_checked_at = Carbon::now()->format("Y-m-d H:i:s");
        $entry->save();
        echo "Results : Performance - " . $scores['performance'] . " | Accessibility - " . $scores['accessibility'] . " | Best Practices - " . $scores['best-practices'] . " | SEO - " . $scores['seo']  . " | PWA - " . $scores['pwa'] . "\n";
    }
}

The echo at the end isn't necessary, but since each test can take about 30 seconds to run, if you're running it against a batch of posts at a time, it's nice to get some visual feedback that it's chuntering away in the background. Remember, also, that since the total column in the results table is a generated table, it will update automatically based on the values in the other five columns, which avoids the need to have to update it manually. There is more information within the results should you want to extract it, but for our purposes the headline numbers are sufficient.

It's also worth mentioning that your site may generate slightly different results when testing on mobile, but using the following line allows you to run the Lighthouse test from the point of view of a mobile visitor :

$result = Lighthouse::url('https://hummingbirduk.com/' . $post->slug)->formFactor(FormFactor::Mobile)->run();

Always worth doing both, just to be on the safe side.

Author : Giles Bennett

About the author

Giles Bennett built his first website in 1996, and is old enough to miss Netscape Navigator. Initially a lawyer, he jumped ship to IT in 2008, and after 5 years as a freelancer, he founded HummingbirdUK in 2013. He can be reached by email at giles@hummingbirduk.com.