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

Filament Link Picker Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. The field class (usually src/...LinkPicker.php or similar) — check its namespace, the Filament\Forms\Components\Field it extends, and what getView() points to.
  2. config/link-picker.php (if published) — this is where the route allow/deny lists, labels, and grouping usually live.
  3. The Blade view — tells you whether the value is stored as a plain string, or a ['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.

Implementation Patterns

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.

Gotchas and Tips

  • Route caching. If the picker enumerates routes via 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.
  • Parameterised routes. Routes like 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.
  • Panel and auth scoping. Route lists are global; if you have multiple Filament panels, the picker will offer routes the current user can't reach. Filter by auth()->user()->can(...) or by a prefix in the config.
  • Array vs. string mismatch. The #1 bug: column is 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.
  • Livewire rehydration. Custom fields with JS-driven dropdowns sometimes lose state after a Livewire roundtrip. If the value disappears after an unrelated field changes, check whether the field needs ->dehydrated() / ->afterStateHydrated() wiring, and make sure the Alpine component initialises from $wire.get() rather than only on x-init.
  • No dependent ecosystem. With zero dependents and one star, treat every behaviour as undocumented. Pin the version in composer.json, and read the source of the field class before relying on any ->method() you saw in a README — signatures drift.
  • Extension point. If you need behaviour the field doesn't expose, subclass the field and override getView() with your own Blade view rather than forking. Keep the fork in app/Filament/Fields/ so composer update stays safe.
  • Testing. Assert on the persisted value, not the rendered widget: assertDatabaseHas('pages', ['link' => json_encode([...])]). For Livewire form tests, fillForm(['link' => 'https://...']) then assertHasNoFormErrors().
  • Consider the alternative. For anything beyond simple route selection, 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.
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.
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
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager