How to Create and Manage Menus in Wagtail

Table of Contents

Wagtail Tutorial Series:

To learn more about Wagtail CMS, please check Build Blog With Wagtail CMS (4.0.0)

  1. Create Wagtail Project
  2. Modern Frontend Techs for Wagtail
  3. Dockerizing Wagtail App
  4. Add Blog Models to Wagtail
  5. How to write Wagtail page template
  6. Create Stylish Wagtail Pages with Tailwind CSS
  7. How to use StreamField in Wagtail
  8. Wagtail Routable Page
  9. Add Pagination Component to Wagtail
  10. Customize Wagtail Page URL
  11. Add Full Text Search to Wagtail
  12. Add Markdown Support to Wagtail
  13. Add LaTeX Support & Code Highlight In Wagtail
  14. How to Build Form Page in Wagtail
  15. How to Create and Manage Menus in Wagtail
  16. Wagtail SEO Guide
  17. Online Demo http://wagtail-blog.accordbox.com/
  18. Source code: https://github.com/AccordBox/wagtail-tailwind-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 
  5. Add Bootstrap Theme to Wagtail

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 field.
  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 wagtail_app.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>
  {% 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 source code

from treebeard.mp_tree import MP_Node

class AbstractPage(
    LockableMixin,
    PreviewableMixin,
    DraftStateMixin,
    RevisionMixin,
    TranslatableMixin,
    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

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 top Sort menu order 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 wagtail_app.blog.models import BlogPage
>>> blog_page = BlogPage.objects.first()
>>> [(page.title, page.path) for page in blog_page.get_children()]
[('PostPage1', '000100020001'), ('MarkDown Example', '000100020002'), ('PostPage3', '000100020003'), ('Contact', '000100020004')]

After I DRAG 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'), ('MarkDown Example', '000100020003'), ('PostPage3', '000100020004')]

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

wagtailmenus==3.1.3

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

INSTALLED_APPS = [
    'wagtail.contrib.modeladmin',  # new

    "wagtailmenus",                # new

    # code omitted for brevity
]

Please make sure wagtail.contrib.modeladmin is also included in INSTALLED_APPS

Add wagtailmenus.context_processors.wagtailmenus to the TEMPLATES in wagtail_app/settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['wagtail_app/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',               # new
            ],
        },
    },
]
# 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_app/templates/blog/components/navbar.html

{% load menu_tags %}

<nav class="bg-white border-b border-opacity-75 border-gray-300 dark:bg-gray-900 dark:text-white">
  <div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
    <div class="relative flex items-center justify-between h-16">

      <div class="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
        <div class="flex-shrink-0 flex items-center">
          <a href="/"><span class="text-bold text-grey-800">Wagtail Blog Demo</span></a>
        </div>
        <div class="hidden sm:block sm:ml-6">
          <div class="flex space-x-4">

            {% main_menu template="menu/main_desktop_menu.html" %}

          </div>
        </div>
      </div>

    </div>
  </div>

</nav>

Notes:

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

Create wagtail_app/templates/menu/main_desktop_menu.html

{% for item in menu_items %}
  <a href="{{ item.href }}" class="text-gray-500 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium" >
    {{ item.text }}
  </a>
{% endfor %}

Now Contact page display on the top navbar.

Notes

  1. We will update the navbar to make it work on the mobile device in later chapter.
  2. wagtailmenu is very powerful and flexible, If you want to make wagtailmenu to generate nested menu, you can take a look at this

Migration

If you have CI job that check Django migration, it might fail

/usr/local/lib/python3.10/site-packages/wagtailmenus/migration/0024_alter_flatmenu_id_alter_flatmenuitem_id_and_more.py
    - Alter field id on flatmenu
    - Alter field id on flatmenuitem
    - Alter field id on mainmenu
    - Alter field id on mainmenuitem

Before https://github.com/jazzband/wagtailmenus/issues/435 is resolved, below are some solutions

  1. One solution is to set DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' in django settings
  2. The other solution is to write a custom command which can ignore some 3-party Django app https://forum.djangoproject.com/t/how-to-fix-ignore-pending-migration-in-3rd-party-app/11408/4

Wagtail Tutorial Series:

To learn more about Wagtail CMS, please check Build Blog With Wagtail CMS (4.0.0)

  1. Create Wagtail Project
  2. Modern Frontend Techs for Wagtail
  3. Dockerizing Wagtail App
  4. Add Blog Models to Wagtail
  5. How to write Wagtail page template
  6. Create Stylish Wagtail Pages with Tailwind CSS
  7. How to use StreamField in Wagtail
  8. Wagtail Routable Page
  9. Add Pagination Component to Wagtail
  10. Customize Wagtail Page URL
  11. Add Full Text Search to Wagtail
  12. Add Markdown Support to Wagtail
  13. Add LaTeX Support & Code Highlight In Wagtail
  14. How to Build Form Page in Wagtail
  15. How to Create and Manage Menus in Wagtail
  16. Wagtail SEO Guide
  17. Online Demo http://wagtail-blog.accordbox.com/
  18. Source code: https://github.com/AccordBox/wagtail-tailwind-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 
  5. Add Bootstrap Theme to Wagtail

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.

Django SaaS Template

It aims to save your time and money building your product

Learn More

This book will teach you how to build a SPA (single-page application) with React and Wagtail CMS

Read More
© 2018 - 2024 AccordBox