saintsystems/nova-linkable-metrics
Laravel Nova package that makes metric cards linkable, letting you turn dashboard metrics into clickable cards that open filtered resource index views.
Install against your exact Nova patch, not "4.x". The card rendering is a Vue override bolted onto Nova's metric card component, so the coupling is to Nova internals, not to a public API.
composer require saintsystems/nova-linkable-metrics
php artisan nova:assets
Before you commit the lockfile change, open vendor/saintsystems/nova-linkable-metrics/composer.json and confirm the laravel/nova constraint against php artisan nova:version. If it's a ^4.0 style constraint and you're on a late 4.x patch, you're relying on the maintainer's card override still matching Nova's current markup — verify with a render, not with semver.
First stop is src/, not the README. The README is feature bullets; there is no cookbook. For a package this size the source is the documentation, and ten minutes of reading tells you the four things you actually need:
->linkTo(), ->url(), ->linkable()) and what it accepts — a string, a closure, or a resource + params array.Value, Trend, Partition, and whether Progress is included.->openInNewTab(), ->target('_blank'), or a config array).First use case — one card, one destination. Pick a Value metric you already ship whose underlying query maps to a filterable Nova resource index. Do the filter work in the UI first, copy the resulting URL out of the address bar, then wire it up:
// app/Nova/Metrics/OverdueAccounts.php
use Saintsystems\NovaLinkableMetrics\Metrics\Value; // confirm real namespace in src/
class OverdueAccounts extends Value
{
public function calculate(NovaRequest $request)
{
return $this->count($request, Account::whereNotNull('overdue_since'));
}
}
// app/Nova/Dashboards/Main.php
(new OverdueAccounts)->linkTo(
'/resources/accounts?accounts_page=1&filter=overdue'
),
Then log in as a non-admin user and click it. That single five-minute check catches the two bugs that will actually bite you: the filter encoding is wrong, or the destination 403s for that role.
One metric class per link. Nova serializes metrics into a card payload keyed by class; parameterizing a single metric into a link generator makes dashboards unreadable and rollback per-card impossible. Keep OverdueAccounts, ChurnedThisMonth, SlaBreaches as discrete classes, each with its own calculate() and its own link target.
Work backwards from the destination URL. The filter is the hard part; the link is one line. Build the URL by applying the filter in the browser and copying it, then paste it into the metric class. Resist reconstructing the query string from memory — Nova's filter parameter shape (filter=, filters[][key]=) has shifted across versions and it's the single most common "the card is clickable but shows everything" failure.
Prefer a named Nova filter over a raw query string. If the destination needs conditions that aren't expressible as a stock query param, add a filter class, register it on the resource, and link to that filter's key. It keeps the "which records" definition in PHP where it's testable, instead of in a URL literal that silently rots when a column is renamed.
// app/Nova/Filters/Overdue.php — key() = 'overdue'
(new OverdueAccounts)->linkTo(
Nova::uriKey(Account::class)
? '/resources/'.Account::uriKey().'?filter=overdue'
: '/resources/'.Account::uriKey()
),
Partition metrics are the highest-value shape. A partition already renders segments with counts — linking each segment to its own filtered index (status=open, status=pending, status=closed) turns a pie chart into a navigation panel. If the package supports per-partition link targets, this is where you get the most perceived value per line of code. Check src/ for a per-label callback; if it only supports one target for the whole card, don't fake it.
Preserve context through the link. Metrics are computed against the current request — dashboard date ranges, resource filters, user scope. If your calculate() reads $request->range or a date filter, the link target almost certainly needs the same range appended, otherwise the card says 12 and the destination shows 40. Pass the range through explicitly:
public function calculate(NovaRequest $request) { /* … */ }
// build the link with the same range the card was rendered with
Use closures/authorizedToSee for anything dynamic. Static URLs are fine for a fixed dashboard; the moment the destination depends on the viewer (their team, their region, their assigned queue), you want the link computed per-request. Pair that with authorizedToSee() returning false for users who can't see the destination — a card that 403s on click is worse than a card that isn't there.
Test the URL, not the card. End-to-end Nova dashboard tests are painful. The pragmatic coverage is a unit test asserting the generated link string for a given metric instance, plus one Dusk/Pest browser click-through if you already have that harness. That catches the two silent failures: stale route 404 and filter drift. Neither throws at render time.
Rollout as a sweep, one dashboard at a time. Ship one linked card on one dashboard, watch whether anyone clicks it (Nova doesn't log clicks — add a UTM-ish query param or check your access logs), then extend. Everything here is additive and reversible, so a revert is composer remove plus deleting the linkTo() calls.
Filter encoding is the bug you will actually hit. The card will render, the anchor will work, and the destination will show an unfiltered index. It looks like nothing is broken. Always copy the URL from a real browser session after applying the filter — never hand-assemble filters[...].
Asset publishing fails silently. The link behavior lives in the Vue card component. If php artisan nova:assets output doesn't reach your deploy pipeline or your CDN cache-busts wrong, you get a metric card that renders perfectly without the anchor — no error, no 500, no log line. Verify the published JS hash changes on deploy, and add that to your release checklist alongside nova:assets.
Pin the version. No ^ floating across Nova minors. The package is scoped to Nova 4.x and Nova 5 support isn't stated anywhere in the release notes. Nova upgrades are usually mandatory (security, PHP version), which makes this a hard decision point at the next major — add verify or fork linkable-metrics to the Nova-upgrade checklist now, while it's a hypothetical.
Mirror the repo internally and write the revert down. Small-agency maintainer, no public battle-testing evidence, dependents count is a weak proxy because Nova packages live in private repos. Under MIT this is fine for a package of this size, but adopt it consciously: vendor-mirror at adoption, exact pin, and a documented revert (annotations out, composer remove, clear published assets) that takes five minutes.
calculate() runs on every dashboard load — the link is free, but your metric query isn't. If you're linking twelve cards to twelve filtered counts, you've just made a dashboard that runs twelve count() queries per visit. Keep the calculate() queries index-friendly and consider Nova's cache/refresh settings.
Don't ask it to be analytics. This adds navigation, not data, segmentation, cohorts, history, or exports. "Click the number to see the list" is in scope; "let me pivot on that list" is not. If stakeholders actually want exploration, they need Metabase/Looker/a Nova tool — and a linkable card will buy you exactly one sprint before hitting the wall.
Scope by real clicks, not by possibility. Before sweeping every card, ask which cards people actually click and which list they expect to land on. If no one has asked "can I click this?", park it. If the answer for a given metric lives in a custom page or an external system rather than a filterable Nova resource, this is the wrong layer — the link will exist and go nowhere useful.
How can I help you explore Laravel packages today?