balajidharma/laravel-menu
A Laravel package for building and rendering navigation menus. Define menus and nested items fluently, highlight active routes, add icons or attributes, and output ready-to-use HTML for your layouts. Ideal for admin panels and site navigation.
The package is database-driven: menus and their items live in tables, edited through your app (or via Tinker/seeder for the first pass), not in a config array. That's the key difference from config-based menu libraries like spatie/laravel-menu — you're modeling content, not hardcoding a tree.
Install and scaffold:
composer require balajidharma/laravel-menu
php artisan vendor:publish --provider="BalajiDharma\LaravelMenu\MenuServiceProvider"
php artisan migrate
Where to look first:
config/menu.php (table names, model bindings, route/URL resolution settings).Menu and MenuItem models the package registers — they define the attributes you'll actually manipulate (name, url, route, target, order, parent_id, is_active, etc.).First use case — seed a header menu and render it:
use BalajiDharma\LaravelMenu\Models\Menu;
use BalajiDharma\LaravelMenu\Models\MenuItem;
$menu = Menu::create(['name' => 'main', 'slug' => 'main']);
$menu->items()->createMany([
['title' => 'Home', 'url' => '/', 'order' => 1],
['title' => 'Blog', 'route' => 'posts.index', 'order' => 2],
]);
<x-menu name="main" />
{{-- or, depending on your version --}}
@menu('main')
Don't assume the Blade component/helper name — read the README's render section once, because this is the one API surface that changes between versions of the package.
Admin CRUD as the primary integration point. This package shines when you wire it into a Filament/Nova/plain Blade admin. Treat Menu and MenuItem as normal Eloquent models: Menu::where('slug','footer')->first(), then eager-load items.children to avoid N+1 in the renderer.
Parent/child via parent_id. Most database-menu packages use adjacency lists rather than nested sets. Building a tree for display means recursive eager loading:
$menu = Menu::where('slug', 'main')
->with(['items' => fn ($q) => $q->whereNull('parent_id')->orderBy('order'),
'items.children' => fn ($q) => $q->orderBy('order')])
->first();
If the package ships a tree helper (e.g. ->tree()), prefer it over hand-rolled recursion — but verify it orders correctly at each level.
Polymorphic linking. For CMS-style sites, link menu items to real models (a Page, a Category) via a menuable morph rather than a raw URL. Store a fallback url so the menu doesn't break when the related model is deleted. If the package doesn't expose the morph, add it: a menuable_type/menuable_id migration on the items table plus a morphTo() in a subclassed MenuItem bound via config.
Route vs URL resolution. For internal links, store the route name + parameters (route, route_params JSON) and resolve at render time. This keeps links alive when slugs change. Reserve raw url for external links and mailto/tel. Set the target/rel attributes on the item, not in the Blade template, so non-developers can control _blank and nofollow.
Multi-menu, multi-location. Use slug (or a location column) as the stable lookup key, never the display name. Your layout references main, footer, account — admins can rename the menu without breaking templates.
Caching. The whole point of the DB is that it changes rarely. Cache the rendered HTML per menu slug and bust it in a model observer:
MenuItem::saved(fn ($i) => Cache::forget("menu:{$i->menu->slug}"));
MenuItem::deleted(fn ($i) => Cache::forget("menu:{$i->menu->slug}"));
Wrap the render call in Cache::remember("menu:{$slug}", 3600, fn () => ...). This removes the query cost from every page request and makes the package production-viable.
Authorization. Don't expose menu editing to every authenticated user. Gate the admin routes on a permission (menu.manage) and validate parent_id on update — a malicious or buggy request can set an item as its own ancestor and produce an infinite render loop.
parent_id isn't the item itself or a descendant. Recursive tree builders will segfault/stack-overflow otherwise. Add this check regardless of what the package does.order = 5 under two different parents are independent sequences; a drag-and-drop admin needs to reorder siblings, not the flat table. Keep an order integer and accept gaps (1, 2, 3, 7) rather than renumbering on every move.DB::enableQueryLog() on a page with a header, footer, and sidebar menu.table_names after migrating, you'll get cryptic "table not found" errors on relationship loads. Re-publish and re-run migrations when changing this.Menu/MenuItem classes, you must also update the morph map and any resolveRelationUsing calls — otherwise menu->items returns base models without your added columns.title is admin-supplied HTML (e.g. an icon + label), confirm the renderer's escaping behavior before letting untrusted users into the admin. Prefer a separate icon column over {!! !!}.route() output and later change APP_URL or deploy behind a different asset host, cached menus keep stale absolute URLs. Cache the resolved tree or relative paths, not the final HTML, if the domain changes.<ul></ul>).saved/deleted), the config's model class bindings, and any published Blade views — publish the views to customize markup without forking; only subclass the models when you need new columns or relationships.How can I help you explore Laravel packages today?