wotz/filament-menu
wotz/filament-menu is a Filament package for creating and managing navigation menus from your admin panel. Build menu structures, attach links, and render them in your Laravel application through a simple Filament-integrated workflow.
Install and inspect before you wire anything up.
composer require wotzebra/filament-menu
php artisan vendor:publish --tag="filament-menu-migrations"
php artisan vendor:publish --tag="filament-menu-config"
php artisan migrate
Publishing tags for an unproven package are worth guessing at first — if the tag names differ, run php artisan vendor:publish interactively and pick the provider by number. That single step tells you more than the README: it reveals whether config and migrations are separable, and whether views are publishable (which is your escape hatch for custom markup later).
Where to look first, in this order:
database/migrations/*_create_*menu*_tables.php — the schema is the real API. Note whether items are a self-referencing tree (parent_id) or a flat sortable list, whether there's a linkable_type/linkable_id polymorphic pair, and whether a panel/tenant_id column exists. Every integration decision downstream follows from these three facts.config/filament-menu.php — look for cache TTL, model class overrides, and a list of "linkable" models. Model overrides mean you can subclass instead of forking.src/*Plugin.php and src/Resources/ — confirm the plugin class FQCN and whether the resource is registered automatically or must be added to the panel's resources().resources/views/ — only if you intend to control markup; otherwise render via the package's component.Register the plugin in your panel provider:
->plugin(\Wotz\FilamentMenu\FilamentMenuPlugin::make())
(Confirm the namespace from composer.json's PSR-4 block — with zero dependents there's no blog post to copy from.)
First use case: create a "Main Navigation" menu in the admin, add three items (one linking to a Page model, one to a route, one external), then render it in layouts/app.blade.php via the package's Blade component or a Menu::findBySlug('main')-style helper. Getting one item of each link type working end-to-end surfaces 90% of the gotchas immediately.
Treat menus as content, not config. The whole value of the package is letting an editor change nav without a deploy. That means:
Menu::updateOrCreate(['slug' => 'main'], ['name' => 'Main Navigation']) so fresh environments have a menu to attach items to, but let humans own the items. Never seed items in production seeders — you'll fight duplicates on every deploy.linkable_type/linkable_id, wire your Page, Post, Category models into the package's linkable config. Editors then pick a page by title, and the URL is resolved at render time — which survives slug changes and domain moves. Fall back to a raw url column only for external links.Cache::remember("menu.main.{$locale}", 3600, fn () => view(...)->render()). Key by slug and locale and tenant if you have one.saved/deleted) and flush by cache tag. Filament's drag-to-reorder often writes order columns in bulk without firing model events — verify this, and if so, hook the resource's reorder action or bust the cache on every menu page render.Rendering partial: keep it in your app, not the vendor views.
@foreach ($items->whereNull('parent_id') as $item)
<a href="{{ $item->url }}" target="{{ $item->target ?? '_self' }}"
@class(['active' => $item->isActive()])>{{ $item->label }}</a>
@if ($item->children->isNotEmpty())
@include('partials.menu-items', ['items' => $item->children])
@endif
@endforeach
Publish and copy the vendor view into your app rather than @include-ing filament-menu::... — you want the markup under your control and diffable when the package updates.
Multi-panel / multi-tenant: register the plugin only in the panels that need menu editing (usually one admin panel), and if the schema carries a tenant or panel column, scope the resource query in a getEloquentQuery() override on a subclassed resource. Don't let editors see another tenant's menus.
Authorization: add a MenuPolicy and register it. Menu editing is a content-editor permission, not a super-admin one — but deleting a menu that a live layout depends on should be restricted harder than editing items.
Testing: assert on a rendered string, not on models. $this->get('/')->assertSee('Main Navigation') plus a test that a hidden menu item 404s its page catches most regressions.
composer.json ("wotzebra/filament-menu": "1.2.3", not ^1.0), commit composer.lock, and read src/ before shipping. Budget for the possibility of forking — check the license (MIT, so forking is fine) and keep your overrides in subclasses and app-level views so a fork stays cheap.composer.json require block) before anything else. Filament v3 → v4 changed form/table APIs substantially; a plugin that hasn't been touched will throw on ->reorderable() or schema method names./en/about vs /about), and on staging→production promotion. Prefer linkable models or route names with parameters; keep absolute URLs only for genuinely external links.with('childrenRecursive') or whatever the package names it). A 40-item menu with lazy children will issue 40 queries per page load, and it will never show up in local profiling because your local DB is warm.Page is deleted, the menu item survives pointing at nothing. Add a nullOnDelete FK if the schema allows, or a Menu::prune() command scheduled nightly, plus a isActive()/url() null-guard so the render doesn't 500 the whole layout.php artisan about shows registered plugins and resources per panel. If the Menus resource doesn't appear, it's almost always a missing ->plugin() call, a cached config (php artisan optimize:clear), or a policy silently denying viewAny.vendor/.composer outdated in CI; treat a patch bump as a code review, not an automatic merge.How can I help you explore Laravel packages today?