Wagtail Tutorial Series:
To learn more about Wagtail CMS, please check Build Blog With Wagtail CMS (4.0.0)
- Create Wagtail Project
- Modern Frontend Techs for Wagtail
- Dockerizing Wagtail App
- Add Blog Models to Wagtail
- How to write Wagtail page template
- Create Stylish Wagtail Pages with Tailwind CSS
- How to use StreamField in Wagtail
- Wagtail Routable Page
- Add Pagination Component to Wagtail
- Customize Wagtail Page URL
- Add Full Text Search to Wagtail
- Add Markdown Support to Wagtail
- Add LaTeX Support & Code Highlight In Wagtail
- How to Build Form Page in Wagtail
- How to Create and Manage Menus in Wagtail
- Wagtail SEO Guide
- Source code: https://github.com/AccordBox/wagtail-tailwind-blog
Wagtail Tips:
- Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
- Wagtail Tip #2: How to Export & Restore Wagtail Site
Write style in Wagtail:
- How to use SCSS/SASS in your Django project (Python Way)
- How to use SCSS/SASS in your Django project (NPM Way)
Other Wagtail Topics:
Objective
By the end of this chapter, you should be able to:
- Understand how to build basic menus with
show_in_menusfield. - Learn what is page path and how page orders work.
- Build menus with
wagtailmenuspackage.
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:
- Here we see the Wagtail page inherit from
MP_Nodeoftreebeard.mp_tree(django-treebeardis a library that implementsefficient tree implementationsfor 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:
depthstore the depth of the node in a tree, and theroot nodehas depth1pathstores the full materialized path for each node, each node would take4 char. That why you see0001,0002- The char in the
pathcan be0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ(length is36), so one node can contains up to1679615(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:
- By default, the index page would order the pages using the
latest_revision_created_atfield. (Recently edited page would be the first) - If we click the top
Sort menu orderbutton, the page will be ordered with default queryset order (pathfield), 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 querystringordering=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:
- As you can see, the
pathfield in the pages all updated. - The core logic of the
pathchange is done bytreebeard node.movemethod, 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:
- In the top, we add
{% load menu_tags %} {% main_menu template="menu/main_desktop_menu.html" %}means we render the main menu with the templatemenu/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
- We will update the navbar to make it work on the mobile device in later chapter.
wagtailmenuis very powerful and flexible, If you want to makewagtailmenuto 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
- One solution is to set
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'in django settings - 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)
- Create Wagtail Project
- Modern Frontend Techs for Wagtail
- Dockerizing Wagtail App
- Add Blog Models to Wagtail
- How to write Wagtail page template
- Create Stylish Wagtail Pages with Tailwind CSS
- How to use StreamField in Wagtail
- Wagtail Routable Page
- Add Pagination Component to Wagtail
- Customize Wagtail Page URL
- Add Full Text Search to Wagtail
- Add Markdown Support to Wagtail
- Add LaTeX Support & Code Highlight In Wagtail
- How to Build Form Page in Wagtail
- How to Create and Manage Menus in Wagtail
- Wagtail SEO Guide
- Source code: https://github.com/AccordBox/wagtail-tailwind-blog
Wagtail Tips:
- Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
- Wagtail Tip #2: How to Export & Restore Wagtail Site
Write style in Wagtail:
- How to use SCSS/SASS in your Django project (Python Way)
- How to use SCSS/SASS in your Django project (NPM Way)
Other Wagtail Topics: