How to Create and Manage Menus in Wagtail

Michael Yin

Last updated on December 10 2021

Table of Contents

Wagtail Tutorial Series:

  1. Create Wagtail Project
  2. Dockerizing Wagtail App
  3. Add Blog Models to Wagtail
  4. How to write Wagtail page template
  5. Add Bootstrap Theme to Wagtail
  6. How to use StreamField in Wagtail
  7. Wagtail Routable Page
  8. Add pagination component to Wagtail
  9. Customize Wagtail Page URL
  10. Add Full Text Search to Wagtail
  11. Add Markdown Support to Wagtail
  12. Add LaTeX Support & Code Highlight In Wagtail
  13. How to Build Form Page in Wagtail
  14. How to Create and Manage Menus in Wagtail
  15. Wagtail SEO Guide
  16. Source code: https://github.com/AccordBox/wagtail-bootstrap-blog

Wagtail Tips:

  1. Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
  2. Wagtail Tip #2: How to Export & Restore Wagtail Site

Write style in Wagtail:

  1. How to use SCSS/SASS in your Django project (Python Way)
  2. How to use SCSS/SASS in your Django project (NPM Way)

Other Wagtail Topics:

  1. How to make Wagtail project have good coding style
  2. How to do A/B Testing in Wagtail CMS 
  3. How to build a landing page using Wagtail CMS 
  4. How to support multi-language in Wagtail CMS 

More Wagtail articles and eBooks written by me

Objective

By the end of this chapter, you should be able to:

  1. Understand how to build basic menus with show_in_menus
  2. Learn what is page path and how page orders work.
  3. Build menus with wagtailmenus package.

Show in Menu

$ docker-compose up -d
$ docker-compose logs -f

Please go to Wagtail admin, edit the contact page we just created.

In the promote tab, you will see Show in menus field, click it and then publish the page.

Next, let's check data in the Django shell

$ docker-compose run --rm web python manage.py shell
>>> from blog.models import BlogPage
>>> blog_page = BlogPage.objects.first()
>>> blog_page.get_children().live().in_menu()
<PageQuerySet [<Page: Contact>]>
>>> exit()

As you can see, if we set show_in_menus=True in Wagtail admin, we can get the page using in_menu.

So we can display the page in the navbar like this.

<ul class="navbar-nav">
  {% for menu_page in blog_page.get_children.live.in_menu %}
    <li>
      <a href="{{ menu_page.url }}" class="nav-link">{{ menu_page.title }}</a>
    </li>
  {% endfor %}
</ul>

Page path

Some people might ask, what if I want the nested menu.

Let's first check this part of the Wagtail core/models.py

from treebeard.mp_tree import MP_Node

class AbstractPage(TranslatableMixin, TreebeardPathFixMixin, MP_Node):
    """
    Abstract superclass for Page. According to Django's inheritance rules, managers set on
    abstract models are inherited by subclasses, but managers set on concrete models that are extended
    via multi-table inheritance are not. We therefore need to attach PageManager to an abstract
    superclass to ensure that it is retained by subclasses of Page.
    """
    objects = PageManager()

    class Meta:
        abstract = True

Notes:

  1. Here we see the Wagtail page inherit from MP_Node of treebeard.mp_tree (django-treebeard is a library that implements efficient tree implementations for the Django)

Let's run some code in Django shell to help us better understand the tree structures.

$ docker-compose run --rm web python manage.py shell
>>> from wagtail.core.models import Page
>>> root_page = Page.objects.get(pk=1)
>>> root_page.depth
1
>>> root_page.path
'0001'

>>> blog_page = root_page.get_children().first()
>>> blog_page.depth
2
>>> blog_page.path
'00010002'

>>> post_page = blog_page.get_children().first()
>>> post_page.depth
3
>>> post_page.path
'000100020001'
>>> exit()

Notes:

  1. depth store the depth of the node in a tree, and the root node has depth 1
  2. path stores the full materialized path for each node, each node would take 4 char. That why you see 0001, 0002
  3. The char in the path can be 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ (length is 36), so one node can contains up to 1679615 (36 ** 4 - 1) child pages by default.

You can also check django-treebeard doc to learn more

Page Order

Please check here Wagtail core/models.py

class BasePageManager(models.Manager):
    def get_queryset(self):
        return self._queryset_class(self.model).order_by('path')

So the PostPage.objects.all() would order the page using the path field by default.

When we check pages in Wagtail admin:

  1. By default, the index page would order the pages using the latest_revision_created_at field. (Recently edited page would be the first)
  2. If we click the SORT button, the page will be ordered with default queryset order (path field), and we can drag the item up or down to change the position in the tree (please note this would change data in the db). (You will see the URL in the browser has querystring ordering=ord)

Next, let's change the page order and then check the data in the Django shell.

Before I change:

$ docker-compose run --rm web python manage.py shell
>>> from blog.models import BlogPage
>>> blog_page = BlogPage.objects.first()
>>> [(page.title, page.path) for page in blog_page.get_children()]
[('PostPage1', '000100020001'), ('PostPage2', '000100020002'), ('PostPage3', '000100020003'), ('PostPage4', '000100020004'), ('Contact', '000100020005')]

After I move Contact page to the first

