Public navigation is data: menus and items created in Appearance → Menus. A plugin extends it by offering link sources in the menu builder and, if it ships a theme, by rendering menus with its own templates. (The admin sidebar is a different system — see Admin menu and dashboard.)
In the user guide: Menus.
| Model | Key columns / behaviour |
|---|---|
Menu (menus) | name. items() = top-level rows by sort; allItems() = every row. Extends BaseModel: changing a menu or item clears its tree/fit/rendered caches and the response cache |
MenuItem (menu_items) | menu_id, parent_id (nesting), sort, label, type (custom | route | resource), link (custom URL), route + parameters (JSON; the key _ui holds mega-menu presentation), resource_type/resource_id (morphTo), target, css_class, icon, is_mega, title, condition |
MenuItem::getUrl() resolves by type: a resource item uses the model's getUrl() method or url attribute; a route item calls route($route, $parameters) (a missing route gives #); a custom item uses link. Because routes are stored by name, do not rename or remove a public route that may be in someone's menu.
{!! menu('Main', 'bootstrap', ['class' => 'ms-auto', 'icon' => true]) !!} {{-- by name or numeric id --}}
{!! menu($id, 'footer') !!}
@php($tree = menu('Main', '_json')) {{-- the processed item collection, no HTML --}}menu($menuNameOrId, $type = 'bootstrap', array $options = []) (global helper → Menu::display()) renders resources/themes/{theme}/menu/{type}.blade.php with $items (a collection of MenuItem with href, label translated, active, children, is_mega) and $options (object).bootstrap (default dropdowns), megamenu, mobile, mobile-level, footer, topbar, inline, list. A theme can add or override any of them by providing the file.menu:{camelName}:tree:v2 and cleared on any menu/item change. Labels that are valid translation keys are translated at render time.primary_menu, secondary_menu, user_nav_menu, footer_menu, top bar menus); page-level settings may pin another.parameters._ui.mega (columns, width_mode, heading, image, …) turns its children into columns; App\View\Presenters\Echo\MenuPresenter prepares the layout (mega(), columnSpan()).Menu::fitBucket($name) returns a 1–15 "weight" the header prints as data-nav-fit so CSS can decide inline vs. drawer without JavaScript (Presets, SCSS and assets).The builder (Menus screen) shows one collapsible container per registered source: Pages, User menu, Posts, Post categories, Tags (core) plus Booking and TruCommerce ones. Items you offer are templates: when the administrator ticks them and clicks Add to menu their data (label, type, route, parameters, resource…) is copied into menu_items rows.
Modules/Playground/app/Helpers/PlaygroundBuilderMenu.php
use App\Facades\MenuBuilder;
use App\Services\Menu\MenuContainer;
use App\Services\Menu\MenuItem;
MenuBuilder::register('playground', function (MenuContainer $menu): void {
$menu->name('Playground')->key('playground-menu'); // container title and DOM key
}, 80); // order among containers
// The item list can be large: only build it on the builder screen itself.
if (! Str::contains(request()->route()?->getName() ?? '', 'admin.menus')) {
return;
}
$container = MenuBuilder::get('playground');
$container->item('playground-home', static function (MenuItem $item): void {
$item->label('Playground home')->key('playground-home')->target('_self')->route('playground.index');
});
Note::query()->latest('id')->limit(50)->get()->each(function (Note $note) use ($container): void {
$container->item('playground-note-' . $note->id, static function (MenuItem $item) use ($note): void {
$item->label($note->title)->key('playground-note-' . $note->id)->target('_self')
->route('resolve.show', ['slug' => $note->slug]); // slug-resolved pages use resolve.show
});
});Call it from your AdminBooting listener. The container then appears on /admin/menus, and adding items from it (admin.menus.add-items) creates MenuItem rows whose getUrl() resolves to your route.
MenuItem (builder template) method | Meaning |
|---|---|
label($text), key($id), target('_self'|'_blank') | Display text, the item's id inside the container (what the form posts), link target |
route($name, array $params = []) | Type route. Prefer this for plugin pages |
resource(Model::class, $id) | Type resource (morph): the URL is taken from the model (getUrl() or url). Use for CMS pages (the core Pages container does, or the page-type route when the page has one) |
url($string) | Type custom |
type(), classes(), icon(), tag(), priority() | Presentation and ordering |
MenuBuilder API: register($key, $callable, ?int $order), get($key), find(), all() (sorted by priority). MenuContainer: name(), key(), priority(), item($key, $callable), items([...]), hasItems().
Route::has($route)) so a disabled feature never leaks dead links.menu() with an administrator-selected menu id stored in your settings.resources/themes/{theme}/menu/{type}.blade.php. Copy bootstrap.blade.php as a starting point; items expose $item->href, $item->label, $item->active, $item->target, $item->children, $item->is_mega and menuIcon($item).children) and set mega options; delete old items through allItems(), not items() (see .ai/rules/niches.md).