jubeki/nova-card-linkable
jubeki/nova-card-linkable makes Laravel Nova dashboard and resource cards clickable, letting you attach an internal route or external URL so the whole card acts as a link. Lightweight, simple to configure, and handy for navigation tiles.
jubeki/nova-card-linkable gives you a Nova dashboard card whose entire surface is a link target. It is a ~1 file PHP + 1 file Vue package (19 stars, zero dependents, last release April 2022), so read the source before you trust any method name in a blog post — including this one. Verify the fluent API against README.md and composer.json constraints in the same sitting.
composer.json and confirm the laravel/nova constraint includes your installed major. Nova 3.x/early-4.x is the era this was built in; Nova 5 is a coin flip. Pin the version:
composer require jubeki/nova-card-linkable:2.0.1
php artisan nova:publish
app/Nova/Dashboards/Main.php:
use Jubeki\NovaCardLinkable\Linkable;
public function cards(): array
{
return [
Linkable::make('Pending Orders')
->url('/'.Nova::path().'/resources/orders?pending=1')
->width('1/3'),
];
}
vendor/jubeki/nova-card-linkable/src/ — there is so little code that you should read all of it before writing your first card. It tells you the exact constructor signature, whether ->url() accepts a closure, and whether the card wraps another card/metric or is a standalone card you must style yourself. That last distinction drives everything else on this page.First real use case: a dead-end KPI tile that becomes a drill-down. "Pending Orders: 42" → click → the filtered OrderResource index showing exactly those 42 rows. That single conversion is usually enough to prove whether the pattern is worth keeping.
Build URLs from Nova's config, never from /nova. The path is configurable (config/nova.php → path), and hard-coding /nova/resources/orders silently breaks the moment someone changes it or mounts Nova on a subdomain path:
$base = '/'.Nova::path().'/resources/'.\App\Nova\Order::uriKey();
Linkable::make('Orders')->url($base);
Match filter keys, not class names. Nova's index filters are applied via a query string keyed by the filter's key() method, so the link only works if it matches:
Linkable::make('Pending Orders')
->url($base.'?'.http_build_query([
'pending_orders_filter' => 'pending', // Filter::key()
]));
Use http_build_query() rather than string concatenation — order IDs, search terms, and dates with spaces will break a hand-glued URL, and it gets you multiple filters for free:
->url($base.'?'.http_build_query([
'orders_filter' => 'overdue',
'per_page' => 50,
]))
Wrap, don't restyle. If your version accepts a card or metric instance, wrap your existing Value/Trend metric so you keep number formatting, ranges, and progress bars and only add the link:
Linkable::make(
Value::make('Revenue', fn () => Order::sum('total'))
)->url($base);
If the constructor takes only a title string, treat the card as a styled tile and put the number in the title — and accept that you've re-implemented the metric yourself. This is the main reason most teams eventually write their own card.
One card per dashboard entry, not per resource. Cards are cheap, so a common pattern is a "dashboards index" where each card points at another dashboard with a different query string (?team_id=3, ?range=ytd). You get a navigable landing page without a single line of Vue.
Centralise card construction. Dashboards accumulate five or six near-identical link cards; put a private helper on a base dashboard class:
protected function linkCard(string $title, string $uri, array $query = []): Linkable
{
return Linkable::make($title)
->url('/'.Nova::path().'/resources/'.$uri.($query ? '?'.http_build_query($query) : ''))
->width('1/3');
}
Guard visibility with the card's own authorization. The link itself performs no authorization check, so pair every card with a canSee() that mirrors the target resource's policy:
Linkable::make('Open Orders')
->url(...)
->canSee(fn (Request $request) => Gate::check('viewAny', Order::class));
Without it, a support-tier admin sees a clickable tile that lands on a 403 — the worst of both worlds.
External links and tools. Pointing at Grafana, a BI dashboard, or a custom Nova tool works the same way, but verify the rendered markup is a real <a href> before relying on middle-click / "open in new tab". If the Vue component uses a JS click handler instead, users lose browser-native link behaviour.
If you need more than a static link, fork now. Conditional targets based on request state, click analytics, per-row drill-downs — none of that is in a 2022 micro-package. Copying src/ plus the Vue component into app/Nova/Cards/ takes an hour and removes the dependency question permanently.
cards.png), not a runtime fix. Treat a Nova major upgrade as a scheduled re-verification of every card, not a composer update you do on a Friday.php artisan nova:publish and confirm something landed under public/vendor/. Check the package tarball for a prebuilt dist/ — if it requires a Node build and you have no build step in CI, that's a blocker, not a bug.config/nova.php or re-register cards, clear php artisan config:clear and hard-refresh; the compiled card bundle is cached aggressively.key()), or the filter is hidden for that user's canSee. Cross-check with php artisan route:list | grep nova and the filter's own key() return value.viaResource / viaResourceId params. Links into a relation's index need those extra query parameters to render; a bare resource URL will 404 or show the wrong context.->url(). Static hard-coded links are low risk; a settings-driven "report URL" card is not.href, not the PHP you wrote. Nine times out of ten the problem is the URL string, not the package.width() / height() are the only layout levers. Titles truncate at 1/3 on narrow screens — prefer short titles and let the link carry the meaning."jubeki/nova-card-linkable": "2.0.1") and note in your upgrade checklist that Nova upgrades invalidate the pin. With one dependent-less package in the tree, composer why output is your audit trail.How can I help you explore Laravel packages today?