$ docker-compose run --rm web python manage.py shell
>>> from blog.models import BlogPage
>>> blog_page = BlogPage.objects.first()
>>> [(page.title, page.path) for page in blog_page.get_children()]
[('Contact', '000100020001'), ('PostPage1', '000100020002'), ('PostPage2', '000100020003'), ('PostPage3', '000100020004'), ('PostPage4', '000100020005')]

Notes:

  1. As you can see, the path field in the pages all updated.
  2. The core logic of the path change is done by treebeard node.move method, and you can check more from the doc

Some times, clients care about the page order in the menu, and we can use path field to help us without adding new fields.

Wagtailmenu

Now we have a good understanding of how menu in Wagtail works, so I'd like to give you a better solution for you to build menu in your Wagtail project.

Add wagtailmenus to the requirements.txt

Django>=3.1,<3.2
wagtail>=2.11,<2.12
psycopg2-binary
django-extensions==3.1.0

wagtail-markdown==0.6
Pygments
wagtail-django-recaptcha==1.0
django-crispy-forms==1.10.0
wagtailmenus==3.0.2

Add wagtailmenus and wagtail.contrib.modeladmin to the INSTALLED_APPS in wagtail_bootstrap_blog/settings/base.py

INSTALLED_APPS = [
    'wagtail.contrib.forms',
    'wagtail.contrib.redirects',
    'wagtail.embeds',
    'wagtail.sites',
    'wagtail.users',
    'wagtail.snippets',
    'wagtail.documents',
    'wagtail.images',
    'wagtail.search',
    'wagtail.admin',
    'wagtail.core',
    'wagtail.contrib.routable_page',
    'wagtail.contrib.postgres_search',
    'wagtail.contrib.modeladmin',

    'modelcluster',
    'taggit',
    'django_extensions',
    'wagtailmarkdown',
    "crispy_forms",
    "captcha",
    "wagtailcaptcha",
    "wagtailmenus",

    # code omitted for brevity
]

Add wagtailmenus.context_processors.wagtailmenus to the TEMPLATES in wagtail_bootstrap_blog/settings/base.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(PROJECT_DIR, 'templates'),
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'wagtailmenus.context_processors.wagtailmenus',
            ],
        },
    },
]
# rebuild image and run
$ docker-compose up -d --build
$ docker-compose logs -f

Now please go to /settigns/main menu/ and add the contact page.

Template

Update wagtail_bootstrap_blog/templates/blog/components/navbar.html

{% load menu_tags %}

<nav class="mb-2 navbar navbar-expand-lg navbar-dark bg-dark">
  <div class="container">
    <a class="navbar-brand" href="/">Wagtail Blog Demo</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarResponsive">
      {% main_menu template="menu/main_menu.html" %}
    </div>
  </div>
</nav>

Notes:

  1. In the top, we add {% load menu_tags %}
  2. {% main_menu template="menu/main_menu.html" %} means we render the main menu with the template menu/main_menu.html

Create wagtail_bootstrap_blog/templates/menu/main_menu.html

{% load menu_tags %}

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

Now Contact page display on the top navbar.

Notes

  1. wagtailmenu is very powerful and flexible, If you want to make wagtailmenu to generate nested menu, you can take a look at this

Wagtail Tutorial Series:

  1. Create Wagtail Project
  2. Dockerizing Wagtail App
  3. Add Blog Models to Wagtail
  4. How to write Wagtail page template
  5. Add Bootstrap Theme to Wagtail
  6. How to use StreamField in Wagtail
  7. Wagtail Routable Page
  8. Add pagination component to Wagtail
  9. Customize Wagtail Page URL
  10. Add Full Text Search to Wagtail
  11. Add Markdown Support to Wagtail
  12. Add LaTeX Support & Code Highlight In Wagtail
  13. How to Build Form Page in Wagtail
  14. How to Create and Manage Menus in Wagtail
  15. Wagtail SEO Guide
  16. Source code: https://github.com/AccordBox/wagtail-bootstrap-blog

Wagtail Tips:

  1. Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
  2. Wagtail Tip #2: How to Export & Restore Wagtail Site

Write style in Wagtail:

  1. How to use SCSS/SASS in your Django project (Python Way)
  2. How to use SCSS/SASS in your Django project (NPM Way)

Other Wagtail Topics:

  1. How to make Wagtail project have good coding style
  2. How to do A/B Testing in Wagtail CMS 
  3. How to build a landing page using Wagtail CMS 
  4. How to support multi-language in Wagtail CMS 

More Wagtail articles and eBooks written by me

Launch Products Faster with Django

SaaS Hammer helps you launch products in faster way. It contains all the foundations you need so you can focus on your product.

Michael Yin

Michael is a Full Stack Developer from China who loves writing code, tutorials about Django, and modern frontend tech.

He has published some ebooks on leanpub and tech course on testdriven.io.

He is also the founder of the AccordBox which provides the web development services.

Table of Contents

Django SaaS Template

It aims to save your time and money building your product, developed by Michael Yin

Learn More

Hotwire is the default frontend solution shipped in Rails, this book will teach you how to make it work with Django, you will learn building modern web applications without using much JavaScript.

Read More

Subscribe

Get notified about new great Web Development Tutorial