artisanpack-ui / pagespeed-insights
Google PageSpeed Insights testing, score history, and UI components for ArtisanPack UI.
Package info
github.com/ArtisanPack-UI/pagespeed-insights
pkg:composer/artisanpack-ui/pagespeed-insights
Requires
- php: ^8.2
- artisanpack-ui/core: ^1.0
- artisanpack-ui/google: ^1.0
- artisanpack-ui/hooks: ^1.2
- guzzlehttp/guzzle: ^7.5
- illuminate/database: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- artisanpack-ui/code-style: ^1.1
- artisanpack-ui/code-style-pint: ^1.1
- artisanpack-ui/livewire-ui-components: ^2.1
- dealerdirect/phpcodesniffer-composer-installer: ^1.0
- friendsofphp/php-cs-fixer: ^3.75
- laravel/pint: ^1.26
- livewire/livewire: ^3.6
- orchestra/testbench: ^10.2|^11.0
- pestphp/pest: ^3.8|^4.0
- pestphp/pest-plugin-laravel: ^3.2|^4.1
Suggests
- artisanpack-ui/cms-framework: Stores the PageSpeed API key via the CMS Settings module and registers the components as admin dashboard widgets.
- artisanpack-ui/livewire-ui-components: Renders the PageSpeed score, Core Web Vitals, and trend panels with x-artisanpack-* components.
- livewire/livewire: Required for the Blade / Livewire score, Core Web Vitals, opportunity, and trend components and the CMS-framework AdminWidget bridge.
This package is auto-updated.
Last update: 2026-08-05 16:12:43 UTC
README
Google PageSpeed Insights testing, score history, and drop-in UI components for the ArtisanPack UI ecosystem.
Google's PageSpeed Insights UI tells you how a page performs right now. This package adds the part it leaves out: history. Scheduled, queued re-tests build a per-URL score timeline, so a performance regression shows up as a visible trend — and as an alert — rather than as a hunch.
- Scheduled PSI runs against a managed list of URLs, with full result history
- Lighthouse category scores, lab metrics, and CrUX field data stored per URL and strategy
- Livewire, React, and Vue components, plus CMS-framework admin widgets
- Regression alerts — and alerts for the URLs that stop reporting at all — through standard Laravel notifications
- A CI-friendly
pagespeed:testcommand with score budgets and one exit code per failure class - Hooks so other packages can register URLs and consume results
Requirements
- PHP 8.2+
- Laravel 12 or 13
- A PageSpeed Insights API key — see API key; the package cannot run without one
artisanpack-ui/google^1.0 andartisanpack-ui/hooks^1.2 — installed automatically- Livewire ^3.6 (optional) — required only for the Blade / Livewire components and the CMS-framework admin widget bridge
Installation
composer require artisanpack-ui/pagespeed-insights php artisan migrate
The service provider and the PageSpeedInsights facade are auto-discovered.
Full details, publish tags, and the queue/scheduler wiring: docs/installation.md.
API key
The PageSpeed Insights API is quota-limited per project. Keyless requests are not a workable fallback — the shared anonymous project has a daily quota of zero, so a request without a key answers HTTP 429 every time. A key is required.
Creating one
- Open the Google Cloud Console and sign in.
- Pick an existing project from the project selector, or create one (New project → name it → Create).
- Enable the API for that project: open PageSpeed Insights API in the API Library and click Enable. A key will not work until this is done, even if the key itself exists.
- Go to APIs & Services → Credentials (direct link) and click Create credentials → API key. Copy the key.
- Click Edit API key and, under API restrictions, choose Restrict key and select PageSpeed Insights API. Leave Application restrictions set to None — the requests come from your server, not a browser, and an HTTP-referrer restriction would reject them.
- Add it to your environment:
PAGESPEED_API_KEY=your-key-here
Billing is not required. Google does not publish the quota numbers for keyed
projects anywhere; check APIs & Services → PageSpeed Insights API → Quotas
in the Console for the limits that apply to yours. That is also why
rate_limit.per_minute defaults to a conservative 30.
The key is sent on the X-goog-api-key header rather than as a key= query
parameter, so it never appears in a URL — a URL-borne key ends up in
connection-error messages, exception reports, and proxy logs.
Where the key is stored
PAGESPEED_CONFIG_DRIVER selects the storage driver, mirroring
GOOGLE_CONFIG_DRIVER in artisanpack-ui/google:
| Driver | Storage | Writable |
|---|---|---|
config (default) |
PAGESPEED_API_KEY / config( 'pagespeed-insights.api_key' ) |
No — managed through config/env |
database |
pagespeed_configurations table, encrypted at rest |
Yes |
cms |
CMS framework Settings module, encrypted at rest | Yes — requires artisanpack-ui/cms-framework |
use ArtisanPackUI\PageSpeedInsights\Facades\PageSpeedInsights; PageSpeedInsights::config()->isConfigured(); // false means PSI cannot run at all PageSpeedInsights::config()->getApiKey(); PageSpeedInsights::config()->save( 'new-key' ); // database and cms drivers only
Running a test
use ArtisanPackUI\PageSpeedInsights\Facades\PageSpeedInsights; $result = PageSpeedInsights::test( 'https://example.com/', 'mobile' ); $result->scores->performance(); // 0-100, or null when unscored $result->scores->bestPractices(); $result->labMetrics->largestContentfulPaint(); // milliseconds, or null $result->effectiveFieldData()?->overallCategory(); // FAST / AVERAGE / SLOW from CrUX $result->opportunities; // pruned, sorted by estimated saving
A run takes 20–60 seconds and sometimes longer, so call it from a queued job or a console command, never from a web request:
use ArtisanPackUI\PageSpeedInsights\Jobs\RunPageSpeedTest; RunPageSpeedTest::dispatch( 'https://example.com/about', 'mobile' );
Failure modes
Three exception types, because at the HTTP layer these look alike and each needs a different response:
| Exception | Cause | What to do |
|---|---|---|
MissingApiKeyException |
No API key configured | Configure one. isRetryable() is false — retrying never helps. |
QuotaExceededException |
A keyed project is out of quota | Back off and try later. |
PageSpeedApiException |
Transport failure, API error, or Lighthouse's own runtimeError |
Usually retryable. |
An unkeyed request and an exhausted project both return HTTP 429, so the client checks whether a key was configured before classifying one — the first is a configuration problem, not quota exhaustion. See docs/troubleshooting.md.
Thin results
The parser tolerates unknown Lighthouse categories, absent categories, missing lab metrics, and missing CrUX data rather than failing the run — but it never does so silently. Every skip is logged with the key it skipped, and carried on the result:
$result->hasWarnings(); $result->warnings(); // run_warnings, unrecognized_categories, missing_categories, missing_metrics, missing_field_data $result->scores->unrecognized(); // e.g. ['agentic-browsing'] after a Lighthouse release
Lighthouse changes its category and metric lineup roughly every two releases —
PWA is deprecated as of Lighthouse 12, AGENTIC_BROWSING has been added, and
Time to Interactive left the performance score in Lighthouse 10. So the package
stores four category scores and a fixed set of lab metrics, tolerates unknown
and absent categories without failing, and does not guarantee that a newly
added Lighthouse category will be surfaced until a release adds it.
Monitored URLs
UrlRegistry is the single answer to which URLs this installation monitors. It
merges the rows in pagespeed_urls with whatever other packages contribute
through a hook, and deduplicates the two by canonical URL.
use ArtisanPackUI\PageSpeedInsights\Urls\UrlRegistry; $registry = app( UrlRegistry::class ); $registry->all(); // stored rows + hook contributions, deduped $registry->active(); // the same, filtered to URLs still being tested $registry->find( 'https://example.com/about/' ); // matches however it is spelled $url = $registry->add( 'https://example.com/pricing', [ 'label' => 'Pricing' ] ); $registry->update( $url, [ 'test_frequency' => 'daily', 'strategies' => [ 'mobile' ] ] ); $registry->deactivate( $url ); // pause testing, keep the history $registry->delete( $url );
Every URL is stored in a canonical form, so https://example.com/about,
https://example.com/about/, and HTTPS://Example.com/about#team are one
monitored page rather than three histories of the same page. Query strings and
www. are preserved, because either can change which document is served.
Or populate the set from your sitemap:
php artisan pagespeed:discover-sitemap --activate
Commands
| Command | Does | Scheduled |
|---|---|---|
pagespeed:test {url} |
Test one URL now and hold it to CI score budgets | No |
pagespeed:monitor |
Queue tests for the monitored URLs that are due | Hourly |
pagespeed:discover-sitemap |
Populate the monitored set from a sitemap | No |
pagespeed:check-staleness |
Report monitored URLs that stopped producing results | Hourly |
pagespeed:prune |
Apply the retention windows | Daily |
Full flags and behaviour: docs/commands.md.
Testing in CI
pagespeed:test runs one test synchronously and exits non-zero when the result
misses a budget. It is the one command in the package that waits out a run,
because a pipeline has nowhere to put a queued job.
php artisan pagespeed:test https://example.com/ --min-performance=90 --max-lcp-ms=2500 php artisan pagespeed:test https://example.com/ --strategy=desktop --categories=performance,seo php artisan pagespeed:test https://example.com/ --min-performance=90 --json
| Code | Class | Meaning |
|---|---|---|
0 |
— | Every budget passed |
1 |
budget |
A budget was violated — the signal the command exists for |
2 |
input |
The command was called with unusable arguments |
3 |
configuration |
No API key is configured |
4 |
run |
The run failed, a budgeted measurement was unavailable, or --store could not save it |
A budget checked against a score that is null or absent is reported as
unavailable and exits 4 — never as a pass. A green pipeline that is green
because three categories quietly stopped coming back is worse than having no
budgets at all.
Scheduled testing and alerts
Monitored URLs are tested by a queued job. Make sure a queue worker is running
and Laravel's scheduler is wired up; the package registers pagespeed:monitor
and pagespeed:check-staleness hourly and pagespeed:prune daily.
After each completed run, the package compares it with the previous completed
run for the same URL and form factor. Three things count as a regression: a drop
of at least alerts.drop_points, a score below a configured floor, and a
category that was scored last run and is not measured now.
A second detector runs on the schedule and reports the URLs that stopped producing results at all — a revoked key, a page now returning 404, a dead worker — because a chart that flatlines at the last good score looks perfectly healthy while monitoring has silently stopped.
Full behaviour: docs/alerts.md.
Components
Five panels in each of three stacks, all rendering the same states from the same stored rows: a score card, a Core Web Vitals card, an opportunities table, a trend chart, and a URL manager.
<livewire:pagespeed-score-card url="https://example.com/pricing" /> <livewire:pagespeed-trend-chart url="https://example.com/pricing" />
php artisan vendor:publish --tag=pagespeed-insights-js
npm install @artisanpack-ui/react # or @artisanpack-ui/vue
import { ScoreCard, TrendChart } from '@/../js/vendor/pagespeed-insights/react' <ScoreCard url="https://example.com/pricing" />
They ship as TypeScript sources rather than as a build, because they take their styling from the host application's Tailwind and daisyUI theme.
With artisanpack-ui/cms-framework installed alongside Livewire, three of them
also register as admin dashboard widgets —
docs/cms-framework.md.
HTTP API
Authenticated JSON endpoints behind the pagespeed prefix and the web and
auth middleware by default. They back the React and Vue components and are the
supported way to build a front end of your own.
| Method | Path | Answers |
|---|---|---|
GET |
/pagespeed/scores |
The newest run's category scores and lab metrics |
GET |
/pagespeed/core-web-vitals |
The newest run's CrUX field data |
GET |
/pagespeed/opportunities |
What Lighthouse says is worth fixing |
GET |
/pagespeed/trends |
One measurement over time |
GET POST DELETE |
/pagespeed/urls |
The monitored set |
POST |
/pagespeed/test |
Queue an ad hoc run; returns a ticket to poll |
GET |
/pagespeed/results/{id} |
Poll that ticket, or fetch any stored run |
These endpoints carry no authorization of their own beyond the configured middleware — by default an authenticated user is an authorized one. If "logged in" is broader than "allowed to manage performance monitoring" on your installation — a SaaS with customer accounts, a site with public registration — set an ability:
// config/pagespeed-insights.php (or PAGESPEED_ROUTES_ABILITY in .env) 'routes' => [ 'ability' => 'view_pagespeed_insights' ], // and define the gate in your own AuthServiceProvider Gate::define( 'view_pagespeed_insights', fn ( User $user ): bool => $user->isAdmin() );
It is appended to routes.middleware rather than replacing it, so auth stays
in front of it. This is the recommended setting for any app whose accounts are
not all staff.
Full reference, states, and error codes: docs/http-endpoints.md.
Hooks
Six extension points, built on artisanpack-ui/hooks:
| Hook | Kind | Fired |
|---|---|---|
ap.pageSpeed.registerUrls |
Filter | Whenever the monitored set is read |
ap.pageSpeed.opportunities |
Filter | While parsing a run, before opportunities are returned |
ap.pageSpeed.beforeTest |
Action | Before a run is sent to Google |
ap.pageSpeed.resultStored |
Action | After a result row is written — including a failed one |
ap.pageSpeed.scoreRegressed |
Action | After a stored run is found to have regressed |
ap.pageSpeed.urlWentStale |
Action | After a monitored URL is found to have stopped reporting |
use ArtisanPackUI\PageSpeedInsights\Urls\UrlRegistry; addFilter( UrlRegistry::FILTER_REGISTER_URLS, function ( array $urls ): array { return [ ...$urls, 'https://example.com/checkout' ]; } );
Signatures and payload shapes: docs/hooks.md.
Configuration
php artisan vendor:publish --tag=pagespeed-insights-config
Every key, env var, and default is documented in
docs/configuration.md — including the warnings
column and what a degraded run means.
The defaults worth knowing about up front:
| Key | Default | Why |
|---|---|---|
rate_limit.per_minute |
30 | Google publishes no rate limit, so the default is deliberately conservative |
test_frequency |
weekly |
Each run costs one request per form factor |
retention.days |
365 | A full seasonal cycle. An hourly cadence on 200 URLs writes ~3.5M rows a year. |
retention.keep_raw_days |
30 | A raw payload is hundreds of KB of already-parsed data |
store_raw_response |
false | Same reason |
routes.allow_external_urls |
false | Testing a URL spends quota; an authenticated user must not aim that anywhere |
routes.ability |
null | Additive, so no existing install changes behaviour by upgrading into it |
Documentation
| Guide | Covers |
|---|---|
| Installation | Requirements, install, publishing, Google Cloud setup |
| Configuration | Every config key, the credential drivers, degraded runs |
| Commands | The five commands, their flags, and exit codes |
| HTTP endpoints | The JSON API, its states, and its error codes |
| Livewire components | The five Blade / Livewire panels |
| React components | The React set and the shared fetch layer |
| Vue components | The Vue 3 set |
| Alerts | Regressions, the digest, and URLs that stop reporting |
| Hooks | The six ap.pageSpeed.* hooks, with signatures |
| CMS framework | The admin-widget and settings bridge |
| Testing | The package's suite, and testing an app that uses it |
| Troubleshooting | Symptom → cause → fix for every failure class |
| FAQ | Quotas, cadence, cost, and the questions that come up first |
| PSI API reference | The verified API behaviour this package is built against |
Development
composer install composer test # Pest composer lint # PHP-CS-Fixer (dry run) + PHPCS composer fix # PHP-CS-Fixer, applied npm install npm test # Vitest, against the React and Vue components npm run type-check # vue-tsc --noEmit
Nothing in the suite calls the live PageSpeed API — runs are served from checked-in response fixtures.
Contributing
See CONTRIBUTING.md.
License
MIT — see LICENSE.