Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Navigation Module Laravel Package

anomaly/navigation-module

A Laravel navigation module for creating and managing site menus in Anomaly-based applications. Provides database-backed menu trees, configurable links, item ordering, and admin-friendly controls for rendering flexible, editable navigation across your Laravel views.

View on GitHub
Deep Wiki
Context7

Getting Started

The Navigation module gives you admin-managed menus (called "navigations") composed of links to pages, module routes, or raw URLs, plus a rendering helper you drop into your layout. Treat the module as the source of menu structure, not as your front-end menu component.

Install and scaffold

composer require anomaly/navigation-module
php artisan addon:install anomaly.module.navigation

Then in the admin panel a Navigation section appears (typically under Content). Create at least one navigation with a slug like main, add a couple of links pointing at pages, and drag them into order.

Where to look first

  • vendor/anomaly/navigation-module/src/Navigation/ – the navigation model/repository (NavigationRepositoryInterface).
  • src/Link/ – links, presenters, and the LinkType abstraction. This is where 90% of customization happens.
  • src/Link/Type/ – the built-in link types (page, url, and whatever else the version ships). Copy one of these when writing your own.
  • resources/views/ – the admin and rendering views; mirror paths under resources/{your_theme}/addons/ to override without touching vendor code.
  • src/NavigationModuleServiceProvider.php – routes, bindings, view/asset registrations, and the admin menu registration.

First use case: render main in your layout

The module's Twig/Blade bridge exposes a navigation() helper that resolves a navigation by slug and returns its links as presenters:

