vmitchell85/nova-links
Laravel Nova package for adding and managing links inside your dashboard. Create labeled URL entries, organize them in a dedicated Nova resource, and give users quick access to important internal or external links.
Resolve the identity of the package before anything else. The repo title says "Add custom links to your nova navigation"; the package summary describes a Link field rendered in resource index and detail views. Those are different integrations with different rollback stories — a config array versus a database column and migration. Open the README and src/ at the exact tag you intend to pin and confirm which one you are installing. Everything below assumes the navigation case (which matches the repo name), with the field pattern noted where it diverges.
Pin before you require. The last release is 2023-03-23. Nova is proprietary and major-version-locked, so prove compatibility rather than assuming it:
composer why-not vmitchell85/nova-links 1.0.0
composer require vmitchell85/nova-links:1.0.0
Pin the exact version in composer.json so a future composer update can't silently move it. A March 2023 release is a strong candidate for Nova 4-era projects and an open question on Nova 5 / current Laravel / PHP 8.3+.
Publish the config (confirm the tag or provider string in the README):
php artisan vendor:publish --tag=nova-links-config
# fallback if no tag is registered:
php artisan vendor:publish --provider="VMitchell85\NovaLinks\NovaLinksServiceProvider"
Register it in app/Providers/NovaServiceProvider.php. Depending on how the package ships, that's either an entry in tools():
public function tools()
{
return [
new \VMitchell85\NovaLinks\NovaLinks,
];
}
…or a Nova::mainMenu() extension inside boot(). If the README doesn't say, grep the service provider for Nova::mainMenu, Nova::tools, or Nova::script.
First use case: one "Status Page" link in the sidebar for the ops team — the thing people currently keep as a browser bookmark or ask about in Slack. Get that single link rendering end-to-end, including under config:cache, before adding link #2.
Static config array is the primary pattern. Keep config/nova-links.php in version control as the single source of truth. Resist generating links from a database table or building a settings CRUD around it — the moment you do, you've paid more than hand-rolling Nova::mainMenu() would have cost.
Per-environment destinations via env() inside the config file. Config files are the one place env() remains safe under config:cache:
'links' => [
[
'label' => 'Status',
'url' => env('NOVA_LINKS_STATUS', 'https://status.example.com'),
'target' => '_blank',
],
[
'label' => 'Runbook',
'url' => 'https://wiki.example.com/runbooks',
],
],
Staging and production then differ by .env, not by branching config.
Conditional visibility belongs in your provider, not the config. If a link should only show for certain roles, wrap registration in the provider:
public function boot()
{
parent::boot();
Nova::mainMenu(function (Request $request) {
$menu = parent::mainMenu($request);
if ($request->user()?->can('viewOpsDashboards')) {
$menu->append(/* link definition */);
}
return $menu;
});
}
Treat this strictly as a UX affordance, not authorization. If the destination (legacy admin panel, status dashboard, internal wiki) is sensitive, gate it server-side at the destination. A hidden nav link is not access control.
Ordering and grouping. If the package exposes priority, order, section, or group keys, use them — otherwise links land wherever Nova appends them and will reshuffle relative to other tools you add later. If grouping isn't supported at all, that's your signal to stop and use Nova's native menu API, which is roughly the whole package inlined:
Nova::mainMenu(function (Request $request) {
return [
MenuSection::make('Operations', [
MenuItem::externalLink('Status', 'https://status.example.com')
->openInNewTab(),
MenuItem::externalLink('Runbook', 'https://wiki.example.com/runbooks'),
])->icon('server'),
];
});
Keep this as your documented fallback and your fork.
If it turns out to be a field, the pattern is the ordinary Nova one — register in fields() with a closure for computed URLs, and scope it to the detail view unless you genuinely want an index column:
Link::make('Docs', function () {
return route('docs.show', $this->resource->slug);
})->text('Open documentation')->onlyOnDetail(),
Smoke-test with a real deploy, not just a local boot. A feature test against the Nova route is nice-to-have; what actually catches failures here is a staging deploy with config:cache, route:cache, and view:cache enabled, then logging in as two different Nova roles and eyeballing the sidebar. Ten minutes, catches the failure class that only appears in production.
Document the adoption in the PR. Record the pinned version, the Nova/Laravel/PHP versions validated against, and the rollback (composer remove vmitchell85/nova-links, revert the NovaServiceProvider diff, clear caches). The next developer to touch this will not read the README.
config:cache is the classic failure mode. A provider that inspects config() before the cache warms, or a config that reads env() from somewhere other than the config file, produces a sidebar that works uncached in staging and is empty in production. Always exercise the cached path. After editing config on a live server, run php artisan config:clear then re-run config:cache as part of the deploy.
Check what Nova internals it relies on. If the service provider hooks undocumented navigation APIs, a Nova minor upgrade can silently break registration with no exception surfaced. Grep the provider before merge — if it calls methods absent from the Nova docs, budget a patch on every Nova upgrade, or inline the ~40 lines into your own provider and drop the dependency entirely.
target="_blank" needs rel. If the package doesn't add rel="noopener noreferrer" to external links automatically, either patch your fork or confirm the config exposes it. It's a one-line security nit that will get flagged in review.
Zero dependents, single maintainer, stale release. This is code you're borrowing, not a vendor you're trusting. Two mitigations: pin the version and state the pin in the PR description; write the rollback next to the adoption decision. If the spike runs past a few hours, inline the pattern instead — the lifetime cost of ownership exceeds the savings.
Asset publishing is a separate step. If the package ships views or CSS, php artisan nova:publish is distinct from vendor:publish and must run in every pipeline that does nova:publish. Missing it yields a registered-but-unstyled sidebar entry that looks like a config bug but isn't.
Debugging an empty sidebar, in order:
NovaServiceProvider::tools() and the class autoloads: php artisan tinker → new \VMitchell85\NovaLinks\NovaLinks.config/nova-links.php exists and isn't a stale cache artifact.storage/logs/laravel.log for a swallowed exception in boot() — Nova frequently fails quietly on tool registration.Nova::mainMenu() closure. If that renders, the package is the problem, not your Nova setup.Links rot. Nothing in this package enforces naming conventions, ownership, or periodic review. Put a comment above each link naming an owner and set a calendar reminder to prune — a dead "Legacy Admin" link in the sidebar is worse than no link at all.
Extension point if you outgrow it: the provider's boot() is your seam. Anything the config array can't express — per-role links, grouping, badges, localization — belongs in a Nova::mainMenu() closure you control, at which point you should remove the dependency rather than wrap it.
How can I help you explore Laravel packages today?