wotz/filament-link-picker
A Filament form field for selecting links without leaving the admin panel. Pick internal pages, model records, or external URLs, set labels and targets, and store clean link data for menus, buttons, and rich content.
Install the package and confirm it registers itself with Filament's asset/component pipeline:
composer require wotz/filament-link-picker
php artisan filament:assets # if the package ships a JS/CSS asset
Look at three things first in the repo:
src/...LinkPicker.php or similar) — check its namespace, the Filament\Forms\Components\Field it extends, and what getView() points to.config/link-picker.php (if published) — this is where the route allow/deny lists, labels, and grouping usually live.['label' => ..., 'url' => ...] array. That single fact drives everything else.Register the provider if the package doesn't use package discovery, then publish config:
php artisan vendor:publish --tag="link-picker-config"
First use case: adding a "Link" field to a Page or NavigationItem resource where editors pick from named application routes instead of typing URLs by hand.
use Wotz\FilamentLinkPicker\Forms\Components\LinkPicker;
public static function form(Form $form): Form
{
return $form->schema([
LinkPicker::make('link'),
]);
}
Before wiring it into a resource, dump the stored value once (dd($record->link)) after saving — knowing the shape up front saves an hour of debugging later.
Single string storage (most common). Store the resolved URL or route name in a string column and resolve at render time:
LinkPicker::make('link')
->label('Destination')
->required(),
Label + URL pairs. If the field returns an array, cast on the model and render defensively:
// Model
protected $casts = ['link' => 'array'];
// Blade
@php($link = $page->link)
@if ($link)
<a href="{{ $link['url'] ?? '#' }}">{{ $link['label'] ?? $link['url'] }}</a>
@endif
Constrain the picker. Whatever option method the package exposes (->routes(), ->only(), ->exclude(), or a config key), narrow it per-field rather than globally — a footer link picker and a hero CTA picker rarely want the same route set:
LinkPicker::make('cta_link')
->options(fn () => collect(config('link-picker.routes'))
->only(['home', 'pricing', 'contact'])),
Inside repeaters and builders. This is the highest-value integration and where link pickers earn their keep. Give each repeater item a distinct ->key()-free but uniquely named field and confirm Livewire rehydration preserves the array — test by adding three items, saving, and reloading.
Polymorphic link targets. A common upgrade path: keep the picker for URL selection, but add a link_type toggle (route / url / model) so editors can point at an Eloquent record. Use ->visible() / ->live() on the toggle to switch between the picker and a plain TextInput or Select.
Table and infolist display. Don't dump the raw array into a TextColumn. Use a formatter or a small accessor:
TextColumn::make('link.url')
->label('Link')
->limit(40)
->url(fn ($record) => $record->link['url'] ?? null)
->openUrlInNewTab(),
Seeding and factories. Because the field is form-only, factories must produce the same shape the view expects. Add a helper on the model or a factory state so fixtures don't drift from real saved output.
Route::getRoutes(), a cached route table (route:cache) can change what's listed — especially for routes registered conditionally. Run php artisan route:clear when the option list looks stale in local dev.posts.show require a {post}. A naive picker will happily offer them and produce a broken link. Either exclude them in config or add a companion "route parameters" input.auth()->user()->can(...) or by a prefix in the config.string but the field emits an array (or vice versa), so $record->link is "Array" or the form throws on rehydrate. Decide the shape once, cast it on the model, and write the column as json/text accordingly.->dehydrated() / ->afterStateHydrated() wiring, and make sure the Alpine component initialises from $wire.get() rather than only on x-init.composer.json, and read the source of the field class before relying on any ->method() you saw in a README — signatures drift.getView() with your own Blade view rather than forking. Keep the fork in app/Filament/Fields/ so composer update stays safe.assertDatabaseHas('pages', ['link' => json_encode([...])]). For Livewire form tests, fillForm(['link' => 'https://...']) then assertHasNoFormErrors().filament/filament's built-in Select with ->options(Route::getRoutes()...) or a maintained link-picker (e.g. a TinyMCE/Curator-style package) may be lower risk than a one-star dependency you'll end up forking.How can I help you explore Laravel packages today?