{# resources/views/layouts/app.twig #}
<ul class="menu">
    {% for link in navigation('main').links %}
        <li class="{{ link.active ? 'is-active' }}">
            <a href="{{ link.url }}" target="{{ link.target }}">{{ link.title }}</a>
        </li>
    {% endfor %}
</ul>
{{-- Blade equivalent --}}
<ul class="menu">
    @foreach(navigation('main')->links as $link)
        <li class="{{ $link->active ? 'is-active' }}">
            <a href="{{ $link->url }}" target="{{ $link->target }}">{{ $link->title }}</a>
        </li>
    @endforeach
</ul>

Start with a hardcoded main slug in one layout. Don't try to build a fully recursive, cache-aware menu renderer on day one — get links on screen, then layer on nesting.

Implementation Patterns

Always render through a single partial. Create partials/navigation.twig (or a Blade component) that takes a slug and a CSS class prefix. Every layout includes that partial. When the client wants a footer menu, dropdowns, or a "utility" bar, they add a navigation in the admin and you include the same partial with a different slug — no code change, no deploy.

Handle nesting with recursion, once. Links carry a parent_id and a position; the presenter exposes children. Build one recursive macro and never think about it again:

{% macro tree(links, depth) %}
    <ul class="menu menu--{{ depth }}">
        {% for link in links %}
            <li>
                <a href="{{ link.url }}">{{ link.title }}</a>
                {% if link.children.count %}
                    {{ _self.tree(link.children, depth + 1) }}
                {% endif %}
            </li>
        {% endfor %}
    </ul>
{% endmacro %}

Programmatic creation via repositories. For seeds, installers, or tests, resolve the repositories and create links rather than inserting rows by hand, so slugs, positions, and type resolution stay consistent:

$navigations = app(NavigationRepositoryInterface::class);
$links       = app(LinkRepositoryInterface::class);

$nav  = $navigations->create(['slug' => 'footer', 'name' => 'Footer']);
$links->create([
    'navigation_id' => $nav->getId(),
    'type'          => 'url',
    'title'         => 'Privacy',
    'url'           => '/privacy',
    'position'      => 1,
]);

Use this pattern for demo content in migrations — clients can then rearrange everything in the UI without you re-seeding.

Custom link types for domain objects. When a client needs a menu of "service categories" or "product ranges," don't make them paste URLs. Add a LinkType that stores an entry_id and resolves title/URL at render time:

class CategoryLinkType extends LinkType
{
    protected $slug = 'category';

    public function url()
    {
        return $this->link->category ? $this->link->category->route('view') : '#';
    }

    public function title()
    {
        return $this->link->category->name ?? $this->link->title;
    }
}

Register it alongside the built-in types in the addon's service provider, mirror a built-in link type's admin form fields, and you get a menu type the client can manage fully themselves. This is the single highest-leverage extension point in the package.

Let pages attach themselves. Note how the Pages module listens for page lifecycle events and creates/updates its links. If you have another first-class content type (a "Team Member" module, say) that should always be reachable, mirror that listener pattern so new records get a nav link automatically instead of relying on editors.

Cache the rendered output, not the query. Navigation links are cheap but rendered on every request in every layout. Wrap the partial in your own cache keyed by slug and bust it on link/navigation save events:

$html = Cache::rememberForever("nav.main", fn () => view('partials.navigation', [
    'links' => Navigation::links('main'),
])->render());

Register a listener on the link/navigation saved/deleted events to Cache::forget("nav.main"). This keeps menus instant while staying correct when an editor reorders things.

Reuse the data, not just the markup. The same link collection is a good source for a sitemap, breadcrumb trail, or mobile drawer. Pull Navigation::links('main') in a controller and transform it, rather than scraping your own HTML.

Gotchas and Tips

  • The linked repository is the forum module. The package name (anomaly/navigation-module) and the URL in the metadata disagree, and docs are thin. Treat vendor/anomaly/navigation-module/src/ as the authoritative documentation — read the link types and views before assuming an API exists.
  • navigation() returns presenters, not models. $link->url, $link->title, and $link->active are presenter accessors; some are computed per-request. Don't try to save() on them or serialize the collection to a queue — store IDs instead.
  • Orphaned links. Deleting a page or a referenced entry leaves a link row that resolves to # or throws. Guard url() with a null check in custom link types and add a cleanup listener on entry deletion. Periodically sanity-check the admin list for dead links.
  • Stored title vs. resolved title. Page and custom links often store a title and can resolve a live one. Decide which wins, then be consistent — otherwise renaming a page silently leaves stale menu text, or editor-provided overrides get ignored. Document the decision in your link type.
  • Nesting is your responsibility in the template. Nothing renders children for you. If nested items "disappear," it's almost always a flat foreach over top-level links. Filter by parent_id === null at the root and recurse.
  • Ordering is a position column. Drag-and-drop in the admin writes it. When inserting programmatically, always set position explicitly (or call the repository's sort method) or new links land unpredictably at the top.
  • Active-state detection is heuristic. It typically compares the request path to the link URL, so trailing slashes, query strings, locale prefixes, and nested routes can all misfire. For anything non-trivial, compute active state in your own view composer from request()->url() and ignore the built-in flag.
  • Multi-site / multi-locale scoping. Navigation records are generally not site-scoped in a way you can rely on across all setups; if you run multiple sites or locales off one install, verify per-site behavior early and, if needed, key navigations by slug prefix (en-main, fr-main) rather than assuming isolation.
  • Cache invalidation is manual. php artisan view:clear after overriding views, php artisan cache:clear after reordering links in the admin if you have added your own render cache, and remember that addon view overrides are picked up by path — a typo in the override path silently falls back to the vendor view, which looks like "my changes did nothing."
  • Permissions bite in staging handoffs. Link/navigation management is gated by module permissions. If an editor can't see the Navigation menu, check their role's module permissions before debugging the code.
  • Don't hack vendor files. Every view in resources/views/ can be overridden from your theme; every behavior can be extended via a link type or a listener. Vendor edits vanish on composer update.
  • Debug with tinker. php artisan tinker → Navigation::links('main') is the fastest way to distinguish "no links exist" from "template isn't rendering them."
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle