Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Custom Links Laravel Package

wehaa/custom-links

wehaa/custom-links is a Laravel package for managing customizable links in your application, with configurable labels, URLs, and ordering to simplify navigation and resource link setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Check compatibility before you read a single line of documentation. This is a 2019-era Nova sidebar extension with one feature release (v0.1.1, September 2019), 12 stars, and zero dependents. Your first ten minutes should go into Composer, not config:

composer show wehaa/custom-links --all

Look at the laravel/nova, illuminate/support, and php constraints. If Nova is pinned to ^1.0/^2.0, Composer will refuse to install on Nova 4/5 — at which point stop and evaluate the native Laravel\Nova\Menu (MenuItem::make(...)->canSee(...)) instead of forcing constraints.

Install and publish:

composer require wehaa/custom-links:0.1.1
php artisan vendor:publish --provider="Wehaa\CustomLinks\CustomLinksServiceProvider"

Auto-discovery should register the provider; if discovery is disabled, add it to config/app.php manually. Publishing typically yields config/custom-links.php and possibly a compiled Vue asset. Confirm both before proceeding:

ls config | grep -i link
php artisan vendor:publish --list | grep -i custom-links

Read these, in this order:

  1. vendor/wehaa/custom-links/composer.json — constraints and autoload map.
  2. vendor/wehaa/custom-links/src/ — service provider and, critically, the link resolver / authorization logic.
  3. The published config file — your real day-to-day surface.
  4. The compiled JS asset — determines whether the sidebar component even mounts on your Nova version.
  5. A local clone with git diff v0.1.0 v0.1.1 to see exactly how "authorization for each link" was implemented.

Ignore the description.md mismatch. The package's own description blurb describes attaching custom slugs to Eloquent models with resolvable redirects — that is not what the release notes, the repo name, or any of the surrounding context describe. The package is a Nova sidebar link injector. Trust src/ and the config, not the marketing copy.

First use case: one visible link, one hidden link. Don't start with a role matrix. Add a single external link (status page, docs, support portal) and confirm it renders in the sidebar. Then add a second guarded by an authorization callback and confirm it disappears for a user who fails the check. That two-link smoke test exercises everything you will actually rely on:

// config/custom-links.php — shape varies; verify against the published file
return [
    'links' => [
        ['label' => 'Status Page', 'url' => 'https://status.example.com'],
        [
            'label' => 'Billing',
            'url'   => 'https://billing.example.com',
            'can'   => fn ($user) => $user->hasRole('finance'),
        ],
    ],
];

Then confirm the hidden link's URL is absent from the page source and Nova's bootstrap JSON — not merely display:none. That is the single most important check in the whole package.

Implementation Patterns

Treat the config file as a declarative registry. Label, target, optional group, optional order, optional authorization. The moment you want to loop over a DB query or generate links per tenant, you have outgrown this package and should be looking at a fork or at native Nova menu code.

Prefer route names over hardcoded URLs. If the resolver in src/ supports route names, use them so links survive domain/path changes between environments:

['label' => 'Horizon', 'route' => 'horizon.index'],
['label' => 'Docs',    'url'   => 'https://docs.example.com'],

If it only accepts raw URLs, resolve them in a small helper or service provider — but read the config-cache gotcha below before putting route() calls directly in config.

Group and order deliberately. Grouping keeps a growing sidebar legible ("Operations", "Finance", "External"). Assign explicit integer order values using gaps of ten (10, 20, 30) so insertions don't require renumbering. If ordering isn't a first-class key, array position becomes order and reordering produces noisy diffs that are easy to misread in review.

Make authorization a first-class pattern from day one. The v0.1.1 feature is the only real reason to choose this over a hardcoded sidebar. Establish the convention immediately: every link gets a can callback, even fn ($user) => true. That makes "no callback" a review failure rather than a silent default. Wrap roles and policies behind named gates rather than inline role strings:

'can' => fn ($user) => Gate::forUser($user)->allows('viewBilling'),

That keeps config stable when roles are renamed and makes the decision testable in isolation. If config:cache is in play (it always is in production), use invokable class strings instead of closures:

'can' => \App\Links\CanViewBilling::class,

Environment variation via .env, not divergent link sets. Pull environment-specific URLs from config sourced from .env (BILLING_URL, STATUS_URL) but keep the set of links consistent across environments. Divergent link sets cost debugging time with no upside.

Testing pattern. Write one feature test per authorization branch, asserting the label is present/absent on the Nova dashboard response:

$this->actingAs($financeUser)->get('/nova')->assertSee('Billing');
$this->actingAs($supportUser)->get('/nova')->assertDontSee('Billing');

Assert both the label and the target URL are absent for the unauthorized user — that's your regression net against a silently broken callback or a refactor that accidentally moves the check client-side.

Run it alongside the native Nova menu during migration. On Nova 4+, keep genuinely static links in this package and move anything needing conditional logic, counters, or submenus into NovaServiceProvider::mainMenu() with MenuSection/MenuItem. That confines your exposure to the simplest case.

Fork before you extend. Need localization, per-tenant link sets, submenus, or click analytics? Copy the source into packages/ as a path repository and own it. The code volume is small enough to read fully in an afternoon.

Gotchas and Tips

Closures in config break config:cache. The most likely day-one failure. Laravel can't serialize closures, so an inline can callback in config/custom-links.php makes php artisan config:cache throw. Confirm before deploying. If it breaks, register links in a service provider at boot, or switch to invokable class strings.

Determine whether authorization runs server-side or in JavaScript. The v0.1.1 note says "authorization for each link" but not where. Read the compiled Vue component. If the check happens client-side, the link and URL are already in the JSON payload — hiding it is cosmetic, and anyone with devtools can read the URL. Fine for a docs link inside an internal panel; not acceptable for a finance system. Decide per link, and never rely on client-side checks alone.

Determine the fail-open/fail-closed default explicitly. Add a link with no can callback, log in as a low-privilege user, and observe. If the default is visible, authorization is opt-in and someone will forget the callback. If hidden, a missing callback yields an invisible link and a confusing ticket. Know your failure mode and document it as a comment in config.

Verify the hidden link's URL is gone from the page payload. Inspect Nova's bootstrap JSON, not just the rendered sidebar. Ctrl-F for the URL. If it appears, "authorization" is presentation-only.

Nova/Vue compatibility is the hard gate. Nova 4+ uses Vite and Vue 3; this 2019 asset is almost certainly a Webpack/Vue 2 build. Symptoms run from a cleanly missing sidebar section to a Vue warning followed by a silently non-mounting component. Check the browser console first on any "nothing renders" report, and check your Nova version against composer.json before debugging config.

Config caching masks edits. After changing links, run php artisan config:clear (and config:cache again for production-like checks). Make config:clear the first step in any debugging checklist.

Route resolution timing. Running route() inside config executes when config loads — potentially before the router is fully booted and definitely before some route providers register. Prefer passing route names as strings and letting the package resolve them at render time, if supported. Otherwise resolve URLs in a service provider.

Asset publishing is explicit. The Vue component must be published and served from public/vendor/.... After composer update or a fresh deploy, run the vendor:publish command with --force (find the correct --tag) and php artisan nova:publish if Nova assets are involved. A missing compiled asset looks identical to a broken component.

Ordering ties are undefined. Two links sharing an order value may sort inconsistently between PHP and any JS-side sort. Use unique integers.

Pin the exact version and commit the lock file. Use "wehaa/custom-links": "0.1.1", not ^0.1. Pre-1.0 semver means a 0.1.2 can change the authorization API without warning, and the release history suggests the config contract isn't treated as stable.

Debugging sequence, in order:

  1. php artisan config:clear — stale cached config.
  2. composer show wehaa/custom-links — installed version and constraints.
  3. Browser console — Vue mount errors and asset 404s.
  4. Inspect the Nova page payload — is the link in the JSON?
  5. dump() inside the authorization callback — is it called at all, and with which user?
  6. Service provider boot() — unguarded env() calls or file reads that behave differently under cached config.

The replacement test. Before investing in workarounds, implement the same two links natively in NovaServiceProvider::mainMenu() with MenuItem::make(...)->canSee(...). If that takes under an hour — and it usually does — you have your answer about which path to maintain across the next Nova major.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource