ryangjchandler/filament-navigation
Filament Navigation is a Laravel/Filament package for building and managing site navigation menus in the admin panel. It offers a drag-and-drop menu builder with custom links, model-linked items, and simple Blade rendering for flexible navigation.
ryangjchandler/filament-navigation is a Filament plugin that gives you database-backed, editor-managed navigation menus (header, footer, sidebar, etc.) with a tree of items and pluggable item types.
Minimal setup:
composer require ryangjchandler/filament-navigation:"^1.0@beta"
php artisan vendor:publish --tag="filament-navigation-migrations"
php artisan migrate
Register the plugin on the panel you want editors to use:
use RyanChandler\FilamentNavigation\FilamentNavigation;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(FilamentNavigation::make());
}
Then in the admin panel create a navigation (e.g. handle main), add items, and render it in a Blade layout — either with the package's Blade component or by resolving the model and recursing over items/children.
Where to look first:
config/filament-navigation.php (if published) for item types and defaults.NavigationResource — that's the editor-facing surface.RyanChandler\FilamentNavigation\Models\Navigation / NavigationItem — the data contract you render against.First use case: a main menu in the site header, seeded once via a seeder, then handed to content editors so marketing can reorder/add links without a deploy.
One handle per menu, treated as code. Create main, footer, legal as separate Navigation records and resolve them by handle in your layout, not by ID. Handles are the stable contract between CMS and templates.
Custom item types for structured links. Instead of raw URLs everywhere, register typed items so editors pick from real app data:
FilamentNavigation::make()
->itemTypes([
'page' => [
'name' => 'Page link',
'fields' => [
Select::make('page_id')->options(Page::pluck('title', 'id')),
TextInput::make('label'),
],
'casts' => ['page_id' => 'integer'],
],
]);
Then in the Blade partial, switch on the item type and resolve the target (Page::find($item->data['page_id'])?->url).
Recursive rendering. Navigation items are hierarchical; a single recursive Blade partial (resources/views/navigation/item.blade.php) that calls itself on $item->children keeps header/footer/sidebar consistent and handles arbitrary depth. Add an @include depth guard if editors can nest freely.
Cache the tree, bust on save. Menus are read on every page request and written rarely:
$menu = Cache::rememberForever("nav.{$handle}", fn () =>
Navigation::with('items.children')->where('handle', $handle)->first()
);
Flush in a NavigationItem/Navigation saved/deleted observer (or a model event on the parent). Eager-load items.children — lazy-loading the tree is the classic N+1 in this package.
Panel scoping. Register the plugin only on your admin panel, and gate the resource with a policy so non-admin panels/roles don't get a menu editor they shouldn't touch. If you run multiple panels, register the plugin per panel and share handles deliberately.
Seeding for repeatable environments. Seed the main/footer records and their baseline items so fresh installs, CI, and staging have a working menu before anyone opens the admin UI.
It's still beta — pin it. Use "^1.0@beta" and commit composer.lock. Between beta5 and beta6 the only shipped change was a fix so the item name field no longer overrides the handle on the edit page. Practically: if you renamed items on a beta5 install, re-check those records after upgrading — a handle that was silently rewritten will now surface, and any frontend lookup keyed on the old handle will return null.
Handles are not content. Because name/handle fields live on the same edit form, it's easy for editors (and previously the bug itself) to mutate the handle. Keep handles stable, ideally exposed only to super-admins or set purely in seeders, and always null-check the resolved navigation — a missing handle should degrade to no menu, not a 500 on every page.
Upgrade checklist: composer update ryangjchandler/filament-navigation, re-run published migrations if the changelog mentions them, then php artisan filament:optimize-clear (or optimize:clear) so Filament picks up the new resource/form schema. Clear your own nav cache too — it won't be invalidated by a composer update.
Item type registration replaces, it doesn't merge. Re-declaring itemTypes() in a service provider can drop the built-in defaults; declare the full set you want in one place. Same for casts: keys must match the field names exactly or the cast silently does nothing and your Blade switch gets strings where it expected ints.
Debugging render issues: dump $navigation->items before touching Blade — most "menu is empty" reports are a handle typo, a cache entry from a pre-seed request, or items saved as children of a deleted parent. Cache::forget("nav.{$handle}") is your first move.
Extension points worth knowing: custom item types + casts, the item form schema, model events for cache invalidation, and the resource's policies for access control. If you need per-locale menus, run one navigation per locale handle (main.en, main.fr) rather than fighting the schema.
How can I help you explore Laravel packages today?