norman-huth/nova-menu
A Laravel Nova tool for creating and managing site menus from the Nova admin panel. Build hierarchical, drag-and-drop menus, link to Nova resources or custom URLs, and render them in your frontend with a simple API.
Install into a Nova app (Nova 4/5 assumed — check the README matrix before committing):
composer require norman-huth/nova-menu
Publish and migrate. This is a database-backed menu builder, so nothing works until the tables exist:
php artisan vendor:publish --provider="NormanHuth\NovaMenu\NovaMenuServiceProvider"
php artisan migrate
Publish before migrating; the config file controls table names and (typically) the set of fields available on a menu item.
Register the Nova resources. Most Nova packages in this family do not auto-register their resources reliably across Nova majors. Add the packaged Menu / MenuItem resources to NovaServiceProvider::resources() (or verify the package registers them itself — look in the service provider's boot()).
Look here first, in order:
config/nova-menu.php — models, table names, item field definitions, cache settings.Menu model — this is your rendering entry point.First use case: create a "Main navigation" menu in the Nova UI, add a handful of items (label + URL/route), then render it in your Blade layout:
{{-- in resources/views/layouts/app.blade.php --}}
{!! menu('main') !!}
Confirm the exact helper/directive name in the README — this package exposes the rendered menu through a helper and/or a Blade component, and the signature differs between releases.
Sanity check: if the menu renders empty, you almost certainly have (a) the wrong slug, (b) a menu that exists but is not marked active/enabled, or (c) a stale cache. Hit the empty-menu path deliberately on day one so you recognise it later.
Multi-menu by slug. The intended model is one row per menu (main, footer, sidebar, legal), each with a slug you query by. Treat the slug as a stable contract in your Blade templates and never let editors rename it — add a config-level enum of allowed slugs if the UI doesn't enforce one.
Layout integration via a view composer, not a helper call in the layout. Calling the helper directly in app.blade.php means every page triggers the same query. Wrap it:
View::composer('layouts.partials.nav', function ($view) {
$view->with('menu', \Cache::remember('menu.main', 3600, fn () => menu('main')));
});
Then {!! $menu !!} in the partial. This gives you one place to invalidate from.
Cache invalidation on edit. Hook the packaged models' saved/deleted events in a small observer and forget the matching cache key. This is the single highest-value integration you can write; without it, editors will report "my changes don't show up" and you'll be clearing caches manually for the life of the project.
Seeding for reproducible deploys. Menus edited in production are content, and content doesn't live in git. Pick one: seed a baseline menu in a seeder for fresh environments, and treat production edits as data. Don't try to seed and hand-edit the same menu — you'll get duplicates.
Custom item types. The item fields are config-driven. For anything beyond label/URL — e.g. a link to a CMS page, a route with parameters, a "mega menu" column — extend the config's field list rather than forking the package, and resolve the final URL in your own presenter/renderer rather than in the database. Storing a resolved URL means it goes stale the moment a slug changes; storing a route name + params survives.
Authorization. The Nova resources are editable by anyone with Nova access by default. Add policies if "who can restructure the site navigation" is narrower than "who can log into Nova" — it usually should be.
Localisation. If the site is multilingual, run one menu per locale (main.de, main.en) rather than items with translated labels. It keeps the tree structure independent and lets translators work without stepping on each other.
Testing. Snapshot the rendered HTML for a fixture menu so a package upgrade that changes markup fails loudly instead of silently breaking your CSS selectors.
composer.json, and if you need a change, plan to fork or vendor it rather than waiting. Budget for the migration path before you build deep dependencies on it.migrate first gives you the package's default tables, and a later config edit (custom table names) leaves the database out of sync with the config with no error — just a menu that silently returns nothing.{!! !!} to render the generated markup. That is only safe because the package escapes item labels — if you add a custom field (a rich-text description, an icon SVG), verify escaping yourself before printing it raw.php artisan cache:clear, (2) confirm the slug exists in the DB, (3) confirm the resources are actually registered in Nova, (4) dump the raw model rather than the rendered helper to separate data issues from rendering issues.composer update and, with an archived package, that update is exactly when you'll be firefighting.How can I help you explore Laravel packages today?