artisanpack-ui/pagespeed-insights adds Google PageSpeed Insights testing, score
history, and drop-in UI components to a Laravel application.
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.
Guides
- Installation — Requirements, install, publishing, and the Google Cloud setup for an API key
- Configuration — Every config key, the three credential drivers, and what a degraded run means
- Commands — The five Artisan commands, their flags, and
pagespeed:test's exit codes - HTTP endpoints — The authenticated JSON API behind the React and Vue components
- Livewire components — The five Blade / Livewire panels
- React components — The same five panels for React, plus the shared fetch layer
- Vue components — The same five panels for Vue 3
- Alerts — Regression detection, the digest, and the URLs that stop reporting
- Hooks — The six
ap.pageSpeed.*actions and filters, with signatures - CMS framework — The optional admin-widget and settings bridge
- Testing — Running the package's own suite, and testing an application 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 PageSpeed Insights API behaviour this package is built against
An API key is required
There is no usable keyless mode. Google's shared anonymous project has a daily quota of zero, so a request without a key answers HTTP 429 every time. The anonymous tier exists in this package only as the diagnostic path that produces the "no API key configured" error.
See Installation → Getting a PageSpeed Insights API key.
The shape of the package
| Layer | What it does |
|---|---|
PageSpeedClient |
Talks to runPagespeed, parses the payload into typed data objects |
UrlRegistry |
The single answer to "which URLs does this installation monitor?" |
RunPageSpeedTest |
The queued job that runs a test and writes a result row |
TestScheduler |
Works out which monitored URLs are due and queues them |
RegressionDetector / StalenessDetector |
Turn a silent regression, or a URL that stopped reporting, into a notification |
| Livewire / React / Vue components | Five panels each, rendering the same states from the same stored rows |
| Console commands | Ad hoc CI testing, monitoring, sitemap discovery, staleness checks, retention |
The PHP API surface
Facade and helper
use ArtisanPackUI\PageSpeedInsights\Facades\PageSpeedInsights;
PageSpeedInsights::version(); // the package version, e.g. '1.0.0'
PageSpeedInsights::config(); // the ApiKeyRepository for the configured driver
PageSpeedInsights::client(); // the PageSpeedClient
PageSpeedInsights::test( $url, $strategy = 'mobile', $locale = null ); // a TestResult
pageSpeedInsights() is a global helper returning the same instance, for code
that would rather not import the facade:
pageSpeedInsights()->config()->isConfigured();
config() and client() resolve from the container on every call rather
than being injected, so a driver, endpoint, or timeout changed at runtime takes
effect immediately — the facade's backing instance is a long-lived singleton and
would otherwise hold a stale one.
test() is synchronous: it blocks for as long as Google takes, which is
20–60 seconds and sometimes longer, so it belongs in a queued job or a console
command rather than a web request. It throws one of the three exception types
listed in Troubleshooting.
Data objects
TestResult is what a parsed run looks like. Everything on it is readonly.
| Object | Reached by | Carries |
|---|---|---|
TestResult |
PageSpeedInsights::test(), PageSpeedResult::toTestResult() |
The whole run: url, strategy, scores, labMetrics, fieldData, originFieldData, opportunities, finalUrl, lighthouseVersion, analyzedAt, and the warning arrays |
ScoreSet |
$result->scores |
performance(), accessibility(), bestPractices(), seo(), get(), has(), all(), unrecognized() |
LabMetrics |
$result->labMetrics |
firstContentfulPaint(), largestContentfulPaint(), totalBlockingTime(), cumulativeLayoutShift(), speedIndex(), value(), display(), all(), missing() |
FieldData |
$result->effectiveFieldData() |
The CrUX metrics at the 75th percentile, plus overallCategory(), isOriginFallback(), and isOriginLevel() |
Opportunity |
$result->opportunities |
id, title, score, savingsMs, displayValue, metricSavings, description, and weight() |
Every score and metric accessor returns null rather than zero when the measurement is absent. A zero would be a measurement; null is the absence of one, and the two must not be conflated — see Configuration → Degraded runs.
effectiveFieldData() returns page-level data when CrUX has it and origin-level
data otherwise; ask isOriginLevel() before presenting it as a verdict on the
page.
Models
PageSpeedUrl is a monitored URL; PageSpeedResult is one run.
use ArtisanPackUI\PageSpeedInsights\Models\PageSpeedResult;
use ArtisanPackUI\PageSpeedInsights\Models\PageSpeedUrl;
PageSpeedUrl::query()->active()->due()->get();
PageSpeedUrl::query()->forUrl( 'https://example.com/about/' )->first();
$url->results(); // HasMany
$url->frequency(); // its own cadence, or the configured default
$url->isDue();
$url->effectiveStrategies(); // its own strategies, or both
PageSpeedResult::query()->forUrl( $url, 'mobile' )->completed()->get();
PageSpeedResult::query()->latestFor( 'https://example.com/about/', 'mobile' )->first();
$result->isCompleted();
$result->isFailed();
$result->wasDegraded();
$result->warningList();
$result->scores(); // a ScoreSet
$result->toTestResult(); // a TestResult
Prefer UrlRegistry over PageSpeedUrl directly for anything that writes: it
canonicalizes URLs, records source itself, and merges in hook contributions.
Compatibility policy
Lighthouse changes its category and metric lineup roughly every two releases.
PWA is marked deprecated in the live schema ("deprecated in Lighthouse's 12.0
release"), AGENTIC_BROWSING has been added to the category enum, and Time to
Interactive was removed from the performance score in Lighthouse 10.
So the package commits to a fixed shape and tolerates drift around it:
- It stores four category scores — performance, accessibility, best practices, SEO — and a fixed set of lab metrics (FCP, LCP, TBT, CLS, Speed Index).
- It tolerates unknown categories in a response and absent categories that were requested, without failing the run.
- Every tolerated gap is recorded rather than swallowed: see the
warningscolumn,TestResult::warnings(), andScoreSet::unrecognized(). - It does not guarantee that a newly added Lighthouse category will be surfaced until a release of this package adds it.
No silent failures
Everything this package tolerates, it also reports. A run that completed while losing a category is degraded, not merely successful; a budget checked against a score that never arrived is unavailable, not a pass; a monitored URL that stops producing results is alerted about rather than left to flatline a chart at its last good score.
Troubleshooting maps each of those signals back to a cause and a fix.

