Navigation is the backbone of any website’s user experience. In Craft CMS, the flexibility of the platform means there isn't just one way to build a menu; instead, you have a variety of tools at your disposal to create a navigation system that fits your specific project needs. Whether you are building a simple one-level list or a complex, multi-level mega-menu, understanding the underlying Twig logic and content modeling is essential.

In this guide, you will learn how to query entries for navigation, how to handle the unique challenge of 'Single' sections, and how to implement a professional-grade menu system that gives content editors full control without sacrificing performance.

The Basic Entry Query Approach

When you are first starting with Craft CMS, the most straightforward way to build a navigation menu is to query entries from a specific section. This is ideal for simple sites where the navigation structure directly mirrors your content structure.

For example, if you want to pull all entries from a section and order them by their ID, you might use the following syntax:

{% set navi = craft.entries.section('not projekteintrag').all() %}
<ul>
{% for entry in navi %}
  <li><a href="{{ entry.url }}">{{ entry.navigationTitle ?? entry.title }}</a></li>
{% endfor %}
</ul>

Optimization Tip: Variable Storage

One common question is whether to define the query as a variable or loop through it directly. While both work, saving your query to a variable like {% set navi = ... %} is significantly more efficient if you plan to use that same set of data multiple times on the page. Looping directly through craft.entries multiple times triggers separate database calls for each loop, which can slow down your site as it scales.

Handling "Singles" in Your Navigation

One hurdle many developers face is including "Single" sections (like a Homepage, About, or Contact page) in a dynamic list. Unlike Channels or Structures, Singles do not belong to a single parent section that can be queried in bulk easily.

To trigger a list of Single pages dynamically, you need to first identify which sections are defined as Singles and then query their entries. Here is how you can achieve that:

{# 1. Get all sections #}
{% set allSections = craft.app.sections.allSections %}
{% set singleSections = [] %}

{# 2. Filter for sections of type 'single' #}
{% for section in allSections %}
    {% if section.type == 'single' %}
        {% set singleSections = singleSections|merge([section]) %}
    {% endif %}
{% endfor %}

{# 3. Query entries belonging to those sections #}
{% set navi = craft.entries.section(singleSections).all() %}

<ul>
{% for entry in navi %}
    <li><a href="{{ entry.url }}">{{ entry.title }}</a></li>
{% endfor %}
</ul>

The Professional Choice: Dedicated Menu Structures

While querying sections directly is easy, it often leads to a "Content Utopia" problem where your site's navigation is too strictly tied to its content architecture. A more robust, client-friendly approach is to create a dedicated Structure Section specifically for your menu.

Step 1: Content Modeling

Create a new Structure section named "Menu." Inside this section, add two custom fields: 1. Related Entry (Entries Field): Allows the user to select an existing page. 2. Custom URL (Plain Text): Allows the user to input an external link (e.g., https://google.com).

Step 2: The Twig nav Tag

Craft CMS provides a powerful {% nav %} tag specifically designed for hierarchical data like Structures. It handles nested lists and "active" states with much less code than a standard for loop.

{% set menu = craft.entries.section('menu').all() %}

<ul> 
{% nav link in menu %}
    <li>
        {# Check if a related entry exists, otherwise use the custom URL #}
        {% if link.relatedEntry|length %}
            <a href="{{ link.relatedEntry[0].url }}">{{ link.title }}</a>
        {% else %}
            <a href="{{ link.customURL }}">{{ link.title }}</a>
        {% endif %}

        {# Handle nested children automatically #}
        {% ifchildren %}
            <ul>
                {% children %}
            </ul>
        {% endifchildren %}
    </li> 
{% endnav %}
</ul>

This method is superior because it allows your clients to drag and drop menu items to reorder them or create sub-menus without changing the actual URL structure of the pages themselves.

Alternative Approaches for Different Needs

Depending on the complexity and performance requirements of your project, you might consider these other strategies:

1. The Hardcoded Array

For very simple sites or footers that rarely change, hardcoding an array in your base layout is the most performant option. It requires zero database queries.

{% set nav = [
    {'title': 'Home', 'link': siteUrl, 'class': 'home'},
    {'title': 'About', 'link': url('about'), 'class': 'about'},
    {'title': 'Contact', 'link': url('contact'), 'class': 'contact'},
] %}

<nav>
    <ul>
    {% for item in nav %}
        <li class="{{ item.class }}"><a href="{{ item.link }}">{{ item.title }}</a></li>
    {% endfor %}
    </ul>
</nav>

2. Global Matrix Fields

If you want a flat list of links with labels and icons, using a Matrix field within a Global Set is a great middle-ground. It provides a clean UI for clients while keeping the data centralized in the "Globals" tab of the Control Panel.

3. The Hybrid Method

Many professional sites use a hybrid approach. For example, the primary navigation might be a dedicated Structure, while the "Featured Products" dropdown is dynamically pulled from a specific Category group.

Performance Best Practices

Navigation appears on every single page load, so it is a prime candidate for optimization.

  • Watch your Query Count: Using an Entries field inside a loop can lead to "N+1" query problems. If your menu is large, consider using Eager Loading to fetch all related entries in a single query.
  • Use the {% cache %} tag: Wrap your navigation in a cache block to save the rendered HTML. Just remember to use a cache key that accounts for different site versions if you are running a multi-site setup.
  • Avoid Plugins when possible: While there are navigation plugins available, Craft’s native tools are usually more than sufficient and keep your site's footprint light.

Frequently Asked Questions

How do I add an 'active' class to the current page?

You can compare the current request's URI with the entry's URI. For example: <li class="{{ craft.app.request.pathInfo == entry.uri ? 'active' : '' }}">.

Yes. The best way is to use the "Dedicated Menu Structure" method described above, providing both an Entries field and a Plain Text field for custom URLs, then using an if statement to decide which to output.

How do I limit the depth of my navigation?

In your entry query, you can use the .level() parameter. For example, craft.entries.section('menu').level(1).all() will only return the top-level items.

Wrapping Up

Building navigation in Craft CMS is about finding the right balance between client flexibility and developer control. For simple projects, a basic entry query or a hardcoded array works wonders. For larger, more dynamic sites, a dedicated Structure section paired with the {% nav %} tag is the gold standard.

By decoupling your navigation from your content hierarchy, you provide a more intuitive experience for your editors and a more maintainable codebase for yourself. Always keep an eye on your query counts, and don't be afraid to use Globals for those items that don't quite fit into a standard section structure.