danilopolani/filament-plausible-widget
A Filament widget that brings Plausible Analytics stats into your admin dashboard, so you can view site traffic and key metrics without leaving the panel. Simple to add to any Filament dashboard.
1. Install and publish
composer require danilopolani/filament-plausible-widget
php artisan vendor:publish --tag="filament-plausible-widget-config"
As of v3.2.0 the package officially supports Laravel 12. If you're on Laravel 10/11 you're fine too — the 3.x line targets Filament 3 and the modern Laravel range. Pin the version you install (see Gotchas) so a future minor bump can't surprise you on a Friday deploy.
2. Get the two credentials you actually need
example.com, not https://www.example.com). Subdomains matter.Configure via .env, not by editing the published config directly:
PLAUSIBLE_API_KEY=xxxxxxxxxxxxxxxxxxxx
PLAUSIBLE_SITE_ID=example.com
# only for self-hosted:
# PLAUSIBLE_BASE_URL=https://analytics.example.com
3. Where to look first
Read the package in this order — it's small enough to skim in ten minutes:
config/filament-plausible-widget.php — the only real API surface. Every knob lives here.src/Widgets/ — the shipped widget class. Copy its FQCN; that's what you register.src/ service provider — tells you what's bindable and whether the HTTP call is isolated (it should be, and that determines how easy it is to test).4. First use case: register it on the dashboard
In app/Providers/Filament/AdminPanelProvider.php:
use Danilopolani\FilamentPlausibleWidget\Widgets\PlausibleWidget; // confirm FQCN in README
->widgets([
AccountWidget::class,
PlausibleWidget::class,
])
That single card — visitors / pageviews / bounce rate for the last 30 days — is the whole value proposition: your content or marketing team stops tabbing over to plausible.io. Ship that, then iterate.
Scope the widget to the dashboard page, not the whole panel. Filament renders ->widgets() on every page that uses the dashboard layout. If you register it globally it will follow users into resource edit screens and burn API calls for no reason. Prefer registering it only where it belongs:
->widgets([
AccountWidget::class,
PlausibleWidget::class,
])
// and gate it:
->widgets([]) // on secondary panels
Cache the response — this is the single most important pattern. The Stats API is rate-limited and every dashboard refresh hits it. Wrap the stats call in your own cache layer if the package doesn't already, or override the data method in a subclass:
protected function getStatsData(): array
{
return Cache::remember(
'plausible:stats:' . config('filament-plausible-widget.site_id'),
now()->addMinutes(15),
fn () => parent::getStatsData(),
);
}
15–60 minutes is the right range. Analytics data isn't real-time-critical for an admin panel.
Set a polling interval deliberately, or not at all.
protected static ?string $pollingInterval = null; // default: no polling, user refreshes
Only enable polling ('60s') if someone explicitly asked for a live view, and only with the cache above in place.
Subclass for every variation. The package almost certainly exposes the period/metrics as overridable properties or methods. Rather than forking, extend and override:
class WeeklyTrafficWidget extends PlausibleWidget
{
protected function getPeriod(): string
{
return '7d'; // Plausible accepts 7d, 30d, 6mo, 12mo, custom
}
protected function getHeading(): string
{
return 'Traffic — last 7 days';
}
}
Multiple sites = multiple subclasses. If you run several properties, create one thin subclass per site and set the site ID per class. Don't try to make one widget "smart" about which site to show — the admin dashboard isn't the place for state.
Gate by permission. Traffic data is commercially sensitive; not every admin should see it:
public static function canView(): bool
{
return auth()->user()?->can('view analytics') ?? false;
}
Combine with internal metrics. The strongest use of this widget is as context next to your own numbers — pair the Plausible visitors card with a StatsOverviewWidget showing signups or orders from your database. Traffic without conversion is trivia.
Testing. Because the data comes over HTTP, Http::fake() should cover it. If the package calls Http directly (check src/), a feature test on the dashboard page plus a faked stats response is enough:
Http::fake(['plausible.io/*' => Http::response(['results' => [...]])]);
$this->actingAs($admin)->get('/admin')->assertOk();
A blank card is almost always auth, not a bug. The Stats API returns 401 for a bad/missing key and 400 for a malformed site ID, and Filament widgets swallow exceptions into an empty state. When nothing renders, Http::fake() isn't your problem — log the raw response once:
Http::withToken(config('filament-plausible-widget.api_key'))
->get('https://plausible.io/api/v1/stats/aggregate', [
'site_id' => config('filament-plausible-widget.site_id'),
'period' => '30d',
'metrics' => 'visitors,pageviews',
])->throw()->json();
Run that in tinker before you touch the widget. Ninety percent of "the widget is broken" reports end here.
env() inside the config file is fine; env() outside it is not. If you call env('PLAUSIBLE_API_KEY') anywhere in your own code, php artisan config:cache in production will silently return null. Reference config('filament-plausible-widget.api_key') instead.
Self-hosted base URL: no trailing slash. https://analytics.example.com/ produces double-slash paths that some reverse proxies reject. Also confirm your instance actually exposes /api/v1/stats/* — older self-hosted images predate the Stats API.
Timezone drift. Plausible aggregates in the site's configured timezone, not your app's. A "today" card can look wrong by several hours if your team is in a different region. Either align the Plausible site timezone with your app, or avoid labelling anything "today".
cache:clear after changing credentials. The widget's data is cached; rotating an API key and seeing stale/empty numbers sends people down the wrong path.
Version pinning — and what v3.2.0 actually tells you. This is a small, zero-dependents package, so treat it as a readable library rather than infrastructure. Pin the version in composer.json, but note that v3.2.0 (Laravel 12 support, via PR #12) is a compatibility release: it signals the maintainer and community are still tracking new Laravel majors, which is the thing you care about most for a tiny dependency. It adds no new features, no config changes, and nothing to migrate. When the next Laravel major lands, check for a matching bump here before upgrading your app — that release will likely be the same shape as this one. Because it's MIT you can always vendor the widget class into app/Filament/Widgets/ if upstream goes quiet; it's a thin wrapper over one HTTP call, so forking costs an afternoon, not a sprint.
Filament major-version compatibility is the real upgrade risk. Laravel 12 support says nothing about Filament 4. Before bumping Filament, check whether this package's widget still extends the current base class — a stale widget will fail at panel boot with a confusing "class not found" or method-signature error rather than a clear incompatibility message. The v3.2.0 release is a good reminder to read the changelog for what it doesn't say (here: no Filament bump), not just what it does.
How can I help you explore Laravel packages today?