Skip to main content

Overview

A catalog menu is the primary navigation element that allows customers to browse the store by category. It is built from Lunar’s collection hierarchy: collection groups organize top-level navigation sections, and collections within each group form nested trees of categories and subcategories. This guide walks through querying collection groups and their nested collections, rendering a multi-level navigation menu, highlighting the active collection, and caching the result for performance. The examples below use standard Laravel controllers and Blade templates. The same concepts apply whether the storefront is built with Livewire, Inertia, or a headless API.

How Collections Are Organized

Lunar organizes collections into a two-level structure:
  • Collection Groups act as top-level containers (e.g., “Main Menu”, “Footer Links”, “Seasonal Promotions”). Each group has a unique handle for identification.
  • Collections within a group form a nested tree using parent-child relationships. A root collection has no parent, and each root can have unlimited levels of children.
Main Menu (CollectionGroup)
Clothing
Men's
T-Shirts
Jackets
Women's
Dresses
Tops
Footwear
Sneakers
Boots
Accessories

Querying the Menu Data

Fetching a Collection Group

Retrieve a collection group by its handle. The handle is set when creating the group in the admin panel or via code.

Fetching Root Collections

Root collections are those with no parent. Use the nested set’s whereIsRoot() scope to retrieve them.
The defaultOrder() scope orders collections by their nested set position, which matches the order configured in the admin panel.

Fetching Children

Each collection’s children can be loaded via the children relationship. To build a full menu tree, eager load the children (and their children) to avoid N+1 queries.
This loads three levels of the menu hierarchy. Add additional children levels as needed for deeper menus.
Eager loading children with constrained queries (using the closure syntax) ensures each level is ordered correctly by its nested set position.

Using descendantsOf for the Full Tree

For menus that need the entire tree regardless of depth, the nested set provides a descendantsOf method. However, for navigation menus it is generally better to eager load a fixed number of levels to keep queries predictable and avoid loading unnecessary data.
The toTree() method on the resulting collection restructures the flat list into a nested tree, with each node’s children relation populated.

Building a Menu Service

Encapsulate the menu query in a dedicated service class to keep controllers clean and make caching straightforward.
Register it in a service provider or resolve it directly:

Rendering the Menu

Sharing Menu Data with All Views

Use a view composer or middleware to make the menu available to every page without passing it from each controller.
When using a view composer with '*', the menu query runs on every request. Pair this with caching (covered below) to avoid repeated database queries.

Basic Menu Template

Recursive Menu for Unlimited Depth

For menus with an arbitrary number of levels, use a recursive Blade partial. Create a partial at resources/views/partials/menu-item.blade.php:
Then render the top-level menu:

Active State Tracking

Highlighting the active collection (and its ancestors) helps customers understand where they are in the catalog hierarchy.

Determining the Active Collection

In the collection controller, pass the current collection and its ancestor IDs to the view:

Applying Active Styles

Share the active IDs with the layout so the menu partial can use them:
This highlights both the current collection and all its parent collections in the menu, giving customers a clear visual trail.

Caching

The catalog menu changes infrequently compared to how often it is rendered. Caching prevents a database query on every page load.

Cache the Menu Result

Update the CatalogMenuService to cache the menu:

Invalidating the Cache

Clear the cache whenever collections are updated. A model observer is a clean way to handle this:
Register the observer in a service provider:

Multiple Menus

A store may need more than one navigation menu, for example a main header menu and a footer menu. Use separate collection groups for each.
Share both via a view composer:
Each menu is cached independently under its own key.

Channel and Customer Group Filtering

Collections support channel scheduling and customer group visibility. To display only collections available to the current browsing context, apply these scopes when building the menu.
When filtering by channel or customer group, the cache key should include the channel and customer group IDs to prevent one customer group from seeing another group’s cached menu.

Putting It All Together

Here is a complete service, view composer, and Blade template for a cached, multi-level catalog menu:

Service

View Composer

Blade Template

Next Steps