codedor/filament-link-picker
A Filament form field that lets editors choose links without hand-typing URLs. It provides a searchable picker for internal pages, model records, and custom routes, with configurable sources and labels. Use it for navigation, buttons, and CTA fields to keep links consistent in Laravel admin panels.
v2.2.0 is the first release in this assessment, so treat the notes as "what this package looks like today" rather than a diff from a known baseline.
composer require wotzebra/filament-link-picker
LinkPicker form component, the Link value object, and the cast. Everything else in the package hangs off those three.pages.link, posts.cta) and make the column json/text. The picker round-trips a structured payload, not a plain string.Link value object:protected function casts(): array
{
return [
'link' => \Wotzebra\FilamentLinkPicker\Casts\LinkCast::class,
];
}
use Wotzebra\FilamentLinkPicker\Forms\Components\LinkPicker;
LinkPicker::make('link')
->label('Call to action');
First realistic use case: a "Read more" link on a Page resource that may point at an internal route (with an anchor) or an external URL, with the external variant opening in a new tab. That single case exercises the internal/external branch, the anchor handling fixed in this release, and the new defaultNewTab behaviour.
Value-object rendering. Keep URL assembly in one place — a Blade component or an accessor — so target/rel logic isn't scattered:
@if ($page->link)
<a href="{{ $page->link->getUrl() }}"
@if ($page->link->opensInNewTab()) target="_blank" rel="noopener noreferrer" @endif>
{{ $page->link->getLabel() ?? 'Read more' }}
</a>
@endif
The exact accessor name for the new-tab flag lives on the Link class — v2.2.0 adds defaultNewTab, so check the class for opensInNewTab() vs shouldOpenInNewTab() before shipping.
Let external links default to new tab. PR #48 enables defaultNewTab for external links. This is the pattern the package now nudges you toward: stop hardcoding target="_blank" on every outbound link in Blade and let the stored link carry the intent. Only override when an editor explicitly unchecks it.
Internal route + anchor deep links. PR #34 fixes anchors being dropped from route-based links. The natural workflow now is:
/blog/post#comments).Use this for in-page table-of-contents links, FAQ jump links, and "jump to section" CTAs. Previously you'd have concatenated # manually in an accessor — delete those shims.
Repeater/Builder blocks. The picker works well inside Repeater and Builder schemas for footers, CTA grids, and navigation items. Store the whole link payload per block; avoid splitting url, label, target into separate top-level columns, or you'll fight the cast on every save.
Form-to-model flow. When using the field outside a Resource (custom Livewire form, action modal), pass the value object in via ->default(fn () => $record->link) and let the cast handle the save. Don't call ->formatStateUsing() with json_decode — that bypasses the cast and produces inconsistent state between create and edit screens.
Testing. Assert the persisted JSON shape once (internal vs external fixtures) and then assert rendered output. That keeps the brittle part contained when the package's payload gains keys like defaultNewTab.
defaultNewTab is a silent behaviour change. After upgrading from 2.1.0, existing external links may start rendering with target="_blank" where they previously didn't, if your Blade layer was already ignoring the stored target. Audit any template that hardcodes target — you can end up with a doubled attribute or a conflicting _self.'#'.$fragment in an accessor to compensate for PR #34, remove it now or you'll render #section#section.rel="noopener noreferrer" is not automatic. The package controls target; you control rel. Keep it in your render layer for every _blank link.defaultNewTab in their JSON. Make your template null-safe (?? false) rather than assuming the property exists on every hydrated Link.php artisan route:clear (or route:cache in deploy) before editors open the picker, or the route search will show stale/broken entries. Deleting a route silently breaks every stored link pointing at it — there's no referential integrity here.Link that hydrated from null and one that hydrated from an empty array behave differently when you call getUrl().Array to string conversion or the cast silently nulling on save, it's almost always a $fillable/$casts mismatch or a manual $model->link = $request->input('link') bypassing the form's state.http, https, mailto, tel) in the field or a FormRequest; the picker is a UI, not a URL sanitiser.actions/checkout 6→7 and fetch-metadata bumps don't affect runtime — nothing to do on your side.LinkPicker over forking the package; the value object is the stable seam, so code against it rather than against the field's internal state.How can I help you explore Laravel packages today?