infinety-es/nova-menu-builder
A Laravel Nova package that provides a drag-and-drop menu builder for creating, editing, and organizing custom navigation menus. Manage menu items, link types, and ordering through an intuitive admin interface, making it easy to build dynamic menus for your Nova app.
Install and publish assets
composer require infinety-es/nova-menu-builder
php artisan vendor:publish --provider="Infinety\MenuBuilder\MenuBuilderServiceProvider"
php artisan migrate
The publish step drops a config file (locales/labels) and two migrations — typically menus and menu_items. Read those migration files first: they tell you the exact column names (name, locale, link_type, url, route, order, parent_id, etc.) that every query and Blade template will depend on.
Register the tool in app/Providers/NovaServiceProvider.php:
public function tools()
{
return [
new \Infinety\MenuBuilder\MenuBuilder(),
];
}
First use case — a "Main navigation" menu. Open the new Nova tool, create a menu named main for your locale, then add a few links (route names or full URLs) and drag them into order. Consume it in a layout:
use Infinety\MenuBuilder\Models\Menu;
$menu = Menu::where('name', 'main')->first();
Then loop $menu->items in a Blade partial, recursing into children where they exist. Do this once in a layout and share it with a view composer (see below) — not inside every view.
That's the whole loop: manage in Nova → query the model → render yourself. There is no meaningful frontend runtime to learn; the package is essentially an admin UI over two tables.
Share menus via a view composer, not per-view queries. Register a composer in a service provider and bind the resolved menu tree to your layout:
View::composer('layouts.app', function ($view) {
$view->with('mainMenu', Menu::where('name', 'main')->first()?->items);
});
Seed menus for reproducible environments. Because the content lives in DB rows, treat menus like fixtures: a MenuSeeder that creates the main/footer records and their items means every dev and deploy gets identical nav, and content editors only adjust from there.
Render recursively with a partial. One partial that loops items and @includes itself for children keeps the markup identical at every depth and avoids the temptation to hardcode two levels:
{{-- resources/views/partials/menu.blade.php --}}
<ul>
@foreach ($items as $item)
<li>
<a href="{{ $item->url ?? route($item->route) }}">{{ $item->title }}</a>
@if ($item->children->isNotEmpty())
@include('partials.menu', ['items' => $item->children])
@endif
</li>
@endforeach
</ul>
Cache the rendered tree, keyed by menu name and locale. Menu content changes rarely but is read on every request. Cache::remember("menu.main.".app()->getLocale(), 3600, fn () => ...) around the query is the single highest-value integration step.
Drive item links by link_type. The package distinguishes URL vs. route (and possibly internal model) links. Centralise the resolution in a MenuLink helper or an accessor on the item model so your Blade stays a single {{ $item->href }} and a broken route name surfaces in one place instead of dozens of templates.
Multi-locale as separate menus. If the config exposes locales, create main per locale and pick by app()->getLocale() with a fallback to your default locale — don't try to build translation logic on top of individual items.
Editorial workflow tip: give content editors a Nova canSee/policy gate on the tool so nav changes don't require a deploy or developer involvement. That's the actual value proposition of this package — it replaces hardcoded nav arrays.
dev-master; upstream fixes are not coming.orderBy('order')) — drag-and-drop writes the sort value, but nothing guarantees relationship queries return in that order.children fires a query per item. Load items.children up front (or use a recursive eager-load) or your nav will quietly become the slowest part of the page.route($item->route) throws at render time — on every page, in production. Prefer resolving in a try/catch with a logged warning, or favour stored URLs for volatile links.Cache::remember. Add an observer on the menu/item models (or a Nova action hook) that flushes the relevant keys, otherwise editors will "save" changes that never appear.parent_id rows depending on FK cascade settings in the migration. Check the migration and clean up in an observer if cascades aren't there.null; guard against ->first() returning null in your composer or your layout breaks on the first new locale.How can I help you explore Laravel packages today?