Learn Wagtail Course

How to Create a Custom Wagtail Menu System

Every good site has a navigation, and it's usually found in the header.

Wagtail already comes with an option to show pages in your menus, and all you have to do is query pages using PageName.objects.live().in_menu().

But that doesn't give you control over the placement of each page. So let's say you wanted a contact button to the very right of your menu, that's gets harder to do. You can drag and drop pages to have the proper order, and that would solve the ordering problem.

But what if you want to link to:

  • A certain part of a page, or
  • An external URL

Well then you're kind of stuck if you want either of those options. Also creating a mega menu would be pretty difficult.

Luckily Wagtail gives us all the ingredients we need to make our own custom menu system using:

  • 1 Clusterable Model
  • 1 Orderable Model
  • 1 Snippet
  • 1 Template Tag
  • and 1 loop in our template

Below you'll find the code that was used in the video.

Note: there are different files, and they all belong to an app called "menus".

The Code

"""menus/templatetags/menus_tags.py"""
from django import template

from ..models import Menu

register = template.Library()


@register.simple_tag()
def get_menu(slug):
    return Menu.objects.get(slug=slug)
"""menus/models.py"""
from django.db import models

from django_extensions.db.fields import AutoSlugField
from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel
from wagtail.admin.edit_handlers import (
    MultiFieldPanel,
    InlinePanel,
    FieldPanel,
    PageChooserPanel,
)
from wagtail.core.models import Orderable
from wagtail.snippets.models import register_snippet


class MenuItem(Orderable):

    link_title = models.CharField(
        blank=True,
        null=True,
        max_length=50
    )
    link_url = models.CharField(
        max_length=500,
        blank=True
    )
    link_page = models.ForeignKey(
        "wagtailcore.Page",
        null=True,
        blank=True,
        related_name="+",
        on_delete=models.CASCADE,
    )
    open_in_new_tab = models.BooleanField(default=False, blank=True)

    page = ParentalKey("Menu", related_name="menu_items")

    panels = [
        FieldPanel("link_title"),
        FieldPanel("link_url"),
        PageChooserPanel("link_page"),
        FieldPanel("open_in_new_tab"),
    ]

    @property
    def link(self):
        if self.link_page:
            return self.link_page.url
        elif self.link_url:
            return self.link_url
        return '#'

    @property
    def title(self):
        if self.link_page and not self.link_title:
            return self.link_page.title
        elif self.link_title:
            return self.link_title
        return 'Missing Title'


@register_snippet
class Menu(ClusterableModel):
    """The main menu clusterable model."""

    title = models.CharField(max_length=100)
    slug = AutoSlugField(populate_from="title", editable=True)
    # slug = models.SlugField()

    panels = [
        MultiFieldPanel([
            FieldPanel("title"),
            FieldPanel("slug"),
        ], heading="Menu"),
        InlinePanel("menu_items", label="Menu Item")
    ]

    def __str__(self):
        return self.title
{% load menus_tags %}
{# Your template; perhaps base.html #}

{% get_menu "main" as navigation %}

{% for item in navigation.menu_items.all %}
    <a href="{{ item.link }}" {% if item.open_in_new_tab %} target="_blank"{% endif %}>{{ item.title }}</a>
{% endfor %}

The Git Commit

Want to see the entire Git Commit? No problem! Here's the link to the entire commit: https://github.com/CodingForEverybody/learn-wagtail/commit/32164bac7eae264a73fac06a3b5fc0e4eeb04599

Wagtailmenus Repo

Near the end of this video I briefly talk about a package called wagtailmenus that basically does all of this for you (and a little more). Here are the link:

Was this helpful to you?

Sharing is caring. Help the community by sharing this article.