Wagtail Documentation
Wagtail Documentation
1. Copyright_2022_Manning_Publications
2. welcome
3. 1_Introducing_Wagtail
4. 2_Setting_up_and_completing_essential_tasks
5. 3_Creating_a_blog
6. 4_Styling_with_Tailwind_CSS
7. 5_Creating_a_Wagtail_store
MEAP Edition Manning Early Access Program Wagtail CMS in Action
With Django and Python Version 4
manning.com
welcome
Thank you for purchasing the MEAP edition of Wagtail CMS in Action! To
get the most from this book you should have a little experience with Object
Oriented Programming and have Python installed on your computer. Don’t
worry if you aren’t an OOP expert because I’ll walk you through everything
you’ll need to know as you progress through this book. If you have any
Django Web Framework experience as well, that’s a plus, but I’m making
the assumption that not everybody has worked with Django in the past, and
I’ll make it as clear as possible to show you how it works, and more
importantly, when Django is being used and when Wagtail is being used, and
how they connect to each other.
When I first started working with Wagtail in mid-2017 there were virtually
no videos or articles that were helpful for developers. As I used Wagtail
more and more in my professional career I had realized that it might be up to
me to help. I then produced over 60 YouTube videos to help alleviate this
problem. Shortly after that I was asked to join the Wagtail core development
team.
Since that time I have helped thousands of developers learn Wagtail, many
who had never heard of Wagtail before, and many who have never written
any Django or Python. If that sounds like you, just know that you’re in good
hands.
Together, you and I will create a simple blog using Wagtail CMS as an
introduction to some of its core features. From there I’ll take you into new
territory by exploring ways to leverage Django and Python inside of your
Wagtail website, along with more advanced ways to customize your Wagtail
website.
At this point I think it’s good to point out that Wagtail does not reinvent
features that it can use directly from Python and Django. This is important to
note because along the way you’ll inevitably learn a little bit of Django, too.
Lastly, I’d love it if you could post any questions, comments or suggestions
you have about this book in the liveBook discussion forum. Your feedback is
what will help make this the best possible book!
Thank you again for purchasing the MEAP edition of Wagtail CMS in
Action, and I’m looking forward to hearing your feedback.
—Kalob Taulien
In this book
On the surface it seems that Wagtail is the same as every other content
management system, but once you dive into the specifics you’ll realize it has
a lot of amazing features that will enable your team to be more productive
and enjoy their jobs, without the ability to accidentally break your website.
That’s why companies like Google, Twilio, The Motley Fool, Mozilla, and
NASA are using Wagtail.
A good CMS should, in all honesty, not do everything. It’s like that old
saying from Aesop’s Fables, “Those who seek to please everybody please
nobody”. And in terms of content management systems, it can be rephrased
like this: If you can do it all, you’re not doing anything well.
This is where Wagtail shines. Although Wagtail has features for everybody,
it doesn’t grant any lone person total system control. Instead, Wagtail wants
you and your team to do what you all do best. Whether that’s writing
(editing), SEO, coding, design, site administration, or anything else - Wagtail
wants you to leverage your skills. The idea is that, with a good CMS, you are
using all the top talent from your team. Or if you’re a single-person team,
then you should be wearing different hats at different times and not trying to
do it all at the exact same time.
Throughout this book I assume that you work with a team where each person
has a specific role or a small handful of roles, but if you don’t work in a
team then let’s assume you’ll be performing every role at different times. For
instance, you might be writing a blog post but then decide you need to make
an enlarged quote area to quote your favorite author. Then you do some
coding (wearing the coders’ hat), deploy your changes (DevOps hat), and
continue writing your blog post (editing hat).
With Wagtail you get to focus on each role one at a time. If you’re editing
content or writing some new content, you can simply focus on writing
interesting content. There’s no need to be thinking about coding or design.
You’re most likely a web developer if you’re reading this book, so you can
focus purely on coding and giving your editors the best experience possible
while keeping your designers happy because the site can’t easily be broken
out of its pre-formatted design.
Why is this such a powerful concept to adopt? Well, the short answer is
context switching, which is when you change tasks and all of a sudden
you’re wasting time trying to remember where you left off and spending
more time getting back into “the zone”. It’s hard to quantify but it absolutely
does exist. Think of it like this: you sleep for at least 8 hours, hopefully, and
then immediately wake up and go for a run. And when you’re done running,
you immediately go back to bed and sleep some more. You’ll be far too
tired, or too awake, to switch context, so you’ll end up running slower, or
lying in bed doing nothing. That’s context switching in an extreme example.
And it happens to developers all the time.
This is where Wagtail steps in and says, “Just focus for the moment on what
you do best”. If something else comes up, you need to purposely change
context, making yourself aware of what you need to do, and being able to
focus on one task at a time - which often leads to better decision making and,
in this case, a functional website that looks and acts perfectly.
As a web developer, you likely want to focus on coding and not worry too
much about other tasks such as design, content, or site administration. If that
sounds like you, then good news is headed your way! Wagtail is probably
the content management system for you based on that sentence alone. But
allow me to continue trying to convince you that Wagtail is the right solution
for you or your organization.
People, especially those who work in teams (or wear multiple hats), get
excited about Wagtail websites because they can focus on a single task. You
can channel your inner programmer to write the cleanest code possible
without making your website feel complicated or overengineered while
maintaining all the features and power you need to create a modern website.
Wagtail is built on top of one of the world’s most famous web frameworks,
called Django. And Django is built on top of the world’s most popular
programming language: Python.
One thing to keep in mind is that you need Python on your computer before
starting this book. You will want Python 3.7 or newer but Python 3.8 or
Python 3.9 might serve you even better over the next couple of years. You
can download and install Python from python.org for free. I also think it
would be beneficial to visualize how Python, Django and Wagtail all fit
together.
Figure 1.1 Shows how Python, Django and Wagtail fit together. These are all large ecosystems of
code, and it is helpful to see how Wagtail fits within Django’s ecosystem, and how Django fits
into the Python ecosystem. This is not indicative of accurate market sizes.
Note
You don’t need to know anything about using Django or Wagtail to make use
of this book. But you will need to know some basic Python, including object
oriented programming (OOP).
Next we need to answer a hard question: how is Wagtail different from other
content management systems?
Tip
Wagtail websites seamlessly scale with you, your needs, and your
organization’s needs. There’s never any need to migrate away from Wagtail
because your organization has different needs.
Beyond all of that, Wagtail comes with opt-in features that allow you to
control your website in an easier way. Features like image cropping and
focal points in photos make your images stand out better; storing documents
and images in digital folders called collections lets you organize all of your
non-written content easily; advanced workflows let you set up a process for
your team to moderate, comment and adjust content before publishing,
enabling a headless (websites that only serve an API) CMS for apps and
modern website architectures to use that data from the API, which can be
done with just a few lines of code; and you can host multiple websites from
a single code base. These are just some of the features that Wagtail offers.
And of course it comes with everything else you would expect such as page
previews, moderation, saving drafts, URL hierarchy, richtext sections and
the beloved feature StreamFields which lets you rearrange content in any
manner you want. And if you ever need to extend the functionality that
Wagtail offers, you don’t usually have to overwrite any of the source code
because Python is built to extend from pre-written code.
What I personally love about Wagtail is that it’s open source. If you have an
idea that doesn’t exist but you feel like it should exist, you can help the
community create it (or contract companies like Torchbox to create it for
you).
I would be lying if I told you that Wagtail is for everybody. It’s not. I don’t
believe that any single program out there is a perfect fit for every
organization. But I do believe Wagtail is a good fit for most organizations. In
an effort to not lead you astray I’d like to quickly talk about who Wagtail is
not for.
If you’re still with me at this point, let’s talk about what you will learn in this
book, and how you will learn it.
Together you and I will set up a new Wagtail website from scratch, create a
simple blog that will introduce you to the Wagtail way of coding, and then
create a functional, real-life e-commerce store. Lastly, you will deploy this
website to the web on our own server. The goal is to use a real life example
of when you would create a Wagtail website, though I don’t assume that you
know anything about Wagtail - so we’ll start from scratch. Our website will
touch the most common features of Wagtail and teach you some Django
along the way. I’ll start each chapter with an example and then integrate the
concept of the example into a working codebase. I will share the source code
on GitHub for each chapter as well, so don’t feel like you need to write all
the code by hand.
What is not covered in this book is the frontend work. Wagtail, by its very
nature, is unopinionated about which frontend technologies you should use.
Whether you like Bootstrap, Tailwind CSS, React.js, Vue.js, or writing
vanilla CSS and JavaScript, Wagtail doesn’t care. Wagtails job is to provide
the editing interface for your team, but it’s your team’s job to make the
website look the way you want. So our website may seem boring because,
like Wagtail, I’m not forcing an opinionated frontend decision on you or
your team. Likewise, you will not be creating a headless website in Wagtail
CMS in Action. That’s a broad topic that could be a standalone book.
Now, I’d like to talk about whether this book is for you. You and I will write
a fair amount of code and spend quite a bit of time together, so I want to
make sure Wagtail CMS in Action is for you. I am making a few assumptions
throughout this book, like the assumption that you know simple Python
OOP. You don’t need to know about databases or web requests, and anything
outside of standard Python I will do my best to explain to you along the way
(like deploying a website). If you are a developer who knows some Python,
this book is absolutely for you. Or if you’re a manager trying to decide if
Wagtail is for you and your organization, this book will help you understand
whether Wagtail is a good fit. However, if you’re not code-inclined
whatsoever, or you’d rather create a website in an afternoon by clicking
some boxes, then Wagtail wont’ work well for you either (at least not at this
very moment).
When a user types your website link in their browser it produces a web
request. This request is processed by Django, which uses Python on a deeper
level to manage the request. The web request is then passed on to Wagtail for
further processing with a template response showing your website design
and content.
Figure 1.2. Shows you how web requests typically work when a user hits your Wagtail website.
When a user types in your website URL, the browser constructs an HTTP
request and sends it to the server where Python, Django and Wagtail are
waiting. Python and Django accept the web request, and parse the data,
format it nicely for developers to work with, then Django passes the final
request to Wagtail before showing you the content.
To understand how Django and Wagtail work together, you must first look at
how a simple web request works. Keep in mind that the flow chart is very
basic and oversimplified. But here’s how it typically works: An internet user
will type in your web address to interact with your website. Your website
will be running Python and Django. Django, along with other technologies
like Nginx, will understand the web request you’re making and take action.
With a standard Django website this would then return your request with an
HTTP response, along with the code your browser needs in order to render
and draw your website. But with Wagtail added, if Django can’t complete
the request, it will fall back to Wagtail where Wagtail will look for a
matching page in the URL structure, such as /blog/, look up the page it needs
to serve, perform any additional logic you’ve written, and then construct the
HTTP response along with the code it needs.
At any time, you can create a Django view to render the HTTP response
before it hits Wagtail. Let’s say you have a checkout page that doesn’t need
to be controlled by your editing team. You can write it in plain Django and
the checkout page won’t be visible in Wagtail. In some cases, you want this
to happen, especially if you’re writing custom API endpoints and don’t want
specific content to be editable or maintained whatsoever by your editing
team. This creates a healthy separation between your editorial team and your
development team. Sometimes you want to break this rule, however,
depending on the tech-savviness of your editing team.
Note
Don’t think developers need to hide things from editors all the time. In fact,
if the editing/content team is well trained you can give them more control.
Each organization is different, so use your instincts to determine if giving up
control is the right move for the people who will be using the website every
day.
Figure 1.3 provides a high-level overview of what happens after a user’s web request reaches
Wagtail, and what it does behind the scenes. There’s more to it than this, but this is a great
starting point to understand what exactly Wagtail is trying to do for you and your users.
This is a more detailed view of what’s going on under the hood of a web
request being sent to your future Wagtail website. Once the request is sent to
Wagtail, it will look for the correct page. If it can’t find the page, it returns a
404 Missing Page error and, typically, renders a missing page template. If
the page is found, Wagtail will execute more logic, then execute your custom
logic, if there is any, and render the final template. Templates can also hold
logic. Wagtail, alongside Django, will try to execute template-based logic
before rendering the final HTML output for your browser to show you.
Now you can see that the process is becoming a bit more complex, and you
can see what Wagtail is trying to do.
Once the request reaches Wagtail, it will dissect the URL and look for the
proper page. For example, if you go to YourWebsite.com/blog/hello-world/ it
will parse /blog/hello-world/ and automatically recognize that /blog/ is a
Wagtail page and that /hello-world/ is a child page.
Tip
Wagtail Pages use a parent/child relationship. Once you get started with
Wagtail you can nest child pages under parent pages and the URL structure
is automatically created for you. These are based on slugs, which are the
words in your URL structure. /blog/hello-world/ turns into 1. `blog` and 2.
`hello-world`. Each represents a different unique page in Wagtail.
In this example Wagtail will look up the final page with the slug `hello-
world`. If a page with that slug does not exist it will return a 404 Missing
Page, or an error if there is no 404.html template file. But if the page does
exist, it will read the Wagtail code and then read any additional code you
give it before using Django’s template rendering system to read the code in
your .html file. If there is any extra logic in your template, Django will
render that as well and return a final pure-HTML response for your server to
send to the user.
Note
If you are new to Django it’s good to know that Django comes with a
templating engine so that you can insert variables, loops, and other forms of
logic into the template. Django reads that logic and converts it into pure-
HTML before the users’ HTTP request is finished. Wagtail uses Django for
this because Django has incredibly powerful features that don’t need to be
reinvented.
At the end of the day, most of the code you write will interact with Django
somehow. You might not see it, and it might not be immediately obvious to
us, but that’s how frameworks tend to work. And with Wagtail, specifically,
there’s a lot of back-and-forth between Django and Wagtail. If this concerns
you, it shouldn’t. Django is a very powerful and mature framework used by
Instagram, Pinterest, Dropbox and tons of other huge organizations. It’s
reliable, well built, open source, and tested over time. Because of its
maturity, not everything needs to be reinvented. Like the templating system -
it’s an advanced feature that is hard to re-create and that works perfectly
fine, so there’s no need for Wagtail to use its own. Instead, it can leverage
what Django offers and layer additional features on top.
You and I will jump into code soon enough and you’ll get a better
understanding of how Wagtail pages work from the inside. I also like to talk
about the core pillars of Wagtail. Pages are what we’ve been looking at -
that’s absolutely vital in a content management system. But snippets,
images/documents, reports and site settings also help a Wagtail page become
its best self, so to speak. For instance, if a page has a user uploaded image
Wagtail will need to know the relationship between that image and the page
so the templating engine can understand what is going on. If that was a
mind-melting statement - don’t give up yet! It’s my job to make this concept
as easy to understand as humanly possible - and I’m going to do just that
throughout the rest of this book.
Before we get started, I find it’s also helpful to understand the relationship
between Django and Wagtail. As I mentioned earlier, this won’t make you a
better developer, nor will it show up on any quizzes or tests down the road,
but it will help your brain understand what is happening between Wagtail
and Django – mostly because Wagtail leverages a lot of Django features, so
it can be, at times, confusing. I like to teach this concept primarily because,
if you ever get stuck, you’ll know to look at either the Django
documentation or the Wagtail documentation, and you’ll know in which
community to ask for help.
Figure 1.4. Shows the intricate interaction between Django and Wagtail. Because Wagtail cannot
exist without Django, the lines between their activities can be blurry.
The two sided figure demonstrates how Wagtail and Django cooperate with
each other, and which system is responsible for particular tasks. It will look
big and scary at first, but as you progress through this book it will make
more sense.
The left side of the figure shows what Wagtail is doing, or trying to do. On
the right, it’s Django. Let’s start at the top and work our way down this
figure.
At the very top, the user is trying to access your website. Your website lives
on a server that has no opinions about Wagtail or Django and that’s why it
sits outside of the two large boxes. But as the request is being processed, it’s
then passed onto Django, which then passes the request to Wagtail. Wagtail
looks up the page by its slug and hopefully finds a published page, executes
core Wagtail logic, looks for your custom logic (if any), and finalizes the
backend (server) request by moving onto template rendering. Django’s
template rendering engine will kick in, looking for Wagtail or Django
functions (called template tags) and create a pure-HTML response. That
response is ultimately what your browser will see as it tries to render the
code into a beautiful modern website.
That’s a lot to take in. But, once again, this isn’t knowledge that helps you
become the best developer in the world – it just helps when you’re seeking
support after you finish this book.
You’ll be working with both sides of that flow chart, but you’ll be mostly
sticking with the left side - the Wagtail side. It may help you understand
which parts are Wagtail and why developers do certain things differently
from Django, and which parts are Django and why developers do things
differently from Wagtail.
With these diagrams in mind, let’s move into Chapter 2, where you and I get
started installing our first Wagtail website.
2 Setting up and completing essential
tasks
This chapter covers
Before you jump right into coding your new Wagtail website, you need to
install Wagtail on your computer. As previously mentioned you’ll need
Python 3.7 or newer on your computer. In this book I’ll be using Python
3.10. I won’t be going through Python installation in this book as I’m
assuming you have some familiarity with Python already. However, if you
don’t have a modern version of Python setup on your computer yet, please
do so now before continuing to read this book.
All the code written in this chapter can be found on GitHub in the Chapter 2
pull request located here: https://github.com/KalobTaulien/wagtail-cms-in-
action/pull/1/files.
Figure 2.1. Shows the difference between Wagtail and Django in a new website request.
For the majority of this book, and especially now, you’ll be working with the
left side of the diagram. This is where Wagtail lives. I’m starting off at the
very beginning of a Wagtail project and throughout this book I’ll guide you
through various parts of Wagtail, including some features that aren’t shown
in the diagram.
First I’ll show you how to create a layer of abstraction in Python, and why
it’s important to use these layers. Then you will properly create a new
Wagtail project on your computer for local development. After that you and I
will take a quick break from coding and explore what Wagtail looks like and
where to find some of the core features.
Note
Wagtail comes with its base set of features enabled by default. Or in other
words, it will not give you every feature right away. In order to keep your
website responding quickly for your users most features are disabled or
come in 3rd party plugins known as packages.
Nearing the end of this chapter you will start writing some code. I’ll give
you the code up front that you can use for reference later, and immediately
afterwards I’ll break down everything so it makes sense to you. This will be
a common theme throughout Wagtail CMS in Action where a bulk of code is
presented first followed by breakdowns and explanations. This chapter is
also when the lines between Wagtail and Django become blurry for most
developers so I will do my best now, and throughout this book, to help you
understand what is Wagtail and what is Django code. Often Wagtail and
Django blend together, and it’s not super important that you know exactly
where Wagtail becomes plain Django, so if it doesn’t make sense right away
please don’t let that slow you down.
2.1 Installing Wagtail on your computer
The first thing you need to do, even before installing Wagtail, is to create a
layer of abstraction. These layers are often referred to as virtual
environments in the Python community. The idea behind a virtual
environment is to separate your projects. Let’s say you have this book as a
project, but then create a second project. If you decided to use Wagtail 3 in
one project, and Wagtail 2.16 in another project, you won’t have to uninstall
and re-install Wagtail every time you change projects. You can simply
change your virtual environment and your Python package versions will stay
intact for each project.
Note
Tip
First, you’ll want to create a new folder where your website will live on your
computer. Using different folders for different projects and virtual
environments will help keep you organized as a developer. This goes for all
operating systems, regardless of which one you use. You’ll also need access
to your command line, whether that’s through a dedicated program or
through your code editor, it should all work for you the same.
Windows
>cd your-new-folder/
>python -m venv .venv
>.\.venv\Scripts\activate
Open your command line and cd (change directory) to your new folder.
There should be nothing in this folder yet.
This command will create your virtual environment. I’m opting to use
Python’s built in virtual environment, but as mentioned before you can use a
different tool for splitting up your projects in a new layer of abstraction.
Once you’ve entered this command you will have a hidden folder called
.venv/. If you don’t want to hide the folder, feel free to replace .venv/ with
venv/ and it will create a visible folder for you.
This command doesn’t create anything, but instead it will activate your
virtual environment on Windows. Every time you want to access a Python-
based project on your computer you’ll want to activate the right virtual
environment.
MacOS/Linux
>cd your-new-folder/
>python3 -m venv .venv
>source .venv/bin/activate
For Unix and Unix-like systems such as MacOS and any flavor of Linux you
only need to write one different command. The only difference between
Windows and Unix-like systems is how you enter your virtual environment.
Like Windows, you’ll create a new folder, create a new virtual environment
(venv for short), and then activate it by sourcing the activate file.
When you’re done with your virtual environment, type deactivate to exit.
To re-enter the virtual environment you just need to run the last line from the
snippets above. No need to re-create a virtual environment.
For all future work throughout this book I am assuming you have activated
your virtual environment. It’s important that you do this so you don’t install
Python packages on your entire system (global package installation) and will
help prevent confusion between your future projects even if you aren’t using
Wagtail.
If you’re using Git, you may want to add an entry to your .gitignore file so
you don’t accidentally commit the entire folder. Your .venv/ folder can get
pretty big, and won’t be needed, so it’ll just slow you down. To do this,
create a .gitignore file (with the period at the front of the filename) in your
main project folder, and add .venv/ or venv/ on a single line. This will tell
Git to ignore the entire folder. When a coworker clones your repository, they
can install Wagtail from the requirements.txt file.
With your virtual environment activated you can now install Wagtail using
Python’s package manager called pip.
Note
If you’re an experienced Python developer and don’t want to use pip you
can swap this out for another tool. pip is the easiest to work with and I
prefer to use the most simple solutions until my websites outgrow it - such as
cases where Redis or ElasticSearch are needed, then I prefer Docker. Don’t
feel like you need to know any of that right now.
I want you to download Wagtail now. In this book you’ll be using Wagtail 3.
If you get an error, make sure you're running Python 3. pip also has different
versions and will use different versions of Python. For pip it also needs to
use Python 3, and you can check it with pip -V or pip3 -V. If python -V or
pip -V gives you a Python version less than 3.7 (3.6 or older) then use the
python3 and pip3 commands instead.
Now that Wagtail was installed in your virtual environment you can create a
pre-formatted project with a global Wagtail command.
These amazing commands will set up your files and file structures for you
and install everything Wagtail needs, including Django and various other
Python packages it relies on. It will download everything it needs into your
.venv/ folder and create a series of files and folders for your Wagtail website.
Note
If you’re using Windows and run into a path length limitation problem,
where the file path is too long, try installing Wagtail on a shorter path such
as C:\Websites\YourWebsiteHere\
If you open your main project folder, you should have all the files you need
to start your website. Lastly, you need to run a few Django-based commands
to run our website.
Note
If you’re on Windows and python3 doesn’t work for you, but py does work
for you then please read python3 as py
Now that you have Django and Wagtail running on your computer using the
runserver command, open your browser and navigate to
http://127.0.0.1:8000/ - alternatively most developers can use
http://localhost:8000/ as 127.0.0.1 typically maps to localhost on most
computers, but not all. Use the link that works best for you.
Additionally, you can always try out the development version of Wagtail by
going to https://wagtail.org/play/ to load an interactive editor in your
browser that also provides a window where you can log into the admin and
explore a fresh Wagtail website installation. It uses a tool called GitPod. You
will need a free GitHub account to login and get this cool tool to start. Also
note that GitPod is designed to be a temporary coding solution and your
code will disappear after about 30 minutes of inactivity. But it’s a fantastic
tool for exploring the code base, experimenting with the features you’ll learn
about in this book, and for see your changes without needing to install
anything on your computer.
2.2 Editing interface introduction
Let’s dive into the editing interface that Wagtail provides, which I commonly
call the Wagtail admin. Now that you have your website up and running
using the Django management command called runserver you’ll be able to
sign into the admin portion of your website. Your users won’t know about
this section. And for the majority of Wagtail CMS in Action you’ll be
operating inside the admin section of Wagtail that’s often called the editing
interface, or just the Wagtail admin.
Once you log into the Wagtail admin, you’ll see a navigation bar on the left
that has Pages, Images, Documents, Reports, and Settings. There are more
options that we, as developers, don’t see and even more that can be created.
Pages are the core of Wagtail website, and in fact, are the core of most
content management systems. Other features like image management or file
management through the documents menu option are also core features of
most content management systems, but I classify them as secondary core
features.
Next, I want to talk about the Wagtail tree. This is vital information moving
forward as you work more and more with Wagtail Pages.
Every page stems from a page that you can’t edit, called the Root page. Then
you typically have a Home page under the root page, and a setting that tells
Wagtail that the homepage is the default page to serve when someone lands
on your website. Because Wagtail can host multiple websites using the same
code, called multi-site, you can have multiple home pages and multiple
settings that point to different home pages. I’m not going to dive into
multisite in this book, but it helps to understand why the Root page always
exists.
At the very top of figure 2.2 is the Root page. Think of this like a real root of
a real tree. Tree roots exist, but they often aren’t seen. Underneath the home
page can be subpages, also referred to as child pages. This gets into the
notion of parent and child pages. If you have a blog page called blog, and 10
blog posts under the original blog page, you have one parent blog page and
10 child pages. Where it can get confusing is the relativity behind this.
Because if that original blog page is nested under the home page, then our
context changes and the homepage becomes the parent page and the original
blog page becomes the child page. You can extend this beyond parent/child
pages. If you have a home page, with a child blog page, and 10 blog posts,
then you could call those 10 blog posts grandchild pages in relation to the
homepage. I know, at first this is confusing but it will make more sense the
more you use Wagtail, and you’ll never want to go back to the old way.
Figure 2.3 is the Wagtail Page tree as it’s visually shown in the page explorer inside of the
Wagtail Admin interface (from left to right)
In the context of Wagtail and the Wagtail admin, you can picture the tree as a
sideways tree. On the left is the root page, which is almost never seen. Then
there is the homepage which will map to the / (as in
http://yourwebsite.com/, it’s the last slash) URL of your website (and you’ll
make changes to this page in the next section). Then you can have child
pages, which you don’t have yet but, let's say you have a Blog Index Page (a
blog home page) it would be /blog/. And those blog post pages mentioned
earlier would live at /blog/your-blog-post-1/ and /blog/your-blog-
post-2/ and so on.
Tuck this knowledge in your back pocket for now. You’ll get hands-on
experience with new page types and creating child pages in Chapter 3.
2.4 Editing the home page
Now is the time that you can start writing some code and editing your initial
Home Page. After all, you’re reading a coding book, so let’s get to the good
stuff. In this section I’ll define models and fields, create a subtitle, add an
image, a link to another Wagtail page, and then show them in the template.
First, I need to introduce you to two new terms: models and fields. Models
are how you map code to our database using Django’s object relationship
manager (ORM). In Wagtail you call page models Pages because they come
with more than just simple data mapping. Pages come with previews,
revision history, privacy settings, and many more things that a Django model
doesn’t come with - but at the end of the day a Wagtail Page is a really fancy
Django model with loads of extra functionality and logic. And fields are the
individual lines of code you add to a page model, known as a Wagtail Page,
that is then mapped to a specific column in your database table.
Note
Uncertain about how databases work? Have no fear! You won’t need to
know anything about databases throughout this book. But I talk about them
from time to time just in case you are familiar with databases and want to
know how it works with Wagtail and Django.
In the suite of files that Wagtail created for you when you set up the project
initially is a folder called home/ and a file in that folder called models.py.
Here you see yet another reference to Django where, typically, Django
Models go in the models.py file. In the land of Wagtail, our Pages also
typically go into the models.py file. Find that file and open it up.
Here you are adding a new field called subtitle and allowing it to be editable
in the Wagtail admin interface through a Wagtail concept known as panels.
The FieldPanel is how you make a field editable by your editors and will
determine how to handle each of your fields for you behind the scenes.
Note
The idea is that you add a simple character field, called a CharField, and let
your content editors change the value and then display it on the website. But
if you try to run this code as is, you’ll run into an error. That’s because
Wagtail Pages, much like plain Django models, have fields that map to a
table in your database. Think of it like creating a new column in a
spreadsheet and naming that column subtitle.
However, in this case, the database doesn’t know about the subtitle column
in the hypothetical spreadsheet, also known as our database. You need to tell
Wagtail (and Django) to create a new column for it to store this information.
You can try editing your Home Page through the Wagtail admin interface at
this time, but you’ll likely run into a database operations error. To fix this,
stop running your Django server in your command line and type out the
following commands:
Making migrations will create a new file in a folder called migrations/ with
all the instructions that your Wagtail website will need to create a new
column in the database table it’s working on. And running migrate will
execute those instructions. You can safely run these commands more than
once.
Tip
If you are ever stuck while working on new fields, models or Wagtail Pages,
you can always run and re-run these commands. Running these commands
more than once won’t do anything unexpected, and will only create new
code, or execute code, when something new is detected. Feel free to use
those commands more than once in a row to test it out.
Once you have made a new migration file and then executed the instructions
from that file, you can re-run your server with python3 manage.py
runserver and open http://127.0.0.1:8000/admin/ in your browser again. Try
to edit your homepage once again, and that error should have disappeared
allowing you to continue editing your home page. But this time your
homepage has a new field - the subtitle field.
Add some text to your subtitle field in the Wagtail editing interface and
publish the page.
Note
Throughout this book I’ll show you how to lightly style some frontend
components, but I’m mostly leaving that up to you to make your website
look the way you want. In the next section I’ll outline how you can get your
new subtitle text to show up in your home page template.
2.4.2 Templates
Templates files are the default way to create a Wagtail or Django based
website. Wagtail CMS in Action will not outline a headless Wagtail website,
but should you need that functionality for a modern JavaScript-based
website using React.js, Vue.js or any other frontend framework, you can
always reference the Wagtail documentation at https://docs.wagtail.org/.
Jump back into the code, you can always assume that a Wagtail Page will
explicitly map to a template, much like how model fields map to database
columns. This is some of the magic behind Django, which controls a large
portion of these connections. Regarding the home page, your home/ folder
has a subfolder called templates/ and in there you can find a file called
home_page.html. Open that file and read through it quickly. You’ll see that
it’s including another .html file called home/welcome_page.html - that’s the
first page you saw when you initially setup your Wagtail website, and it’s
currently the page you see when you view your homepage, or even preview
your home page while making editorial changes in the Wagtail editing
interface.
Your final home_page.html template can be as bare as this. There is still a lot
going on but I only want to focus on the double curly braces which I like to
call double mustache syntax.
Every Wagtail page template will have access to page and self. These are
identical, but I prefer to use page as it’s more explicit about what’s going on,
and other developers you work with will appreciate that. The page object
holds everything that comes from wagtail.core.models.Page. You can
always view the source code in your venv/ folder, or directly on GitHub. In
this instance the page object is using title, and that comes with every
Wagtail Page. The template also has {{ page.subtitle }} which is the
field you added in the last section.
This is a great demonstration about how to add a simple text field into your
templates after you have added it to the backend.
Note
When I say backend I’m most often referring to Python code including
Wagtail and Django. When I say frontend I’m referring to templates, HTML,
CSS and JavaScript. Most developers will follow this convention as well.
If you want a field to show up in your template the key takeaways are:
A Wagtail image comes with a suite of tools around images, like cropping
for instance. Because of that, images are different in Wagtail than they are in
Django. Open up home/models.py and let’s edit the HomePage class.
This is different from a regular text based field. This is a ForeignKey, which
is a relationship to the Image model in Wagtail. Instead of using a Django
ImageField, Wagtail uses a ForeignKey relationship to its own version of a
Django ImageField. Again, it’s because Wagtail comes loaded with tons of
features that Django doesn’t have.
Tip
If you’re keen on best practices it’s a good idea to create a custom image
model. The Wagtail documentation covers how to do that quite nicely.
Now that the home page has another new field, it will need a migration.
These two commands are very useful and quite popular so you may want to
memorize these as well:
Make sure you’ve selected an image and published your page in the Wagtail
admin before you view your new homepage. Load up http://127.0.0.1:8000/
and you will see your image, cropped to exactly 250 pixels wide by 250
pixels tall. And Wagtail was generous enough to add the HTML image tag
along with it.
If you prefer to handle your images in a more controlled way, you can assign
the output of the {% image %} function into a template variable and access
raw object properties. I personally like using images as template variables
because of the control it allows, and is very useful for things like background
images in your design.
There are all sorts of ways to crop your images and to even produce .webp
versions of images. All of that is well documented in the Wagtail
documentation so I won’t copy and paste that for you to read through right
now.
There are some keywords that are provided as well: null, blank, on_delete,
and related_name. I think it’s important that everyone understands what they
do and why. For the null keyword this tells Django when it creates a
migration file that this field is allowed to have absolutely no value in the
database; it’s completely empty. The blank keyword tells Wagtail whether or
not this can be blank when the page is saved - it’s like making a field
required or not required. The on_delete keyword provides instructions
when the linked page is deleted - sometimes you see models.CASCADE which
would delete this page too, but models.SET_NULL simply unlinks the selected
page from the home page. And lastly, related_name sets the reverse name to
travel from one object to another - this is set to + to avoid explicitly giving
the relationship a name.
When you create a relationship using a ForeignKey you can access the final
objects properties in the template. This is not unique to Wagtail, but it’s an
incredibly powerful feature that comes baked into the Django templating
engine, and Wagtail leverages the Django templating engine to render its
pages.
Any of the default fields that come with a Wagtail Page can be immediately
used. If you try to access a custom field you wrote on another page it won’t
work and will show nothing, not even an error. This gets into specific class
data which I want to talk about briefly before you finish this chapter.
At this point if you try to run the code in this section you will see an error.
Oh wait! You’ve seen this error before and know how to handle it.
Whenever you see the database operations error, it’s usually a good idea to
run the makemigrations command followed by the migrate command.
2.5 Specific class data
Specific class data refers to a specific class's fields, properties and methods.
When you created a ForeignKey relationship in the last section to a generic
Wagtail Page, Wagtail will only look up the data it needs. It doesn’t know
that you might want specific fields from a specific model; it assumes generic
fields only.
I’m throwing you in the deep end, but don’t worry you don’t need to fully
grasp this concept at this very moment. I’m merely arming you with the
knowledge you will need at some point in your Wagtail journey.
When this inevitably happens to you, and I’ve seen this happen to just about
every developer out there that was learning Wagtail, you’ll probably scratch
your head and wonder what you’re doing wrong. The fact is, you’re not
doing anything wrong. You may want to be able to select any type of car-
based page in the future: Tesla, BMW, Audi, Lamborghini, etc. So you
would tell Wagtail to allow every one of those pages to be selected by
creating a relationship to the CarPage (and all of its child classes). But if a
customer can only choose the color of their Tesla, and you need to find that
color, you’ll run into this specificity issue.
I won’t bombard you with more reasons or information as to why at this
moment because you’ve already done a lot of work in this chapter. But I do
want to make you aware of this problem and the solution. The solution is
easy: add .specific to the end of your relationship object. So if this were a
real example in a real template file, you would write {{
page.car.specific.color }}. Likewise for the Python version of this:
>my_car = Car.objects.first()
>color = my_car.specific.color
Once again, tuck this in your back pocket for later. It’s not something you
will need right now, but as you decide to customize your website you may
run into this and I want you to be well equipped to deal with this scenario.
2.6 Summary
It’s usually a good idea to create a blog on most websites to keep your users
up to date on new events, products, or general news. Blogs help your users
see that you are an active business. But more than anything, a blog can help
you market your products. After all, you’ll be creating an e-commerce
website and need some marketing tools. This chapter doesn’t focus on the
business aspect of your website, but it helps to set the underlying foundation
for all future Wagtail websites and it just happens to give you another tool in
your website tool belt.
As you can tell, you have a lot to absorb in this chapter. My goal is to keep
each section as short as possible so you can start writing more code. Don’t
forget that it’s completely acceptable to experiment beyond what is covered
in this book, too.
The source code for this chapter is broken down into individual commits
based on each section on GitHub. Here are the commits for each section
https://github.com/KalobTaulien/wagtail-cms-in-action/pull/2/commits. To
see all the changes in one place you can view the Files Changed here
https://github.com/KalobTaulien/wagtail-cms-in-action/pull/2/files.
3.1 Creating a new Wagtail app
When you start to create a new section in your website it’s often a good idea
to create a new Django app. Apps are how we logically separate content.
The idea is to separate apps so they can be easily enabled or disabled as your
website grows. Typically, apps hold together similar functionality and logic.
In the case of this new blog, it has nothing in common with the home app or
anything inside of it so we can put this into its own app.
Note
You don’t have to separate your apps, but it helps keep your code organized
in a very friendly way. If you are brand new to Wagtail and Django I highly
recommend getting into this habit.
With your virtual environment activated you need to type a new command
that hasn’t been introduced yet:
Figure 3.1. A tree structure showing your new blog app, along with its files and folders.
Figure 3.1 shows you the blog app you created by only showing the files and
folders. In most cases, but not all, you can delete the admin.py and views.py
files. In larger websites where you might write standard Django code inside
of your Wagtail website you may want to keep the admin.py and views.py
files, but you can safely delete them right now and recreate them if you find
out you want them at a later date.Wagtail uses models.py to store its pages,
and Wagtail will magically merge Django’s admin functionality, view
functionality, and model functionality into its Page class.
Tip
I like to keep tests.py for writing and running unit tests. That topic is beyond
the scope of this book. If you won’t be writing any unit tests you can delete
tests.py as well.
Until this point, nothing has changed. You won’t see a blog magically appear
in your Wagtail admin, and Django won’t even know that the blog app
exists. This is due to Django’s security. Unlike other content management
systems, like WordPress, where you can simply add files and they have an
immediate effect on your website, Django takes a security-first approach and
ignores whatever it can until explicitly told to accept something. This isn’t
the case 100% of the time, but it’s definitely the case when creating a new
app on your website.
INSTALLED_APPS = [
'home',
'search',
'blog',
…
]
To enable your new blog app, simply add "blog" to your INSTALLED_APPS.
Keep in mind that this is a Python list with strings in it so you need to use
commas after each app and use double or single quotes around the name of
your app.
Go ahead and start your local development server with python manage.py
runserver. If there were no complaints from your command line then
you’ve successfully created and installed a new app! Don’t worry if your
blog doesn’t show up in your Wagtail website yet; I’ll walk you through that
process in the next section.
As your website extends beyond this book you’ll eventually create new apps,
whether that’s for new Wagtail Pages or for Django specific features. Try to
create a new app called mytestapp, install it in your settings, and create a
new Wagtail TestPage. Don’t forget your migrations along the way!
3.2 Creating custom Page types
Now that you’ve created a new app using the startapp management
command and enabled it in your INSTALLED_APPS settings, you can begin
adding new page types. Pages rarely do the exact same thing as other pages,
so we tend to create new page types for specific purposes. Wagtail, by
default, makes no assumptions about your website, other than needing a
home page and likely needing a search app. It doesn’t know if you’ll be
creating a blog or anything else. Think of it like building a website from the
ground up. At first this might seem like a lot of work, but I’ll show you how
quickly you can create new page types and have them immediately work on
your Wagtail website.
Tip
No two websites are the same, and Wagtail understands this as a core
principle. This is why Wagtail websites are created with most features
disabled and with the option to enable them later. That, and it keeps your
website operating quickly, efficiently and it’s more secure due to less
exposed surface area..
To create new page types you only need to create a Python class that inherits
from the Wagtail Page class in an installed app. Open up your
blog/models.py file and add the code in the following listing:
class BlogIndexPage(Page): #A
pass
class BlogPostPage(Page): #A
pass
You’ll notice that the Django models import line disappeared. At this time it
isn’t needed, nor is it being used. Anytime you have an import that’s not
being used you can safely delete it from your file. There are no additional
fields yet so your new Wagtail pages will be the same as your initial home
page before we added any fields. If you try to add a child page before
making (and applying) your migration files may see an Error 404 page,
depending on your version of Wagtail, as shown in figure 3.2.
Figure 3.2. An example of a 404 page you should see when creating a new page that doesn’t yet
have migration files.
When you create a new Wagtail Page and you don’t yet have any migration
files and then you try to add a child page, you’ll run into a 404 error, as
shown in figure 3.2. In recent versions of Wagtail you’ll be presented with a
standard page editing interface to create a new page, but you may not be able
to save the page without seeing an OperationalError, like you see in Figure
3.3.
Figure 3.3. An example error suggesting that you need to create and apply migrations.
If you end up seeing a similar error, like what you see in figure 3.3, just
know this is perfectly normal. I want you to experience a common problem
by opening your Wagtail admin at http://127.0.0.1:8000/admin/, editing your
homepage, and clicking Add Child Page at the top of the page. I’ll show you
what this looks like using Wagtail 4, shown in figure 3.4.
Figure 3.4. You can find your page actions, such as adding a child page, in this dropdown menu.
When you’re viewing any page in the Wagtail admin, as shown in figure 3.4,
you will see three dots next to the name of the page near the top of the page.
This is your breadcrumb area, and the three dots are called an ellipsis. Click
on the link that says Add child page.
You should see three options when adding a child page: Blog index page,
Blog post page, and Home page. Click the Blog Index Page option. In
Wagtail 4 and newer, you’ll be able to edit your content without making
migration files and applying them; however, you won’t be able to
successfully save the page.
If you are working with an older version of Wagtail, being Wagtail 3.0 or
older, you are served a 404 error – 404 means missing page. This is Wagtail's
way of politely telling you that the page you want to add doesn’t exist.
Whenever you run into this issue you need to make migrations and apply
them. The reason this happens is that Wagtail is trying to look up your new
page in the database and determine which fields you can edit. However, your
database hasn’t been set up to match your code. This is where migrations
come into play.
When you create your migrations you’ll see a new file was created at
blog/migrations/0001_initial.py. Feel free to open this file and read
through it, but don’t edit it unless you know exactly what you’re doing.
Those are the instructions Django needs to create a new table in your
database with two page types.
Refresh the page in your browser and you’ll be able to add your new page.
Keep in mind you want every page to exist under the original Home Page.
Your site is set up by default to serve the Home Page as the root of your
website. So the Home Page will map directly to http://127.0.0.1:8000/.
When you create a new Blog index page as a child page (under the home
page), it will create http://127.0.0.1:8000/blog/ - that is if you named the
page Blog and the slug under the Promote tab is blog. The slug will match
the URL path on your website, such as a page with the slug of blog would be
accessible at http://127.0.0.1:8000/blog/. If you see a Page Not Found error
or a missing page template appears, it’s because Wagtail was unable to find a
published page with blog as the slug.
Note
If you try to preview your page before it has an official template file you’ll
see a TemplateDoesNotExist error. This is normal.
I’m assuming you’ll be calling this page Blog with the slug of blog, but you
can name it anything you like. Just keep in mind if you use a different slug,
the code in this chapter will need to be adapted for your use case.
Tip
You can change the slug of any page at any time. However, the home page
will always map to the `/` URI on your website, and the root page will
never be shown.
Once you have created a Blog index page you can create a Blog
post page as a child page to your Blog index page. So now you
have:
Root / Home Page / Blog Index Page / Blog Post Page
Figure 3.5. An example TemplateDoesNotExist error, showing the top half of the error page.
If you take a look at the file structure of the home app, you’ll see a folder
called templates/home/. This is just one location where Django will look for
your page templates. Quickly click the Live button for your Blog index page
or go to http://127.0.0.1:8000/blog/ and you’ll see a TemplateDoesNotExist
error. Or if you are live-previewing your page while you edit it, in Wagtail 4,
you’ll see the same error.
When you read the top portion of this error page, you’ll see it’s looking for
blog/blog_index_page.html. That’s coming from your app name followed by
the page model name. And in the first subsection of this page you’ll see
exactly where Django is looking - it will look in several directories for this
file. As long as a file called blog_index_page.html exists in one of those
directories, Django will stop throwing you a TemplateDoesNotExist error.
Tip
{% extends "base.html" %} #A
{% block content %} #B
<h1>Welcome to {{ page.title }}</h1> #B
{% endblock content %} #B
If this does not work for you, it will boil down to one of these three
scenarios:
Next you’ll need to do the same thing for your BlogPostPage class.
Remember that it will convert BlogPostPage to blog_post_page.html and
because this is inside of the blog app, it needs a template folder called blog/
as well. You can either create this file in your blog/templates/blog/ folder, or
in the generic mystore/templates/blog/ folder. I decided to put the
blog_post_page.html file inside of the blog/templates/blog/ folder so that all
my blog related files are grouped together.
By now you might have realized that you can create unlimited blog pages
anywhere in your Wagtail tree. In fact, you can create home pages as child
pages to other home pages. Often this is not what you want, and it might
confuse your content editors. So in the next section I want to introduce you
to strict page structures in the Wagtail tree.
3.3 Enforcing a strict page tree structure
Enforcing a stricter page structure is a good way to help your content editors
create content in the right place on your website. This concept can become
truly powerful when you progress into advanced or complex websites. On
the surface, restricting where pages can exist can be really helpful, especially
in the case of your blog. You may not want people creating blog posts under
blog posts under blog posts. It’s the same with home pages - you usually
have just a single home page. A home page is typically a uniquely designed
page and you may not want multiples of it. That’s where enforcing a strict
page structure can be useful. You can see this problem in action by creating a
child page under the home page, and you’ll see that you can create numerous
Blog Index Pages, Blog Post Pages, and even more Home Pages.
Another benefit of restricting where pages can live, and how many can exist,
is for search engine optimization. By forcing all of your blog posts to live
under the /blog/ URL you are telling search engines like Google and Bing
that all the child pages are relevant to the blog. Granted, search engines are
typically smart enough to figure this out on their own and this isn’t needed
for good SEO, but keeping a clean URL structure certainly helps search
engines, and your readers, understand what is relevant.
I want to start off by limiting how many pages of a certain page type can
exist in your entire website. For instance, there needs to be only one blog
index page:
class BlogIndexPage(Page):
max_count = 1
In your blog/models.py file, or any file that has a Page in it, you can restrict
how many pages of this type can exist using the max_count property. Here I
am limiting the BlogIndexPage to only ever have a single instance exist in
my Wagtail website. If I don’t plan on having more than one BlogIndexPage,
I typically restrict it. No migrations are needed for this functionality to work
because it’s not creating new columns in the database. Try making this work
for you and testing it out. Assuming you already have a BlogIndexPage that
exists, add a second one as a child page to any other page. If done correctly,
the option to create another instance of the BlogIndexPage will not show up.
You can apply the max_count property to any Page you have, as well. Go
ahead and add it to your HomePage class now, then try to create a child
Home Page. If you’ve applied the max_count property to your HomePage
and your BlogIndexPage, you’ll see the only option left is the BlogPostPage
– and Wagtail will automatically assume you’re adding a blog post anywhere
on your site, and will throw you right into the editing interface rather than
giving you a choice of which page to choose. This alone is often not enough
structure, so what I like to do is tell Wagtail that blog posts can only exist as
child pages to the blog index, as shown in the following listing:
class BlogIndexPage(Page):
max_count = 1
subpage_types = ['blog.BlogPostPage'] #A
class BlogPostPage(Page):
parent_page_types = ['blog.BlogIndexPage'] #B
There are two ways to restrict where pages can live in your Wagtail tree:
subpage_types are for child pages, and parent_page_types are for parent
pages. You don’t need to set both as one option is usually good enough.
Personally, I like sticking with subpage_types because that tells me, and my
peers, that certain pages can be child pages and it seems to be a bit easier to
understand than restricting parent page types. It’s good to know that both
options exist.
Tip
If you want a page to have no child pages, you can use an empty list, like so:
subpage_types = []. This is a Python list and takes any number of items
(as strings), or no items at all.
As of right now, our two blog page types are very basic. They only come
with whatever Wagtail gives us, which isn’t very much. In Chapter 2 you
created new model fields for the Home Page but Wagtail has a very powerful
feature called StreamFields that lets you create dynamic content and
rearrange it in a way that looks great for your website. Once you’re finished
up here please carry on to the next section where we learn about
StreamFields and create some basic StreamField types.
3.4 Adding Wagtail StreamFields
StreamFields are a powerful feature in Wagtail and in this section I want to
make it clear how they are so powerful and why they are absolutely vital to a
modern website. You’ve surely seen a beautiful website where a page
featured a nice image, then some rich text, followed by a video or another
image, a large quote, and various other content. In some content
management systems, those template components are hard coded so you
always need to fill in those sections so that every page looks exactly the
same. With a StreamField you can create these blocks of content and
rearrange them. And each section is optional, so if you didn’t have a video to
show on your page, you can simply omit adding that content. Each of these
sections is called a block and you can have an unlimited number of blocks in
each StreamField.
Tip
If you know about noSQL databases, StreamFields are stored as giant JSON
blobs in a large text field and will be unwrapped, processed, and rendered
when someone views your page.
Why are StreamFields so important? Well, they let you mix and match
content on the page. For apps like a blog, this means your readers aren’t
looking at (basically) the same page with different written content. You can
keep your content fresh and exciting, whereas standard fields on your page
are typically hardcoded into your template in a specific location. You also
benefit from reusability when using StreamFields, as shown in the following
listing:
content_panels = Page.content_panels + [ #C
FieldPanel('body') #C
] #C
Let’s break this down and digest what’s going on. First, the body is the name
of the field. By convention it’s usually called body but you can name it
anything you like. The body is set to a StreamField field type and takes a
list of tuples that can take optional keywords like form_classname and
template. The field itself takes additional keywords such as blank and null.
If blank is True, this means you can save the page without any StreamField
blocks in it, and null means the database value is allowed to be void of any
value (including empty strings).
Once you have created your migration files and applied them, you can load
your Wagtail admin in your browser and edit your page. Figure 3.6 shows
you what you should be seeing.
Figure 3.6. An example of what your StreamField will look like when editing the page.
Figure 3.6 shows what your StreamField blocks look like in your Wagtail
admin when editing your page. Now you can add a title block, rich text
block and an image. These are just examples and aren’t limited to these three
types. In a future chapter I’ll show you more advanced StreamField blocks.
And like all things in the world of Wagtail and Django, there is no
assumption about how you want to display your new content and thus the
templates won’t magically guess where you want your StreamField blocks to
show up, as shown in the following listing:
{% extends "base.html" %} #A
{% load wagtailcore_tags %} #B
{% block content %}
<h1>{{ page.title }}</h1>
{% include_block page.body %} #C
{% endblock content %}
If you add some blocks to your BlogPostPage and preview (or publish the
page and view the live page) you’ll see a TemplateDoesNotExist error. This
is caused by template="blocks/title_block.html" in the page model. If
you comment out that line you’ll see that the page renders perfectly, albeit a
bit ugly because there isn’t any styling on the website yet. I dropped that
template keyword in there on purpose to set you up for the next example.
Tip
You can add the template keyword to any StreamField block to give it more
markup and styling.
If you are following along with the sample code so far, please uncomment
the template=… line so you experience the TemplateDoesNotExist error
again. The TemplateDoesNotExist error will be familiar to you soon
enough. This error page is incredibly useful if we read through it because it
tells you exactly where it’s looking for the template file. For StreamField
block templates I like to put them in the generic templates folder rather than
in a specific app folder because blocks tend to be re-used across several
apps.
Here we wrapped the content of the block in an HTML <h1> tag and placed
the content of the tag in {{ self }}. Notice that blocks don’t use {{ page
}} because blocks aren’t limited to just pages and they always refer to
themselves as self in their templates. Blocks can also be created in snippets
and site settings, two topics that I cover later in this book.
If you view your blog post page now you will see that the title text is in a
larger font whereas it was originally in a body-sized font and didn’t meet our
purposes. With custom templates you can customize how your block is
portrayed. Another example is the ImageChooserBlock where it shows the
original image but we may want to crop the image, as shown in the
following listing:
class BlogPostPage(Page):
…
body = StreamField([
("title", blocks.CharBlock(
form_classname="full title",
template="blocks/title_block.html"
)),
("richtext", blocks.RichTextBlock()),
("image", ImageChooserBlock( #A
template="blocks/image_block.html" #A
)) #A
], blank=True, null=True)
…
Also keep in mind that migrations need to be created and applied when a
Wagtail Page model has field changes. Although StreamField changes don’t
actually create any database changes, Django will pick them up and create a
migration file for you anyway, as shown in the following listing:
{% load wagtailimages_tags %} #A
Try this with the RichTextBlock and get it to render links and images
normally. You may need to refer to the Wagtail documentation for this
exercise and that’s OK – it’s good practice for when you outgrow this book.
You can add StreamFields to any page at this point. And if you want, you
can add multiple StreamFields to a single page if you need to - though that’s
fairly rare for most pages and websites that I’ve seen.
Next we want to take a look at adding blog posts to the /blog/ page because
right now it’s a fairly empty page.
3.5 Adding blog posts to Page context
Adding context to your page is the act of passing additional information to
your template. Context is the data that you, Django and Wagtail send to your
templates. Whenever you see {{ page }} that means a variable called page
is being passed to the template. In a StreamField template there is no explicit
page variable because StreamFields can exist outside of a Wagtail Page.
Instead, you’ll use the {{ self }} variable to access StreamField block
properties.
Wagtail adds these context variables on its own so you don’t have to write
that functionality for every page. But sometimes you want to add something
that’s unique to your site, as shown in the following listing. Or maybe you
want to execute some kind of logic written in Python and show it in your
template - that’s a good example of when you need to add to your page
context.
class BlogIndexPage(Page):
…
def get_context(self, request, *args, **kwargs): #A
context = super().get_context(request, *args, **kwargs) #B
context['blog_posts'] = BlogPostPage.objects.live() #C
return context #D
If you’re familiar with object oriented programming in Python this will look
somewhat familiar to you. But if you are new to OOP, this will look
incredibly strange. What’s happening here, however, is quite simple if it’s
broken down, line by line. First, I’m writing def get_context(…) which
matches what Wagtail has in its Page model, in the original code Wagtail
comes with. Then context = super().get_context(…) tells Wagtail to
execute its original get_context() function and return it in a variable we
can use. Then context['blog_posts'] = … is querying the website for
BlogPostPages that are child pages of the BlogIndexPage, and are published
using .live(). Lastly, the entire context, along with the new stuff I added, is
returned.
Now you can create new BlogPostPages, and publish them, and they’ll be
available to you in the BlogIndexPage template. It’s also worth noting that
your site should be restricting where pages can exist. If you have a
BlogPostPage outside of the BlogIndexPage it will continue to get picked
up in this example, but the link may not go to /blog/your-blog-post/.
{% extends "base.html" %}
{% block content %}
{% for child in blog_posts %} #A
<h1>{{ child.title }}</h1> #B
<a href="{{ child.url }}">Read blog post</a> #B
{% endfor %} #A
{% endblock content %}
Here I’m looping through all the blog_posts. If there are no blog posts,
nothing will show up so make sure you have at least one published
BlogPostPage in your website. With each loop, it’s assigning {{ child }}
as a temporary variable in the template that mimics the {{ page }} variable,
but this time it holds data for the child page rather than the overall
BlogIndexPage. Head on over to http://127.0.0.1:8000/blog/ to see your new
blog index page. You’ll notice it’s rather bland right now, but it’s
automatically pulling in your blog posts. You can see an example of my
work so far in figure 3.7.
Figure 3.7. A simple HTML-only page that shows my latest blog posts.
Note
It’s often called an index page because it holds links to other pages, like an
index of listings.
I often see context used for features like this. But it’s not limited to just this.
If you have custom Python logic you want to execute and then display,
adding additional context is good for that too. Or if you end up creating a
complex Wagtail website where Pages inherit from other custom page types
you’ve created, you may want to overwrite, or delete, variables from your
context entirely. As for the aesthetics of our pages, don’t worry too much
about that right now. In the next chapter you and I will apply some frontend
styling to make the website look a bit more modern, rather than plain text on
a boring background.
3.6 Creating a global navigation
Global navigation is one of the most important parts of your website. Until
now the website you’re creating didn’t need a global navigation because
there weren’t links to other top level pages. Now I want people to land on
any page and be able to click around. Without this ability, your users will
have to guess pages and type them into the URL bar in their browser. So in
this section you will create a simple template tag that will fetch child pages
under the Home Page that are allowed to exist in menus.
Note
The foundations of the code in this section use pure Django to set the
foundation of a template tag. But the function itself will use parts of Wagtail.
Open up your home/ folder and create two new files located at:
1. home/templatetags/__init__.py
2. home/templatetags/global_navigation.py
The __init__.py file tells Python that this folder can be used as a module and
should not be overlooked. The __init__.py files are often left empty and
that’s exactly what this is going to be: empty. The global_navigation.py will
be loaded into the template using the {% load %} template tag and you’ll use
the function that’s about to be created in the template.
register = template.Library() #A
@register.simple_tag #B
def global_nav_items(): #B
homepage = HomePage.objects.first() #C
return homepage.get_children().in_menu().live() #C
This template tag called global_nav_items will look up the first HomePage
it can find (another reason why restricting your page tree can be important)
and then look up the child pages that are published (not drafts) and are
allowed in menus. The key here is the phrase allowed in menus. By default
your pages are not allowed in menus. Stop here, edit your BlogIndexPage,
click on Promote and check the box at the bottom that says Show In Menus.
You’ll need this turned on for your menu to work. And for all future pages
you want to link to in your global navigation, you’ll need to check that box.
Keep that in mind as you grow your website over time as it’s a small but
important feature.
Note
Next you’ll need to update the base.html file. Up until this point the
base.html file hasn’t been edited whatsoever and for most websites, once it’s
finalized it’s rarely edited. The base.html file is also the main point of entry
for most of your future templates. Open up the base.html template file using
your text editor. Your base.html file will be in the mystore/templates/ folder,
as shown in the following listing:
{% load … global_navigation %}
…
</body>
…
Notice how the {% load %} tag accepts the name of the file:
global_navigation.py, with the .py part being chopped off the end. The
function itself is called global_nav_items() and the template executes that
function by using {% global_nav_items … %}
Note
If you don’t see a link to /blog/ on all your pages, make sure that it’s a direct
child of the Home Page and that the Show In Menus option is checked and
that the page is published.
Just like that, you now have a global navigation menu. Once you start
creating new top level pages (directly under the HomePage) you can activate
them in the menu by clicking the Show in Menus option under the Promote
tab when you’re editing a page in the Wagtail admin. Go ahead and update
that option in your BlogIndexPage and republish that page. When you visit
any page on your website, you’ll see the link appears at the top, just as you
see in figure 3.8.
Figure 3.8. A link to the main blog page is shown at the top of every page.
In figure 3.8 you’ll see your navigation links to the main blog page now,
along with a hardcoded link to your websites home page. If you don’t see the
link to your blog, make sure you check the Show in Menus checkbox when
editing your BlogIndexPage, and that the page has been published (and not
just saved as a draft). Also keep in mind that Wagtail holds no opinions
about your frontend, so the styling is completely up to you. However, in the
next chapter I’ll provide some frontend styling using Tailwind CSS. This
very light styling will help the website look more organized as this website
gets converted from a simple blog to an e-commerce website.
In the next section I want to get you set up with an advanced way to
experiment with Django and Wagtail. It’s not exactly related to creating a
blog, but I think this chapter covered plenty of topics that you may want to
experiment with, most notably Django's ORM (object relational mapper).
3.7 Learning by experimenting
Learning by experimenting is how I originally learned Wagtail. It’s fun and it
exposes you to parts of Wagtail that aren’t formally documented. There are
two primary ways to learn while experimenting: writing a ton of code, or
using the Django shell and writing interactive snippets. Both are completely
viable options but in this section I’m highlighting the second option:
interactive coding.
Django comes with a powerful tool called a shell and you can access it at
any time with python manage.py shell. You can import classes and
functions you need and test them out directly in your terminal and get live
feedback. But the shell is typically quite limiting and doesn’t support a lot of
great features. If you’ve ever used Jupyter Notebooks to experiment with
Python, this is very similar to that but it goes through your command line.
INSTALLED_APPS = INSTALLED_APPS + [
'django_extensions',
]
If your local development server runs without any complaints, then you are
good to use shell_plus.
The shell_plus option is very powerful and can save you a lot of time
because it will automatically import a lot of code for, as you can see from
the green text in figure 3.9. I won’t go into all the power behind shell_plus
and ipython because that could be its own book, so feel free to read through
their documentation if you want to learn more. The shell_plus functionality
comes from a Django package called django-extensions and you can read
more about it here https://django-
extensions.readthedocs.io/en/latest/shell_plus.html
Go ahead and open your Django shell, whichever one you prefer to use, and
type the commands in the following listing, one by one:
These are some basic examples of what you can do. Now I like to take this
to the next level. Whenever you assign a variable, type a period after it and
hit tab (on Windows you might need to hit tab twice). This should auto-
populate some options for you and you can see what’s possible with each
variable. Pages come with lots of goodies that are worth exploring, along
with standard Django Models, if you have any plain models at this time.
Before you continue it’s a good idea to style the templates you’ve been
working on. They don’t need to be perfect by any means, but your website
should look like a basic website at a bare minimum, rather than plain text on
a white background. The reason it’s a good idea to style some of your
templates as you progress through Wagtail CMS in Action is to help reduce
confusion when you add a lot of content to your templates. If your pages just
look like giant walls of text and you come back to this chapter later on,
you’ll likely be confused about where you left off and what your site is
supposed to look like.
I’d like to emphasize two points. First, your templates do not need to be
perfect right now — you need some kind of styling or design to help you
understand where your content is. Without this step, most developers get
stuck and think, “Why is my content not showing up?” when in reality the
content is showing up but it’s in a massive heap of text and developers don’t
see it right away. The second point I’d like to emphasize is that this is not a
design book and you won’t be making the most beautiful website on the
planet - the focus is on Wagtail, not on design. That said, if you are a
frontend developer and would like to make your website look amazing right
now, feel free to do that!
As always, the code is available on GitHub. The full pull request can be
found here: https://github.com/KalobTaulien/wagtail-cms-in-action/pull/3.
You can also view the individual commits for each section of this chapter,
along with all the files changed.
4.1 Installing Tailwind CSS
Installing Tailwind CSS can be quite the task for a non-frontend developer,
and for that reason I provide you with two ways to install Tailwind CSS. The
first option is more friendly for non-frontend developers and does not
require you to use Node.js or NPM. Rather, you can simply add a single
<script> element to your <head> in your base.html file. The second option
is more involved, and is the proper way to install Tailwind CSS. It will
require you to have Node.js installed on your computer, and the ability to run
a second command in a second command line terminal window. But don’t
worry, it’s not as scary as it used to be and I’ll guide you through it.
Throughout this book I’ll be using Tailwind CSS. Tailwind is easy to install
and creates beautiful websites. Unlike Wagtail, I am applying my opinion
here. But that does not mean you need to follow in my footsteps. If you’d
rather use Bootstrap, write your own CSS, or use a different styling library
— that’s completely up to you. That’s one of the beautiful things about
Wagtail!
It’s at this point that you have created a functional website. However, the
website you’ve created so far is probably an eyesore. In this chapter you will
apply some form of styling to your Wagtail website. But first, you need to
install Tailwind.
I’m going to cheat by installing the content delivery network (CDN) version
of Tailwind CSS. CDNs are a good idea because they’ll connect you to the
closest server to you in order to serve your files faster. This is better than
reaching across the planet, so to speak, for a file.
Warning
You need to know that the CDN link to the Tailwind CSS file I’m about to
use is not supposed to be used for production websites. I’m simply taking a
shortcut at this point so we can focus more on Wagtail development than
frontend development. When you are ready to launch your website, you will
want to properly install Tailwind. The Tailwind documentation is quite good,
as well, and you can find that here https://tailwindcss.com/docs/installation.
<script src="https://cdn.tailwindcss.com"></script>
Save your base.html file, start your Django server, and head on over to
http://127.0.0.1/ (or http://localhost:8000/) to see your new styles.
If you don’t see any changes, make sure you reload your page once or twice.
Sometimes there is a level of caching that’s applied.
Note
If you run into caching problems using the JIT JavaScript file you can
always load the full version of Tailwind using a CDN. The full version of
Tailwind is a very large file, up to 5mb or more, so take extra caution not to
deploy your production website using it as it’s not supposed to be used in
production and should only be used in local development.
Once you see the styling affecting your website and can be assured that
Tailwind CSS is loaded and working the way you expect, you can delete the
sample code you wrote earlier as that was just a test to ensure your CSS
library is loaded properly. The sample code I’m referring to was the stuff
here example:
The sample code you can safely delete can be referenced again just before
this paragraph.
If you’ve never worked with Node.js, that’s OK! It’s a lot like installing and
using Python in the sense that you can execute commands directly from the
command line. In the event that you do not have Node.js installed on your
computer, please install it now by going to https://nodejs.org/en/download/
and downloading the LTS version for your operating system. Once the
installer is downloaded, run it to install Node.js on your computer. After you
have completed the full installation of Node.js, open a new command line
window and type node -v to see your current version of Node.js.
Tip
When you install a new programming language you might need to close and
re-open your command line program before the new commands will be
available.
When you run the node -v command in your command line terminal you’ll
see the version of Node.js that’s been installed on your computer, as
demonstrated in figure 4.1.
Figure 4.1. Displays my currently installed version of Node.js through my command line
program.
Keep in mind that I’m using Node.js v14, as you can see in figure 4.1,
because I installed Node.js prior to v16 being released. You might have
Node.js v16 or newer, and that’s OK.
The next thing you want to do is head over to the Tailwind installation
documentation, located here https://tailwindcss.com/docs/installation. I
always keep this page open when I’m installing Tailwind into an existing
project just for reference.
Open a new command line window (or tab) and cd (change directory) to
your main Wagtail project folder. You’ll know when you’ve reached the
right folder because you will seee a manage.py file in there.
Tip
On Mac and Linux you can type ls to list your files, and on Windows you
can type dir to list your files.
Once you’ve located your main Wagtail project folder, go ahead and type the
following commands:
The npm install command will install Tailwind CSS, PostCSS and
Autoprefixer by downloading all the necessary files into a node_modules/
folder. The npx tailwindcss init command will generate your
tailwind.config.js file for you, which tells Tailwind which options to
consider when you, eventually, execute a Node.js command to generate a
.css file.
Tip
Add node_modules/ to your .gitignore file, if you have decided to use Git
with your project. This way you don’t accidentally commit thousands of
small files to your repository.
Now that you have a file called tailwind.config.js in the root of your project.
Open that file and ensure it looks like the following listing:
I won’t need to watch and compile any JavaScript or custom CSS right now,
so the only file type I’m telling Tailwind to look for are .html files.
Next, create a new file called postcss.config.js in the same directory that
your tailwind.config.js file exists in, as shown in the following listing. Using
PostCSS as a plugin is the most seamless way to integrate it with build tools
like webpack, Parcel and other build tools. It doesn’t hurt to have this in
your project.
module.exports = {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {},
}
}
This is a fairly minimal PostCSS configuration file, but it’s slightly different
from the one you’ll find in the official Tailwind documentation. I’ve opted in
to use PostCSS imports, and to enable CSS nesting, like in standard
SASS/SCSS files.
Lastly, before you start running Node.js commands to compile and create
.css files for you, you need to create a frontend source folder, often called
src/, as shown in the following listing. Inside of that folder, create a file
called input.css. The final location of this file should be src/input.css, which
you will need to manually create.
@tailwind base; #A
@tailwind components; #A
@tailwind utilities; #A
The last step you’ll need to take is to run a couple of commands which
depend on the phase of your development. I’ll explain that in more detail in
just a moment.
During local development you may want to watch your files for any changes
and have a new .css file automatically compiled for you. The magic at this
stage can save you so much time! Imagine that every time you make a small
design change in one of your Wagtail templates, you want to see what it
looks like, but you need to open a second program and run another
command to see those changes. That would get pretty annoying, pretty
quickly! Maybe at first that doesn’t seem so bad, but imagine doing that 100
or 1,000 times every day. This is a tedious task, and it’s no way to spend
your time as a developer! Instead, you can tell Tailwind to watch for changes
in your .html files and recompile a final .css file immediately. All you need
to do is refresh the page in your browser to see the differences. That’s the
good life!
Open up your favorite command line program, and type the following
command, as seen in figure 4.2:
npx tailwindcss -i ./src/input.css -o
./mystore/static/css/mystore.css --watch
Figure 4.2. The tailwindcss watch command that looks for changed files and rebuilds the .css file
your website uses.
Enter the command you see in figure 4.2 into your command line, and hit
Enter. It will continuously watch for changes in your .html files, and create a
new mystore.css file whenever it detects a change. You can test this by
changing any of your .html template files and watching it automatically
update in your command line terminal.
Note
The output of that command was mystore.css. If your project was named
something else, or if you’re using a different .css file in your base.html
template, you will want to adjust the output command.
The previous command has three flags I want to identify for you:
The -i flag, which is immediately followed by the input.css file. It
takes a direct location to the input.css file, including the folder name.
The -o flag, which is your output file. You can tell it to create your final
.css file anywhere you like, with any name you like. I kept it simple by
overwriting the existing mystore.css file in my mystore/static/css/
folder.
The --watch flag, which tells npx and tailwind to continue polling
those files for changes.
Next up, test out your new setup by writing some miscellaneous frontend
code in your base.html file while the npx tailwindcss -i
./src/input.css -o ./mystore/static/css/mystore.css --watch
command is running. In another command line window, make sure your
Django server is running. You can write some test code like so:
When you check your command line program, you will see it is rebuilding
your assets and dumping the final code into your mystore.css file. When you
view your website you will see a big red banner with some text, like in
figure 4.3.
Figure 4.3. Shows that your entire website has changed and that the classes from your template
files have been compiled properly.
Load up your Wagtail website in your browser, and you should see your
example code with a red background, blue text and a border, as shown in
figure 4.3.
Tip
If your CSS is not loading properly in your browser, check that your website
is loading the correct .css file by opening your browser’s dev tools. The
filename must match the filename from the Node.js command. You will also
want to check your command line terminal for possible errors when it tries
to compile your CSS.
When you’re ready to compress your mystore.css file, or minify it, you can
run another command that will remove all the unnecessary spaces and line
breaks.
Once you see the styling affecting your website and can be assured that
Tailwind CSS is loaded and working the way you expect, anyou can delete
the sample code you wrote earlier as that was just a test to ensure your CSS
library is loaded properly.
Tailwind CSS is a utility-based CSS library, meaning you rarely ever need to
write your own CSS because all your styles are in prewritten CSS classes for
you. However, that means there are a lot of classes to make use of, and that’s
nearly impossible to remember. So if you ever get stuck or become curious
about what Tailwind CSS can offer, you can always go to the official
documentation and see what they have to offer:
https://tailwindcss.com/docs/installation. The search feature is absolutely
amazing!
4.2 Base template and home page styling
Every template a Wagtail Page uses will, technically, have a minimum of
two templates: the template it’s trying to render, along with the template it
extends from - which is typically the base.html template. When you’re just
starting on a Wagtail project your website doesn’t look great. That decision
was made on purpose because Wagtail holds no opinions about which
frontend technologies you use. Again, Wagtail does not want to dictate how
your website should look or act on the frontend of your website.
Note
When I say frontend I’m talking about the design of your website and any
technologies related to it, such as React.js or Tailwind CSS.
Keep in mind that no two websites are exactly the same. So feel free to copy
what I’m doing in this chapter,and steal anything you like or implement your
own design. The goal is to add simple styling to your website so you can
keep the frontend of your website organized as you continue to develop
more features.
The first thing I usually tackle is the navigation. Right now the navigation is
a bare bones list of links. Your navigation typically has two primary
subsections: one for your logo and another for the links to other pages.
Note
For now, you’re not going to make this website responsive/mobile friendly -
let’s just focus on a desktop version. With Tailwind CSS it's easy to add
mobile support later on.
Go ahead and replace the menu work in base.html with the following code.
Also feel free to read through this, take some time in the Tailwind CSS
documentation to read about each class and what it does. Most of it is pretty
self-documenting, but classes like max-w-6xl aren’t exactly intuitive to
begin with and the 6xl maps out to a certain number of rems.
The next section to add is the hero section. It’s the big banner that’s typically
right below the navigation. It often has an image, some text and a call to
action. The goal of the hero is to explain what the page is about and to get
the user to take an action. But sometimes they are purely informative, too.
Keeping your base.html file open find your </nav> tag and immediately
below it add the following:
…
</nav>
{% block hero %}
<h1>testing</h1>
{% endblock %}
{% block content %}
…
Let’s add real content in your home_page.html file. Replace the {% block
hero %}{% endblock %} code with the code in the following listing:
{% block hero %}
<div class="bg-gradient-to-r from-green-300 via-blue-500 to-
purple-600">
<header class="max-w-6xl mx-auto py-24 text-white grid grid-
cols-3 gap-8">
<div class="col-span-2">
<h1 class="text-5xl">{{ page.title }}</h1>
</div>
<div>
{% if page.subtitle %}{{ page.subtitle }}{% endif %}
</div>
</header>
</div>
{% endblock %}
Feel free to experiment with the above code in any way you want. Once
more, this website won’t win any awards for its spectacular design, but
rather, it’s going to look quite basic and about 200% more organized than it
originally did.
Now let’s make all of the future content for your website exist in a container.
Note
A container is a block of HTML and CSS that, typically, centers your code
in the middle of the page so your words don’t hug either side of the browser.
In your base.html file, wrap a <div> with a max width and automatic
left/right margin alignment around your {% block content %}. This will
make sure that all of your future content from all of your pages has a max
width and is centered. This also lines up with your navigation content and
the hero content.
…
<div class="max-w-6xl mx-auto py-12">
{% block content %}{% endblock %}
</div>
All of your content from every page will now be aligned in the middle of
your page. This is a common theme that modern websites use to avoid
having text stretch too wide across large monitors and TV displays.
Note
You can only have one block with the same name. If you run into an error
that says you have two blocks named content, simply delete one of them
and refresh your page.
Next up is the footer. Just about every website has a footer – it’s for adding
additional links, copyright symbols, and other misc. information that your
readers may want to see.
Note
Most web pages have a footer. These are great for helping Google and other
search engines find their way around your website by adding extra links or
information to every page.
If I ask you which template file you need to edit, knowing that every page
has the same footer, which file would you open?
In your base.html, and above the </div> element containing the {% block
content %} add the code in the following listing:
{% block footer %}
<footer class="max-w-6xl mx-auto border-t py-12 my-12">
<div class="flex justify-center space-x-4">
<div>
<a href="/">Home</a>
</div>
{% for menu_page in nav_items %}
<a href="{{ menu_page.url }}" class="text-black">
{{ menu_page.title }}
</a>
{% endfor %}
</div>
<div class="text-center text-sm">
Copyright © {% now "Y" %}
</div>
</footer>
{% endblock %}
I opted to wrap the footer in a {% block %} so that future pages can
overwrite this with something else, possibly replacing the entire section with
absolutely nothing. This is good for checkout pages that allow the user to
focus on a task without being distracted by more links.
If you go to any blog page right now, you'll see that the navigation and the
footer are applied to every page and that your content is immediately
centered on the page, too. However the styling for each blog post in the
BlogIndexPage template and the individual blog posts are not styled yet. At
this point I would like you to attempt applying styling to your
blog_index_page.html and the blog_post_page.html template files before
moving on.
Tip
If you get stuck you can always undo your work and continue reading this
chapter.
4.3 Blog page styling
There are only two templates left that need some styling. At this point most
of the hard work is done because of the blocks and global components in the
base.html file. The two templates remaining are the blog_index_page.html
and the blog_post_page.html page. As I've mentioned before, this is a book
about Wagtail CMS so I’m not going to dive too deep into the frontend
specifics of this Wagtail website, but you can always pause here and
continue styling your website the way that makes you happy. As always, this
frontend code is completely optional. At no point should you feel any
pressure to style or template your website the same way I'm doing in this
book. And you certainly don't need to learn and/or use Tailwind CSS, if you
don't want to.
Let’s start with the blog_index_page.html template - the template that lists
all the blog published blog posts. The blog index page, sometimes called a
blog listing page, is a very simple page that links to other blog posts. That’s
all it does, and rarely does it need anything more.
Because the blog detail pages are relatively simple you really don't have a lot
to work with. But if you feel like this is too basic and want a challenge, I
would suggest adding, and displaying, a preview image to each blog post,
and displaying that image on the blog index page. But don't worry about
adding authors right now. Adding blog authors gets into a feature known as
Snippets, which I’ll guide you through later in this book.
First things first, let's add a hero section to the blog index page. This can be
copied from the home_page.html if you’d like, and modified to suit your
needs. You’ll notice that the hero section is nearly identical in code to the
home_page.html, so if you’re feeling up for a challenge you can always take
this code and make it reusable by creating a new template file and including
it in both templates so that future changes will affect both page templates.
Then in your {% block content %} you just need to make this look a little
nicer, as shown in the `sting. You can use a typical list if you'd like, but I
opted for a bigger section that could have room for a subtitle or image if you
wanted a bit of a challenge.
{% block content %}
<div class="divide-y divide-solid">
{% for child in blog_posts %}
<section class="py-12">
<a href="{{ child.url }}" class="text-3xl">
{{ child.title }}
</a>
{% comment %}
You could add a subtitle and image in here, too.
Don't forget to add those fields to your models.py
file, run migrations, and then you can access them
with {{ child.specific.subtitle }}
{% endcomment %}
</section>
{% endfor %}
</div>
{% endblock content %}
Save your blog_index_page.html file and view your main blog page. And it
should look like figure 4.4:
Like the blog index page and home page templates, you probably want to
add a hero section so users know what the post is called.
Note
We aren't using a standard hero template because all of the hero sections
have been too different so far. But if your hero sections are all the same, you
can write {% include "includes/hero.html" with title=page.title
subtitle=page.subtitle %} to write one file but use it over and over
again.
{% block hero %}
<div class="bg-gradient-to-r from-green-300 via-blue-500 to-
purple-600">
<header class="max-w-6xl mx-auto py-24 text-white text-center">
<small>{{ page.get_parent.title }}</small>
<h1 class="text-5xl">{{ page.title }}</h1>
</header>
</div>
{% endblock %}
This is very similar to the hero section from the blog index page. In fact, I
stole about 90% of the code from the other template. But notice the one
thing I added here: {{ page.get_parent.title }}
Every Wagtail page comes with a parent, even the home page. The template
syntax {{ page.get_parent.title}} is the same as the following code, but
used in the template rather than being written in Python.
def get_parent_page_title(page):
parent = page.get_parent()
title = parent.title
return title
# Or
page.get_parent().title
Like all generic page objects, you can use .url, .title and any other field
that comes with the Page object. The default Wagtail Page object is different
from the custom page types that you’ve created so far in the sense that your
custom page types have additional data. The only difference between what
you did earlier, compared to what we're doing now, is you’re looking for the
parent page information instead of the child page information.
Hopefully at this point the Wagtail page hierarchy starts to make more sense.
If you load up your website in your web browser you'll see that our entire
website has a theme with different sections. Every page has these four
elements:
1. Navigation bar
2. Hero section
3. Content section
4. Footer
The design isn't perfect. Not even close. But it's exponentially better than
what I started with not that long ago, which was just a basic HTML page
with no color or styling whatsoever. And design, like many things in web
technology, is constantly evolving.
4.4 Summary
Your initial design work is a way to organize your content at this point.
Parent pages allow you to create child pages on your website.
Child pages are like files in a folder on your computer.
There can be numerous child pages beside each other called siblings.
Child pages can only have one parent page, whereas parent pages can
have multiple child pages.
Think of parent pages and child pages like a folder system that has
subfolders and files.
You can use Tailwind CSS, through the Just-In-Time JavaScript library,
to start writing Tailwind without needing to compile and purge unused
CSS – and no webpack or frontend build systems needed!
Global components almost always go in the base.html template file.
Injecting code into your global template system requires adding a {%
block %}{% endblock %} in your base.html file.
Reusing template files is as easy as writing {% include
"folder/template_name.html" %} and the file itself should exist in
mystore/templates/folder/.
To access the parent page from inside of a template you can write {{
page.get_parent.title }} and {{ page.get_parent.url }} .
5 Creating a Wagtail store
This chapter covers
Welcome to what I like to call “the good stuff.” In this chapter I will show
you how to use more advanced features such as Orderables and customized
StreamField blocks. Don’t worry if those topics sound daunting right now.
By the end of this chapter you’ll be familiar with how it all works. I’ll also
show you how to use Wagtail’s built-in search capabilities to create a global
search function so people can find your products. Some of this is repeated
code for you, if you created the blog with me prior to this chapter. But I like
to make sure everyone is on the same page without making assumptions
about whether you started this book from the very beginning.
At first I will go over creating a new app called products and together you
and I will create two new page types: a ProductIndexPage and a
ProductPage. You might have noticed, already, that this is following the
same pattern from the blog. This is a standard pattern across most Wagtail
websites I have seen, where the app's primary page is called an index page
(or sometimes a listing page).
It’s absolutely vital that you have a clear understanding of how Wagtail’s
tree works at this time, otherwise you may find yourself confused in the
future when working with nested pages. Don’t worry, though, it’s super
easy! An example of how the page tree works can be found in figure 5.1.
Figure 5.1 A set of Wagtail Pages shown in a file and folder tree.
The homepage of your website will always be the first slash after your
website's domain name. As an example, when somebody goes to
https://yourwebsite.com/, Wagtail will look for the slash after the .com and
lookup the default homepage. You can see this in action in figure 5.1.
Every page you create after the home page will be nested under the home
page and the slugs of each page will be visible in the browser URL bar. So if
you created a child page under your homepage called blog, that would
convert into https://yourbwebsite.com/blog/. When you create another child
page under the blog page, Wagtail will generate the URL like so:
https://yourwebsite.com/blog/dev-life-or-goat-farmer/. An example of how
this would be broken down inside of Wagtail can be seen in figure 5.2.
Figure 5.2 A breakdown of an example blog post URL from a Wagtail website.
You might be asking yourself, “Kalob, why is this important to know right
now?”. Well, this is the Wagtail Page tree, and in this example the /blog/
page is also known as your Index page (or Listing page), because that’s a
page that, typically, links to its own child pages. All of this can be visually
seen in figure 5.2.
Index pages are not strictly needed but provide a way for you to add
functionality to your app and help keep your website (adn Wagtail Pages)
neatly organized for your content editors. Let’s say you want to add an
internal search function that only searches for products and doesn't search
every page across your website. You could put that search bar on the
ProductIndexPage, along with information about your policies, and then add
marketing efforts and several calls to action, known as CTAs, that don’t
interfere with the actual product itself.
This chapter tackles a lot all at once and by the end of the chapter you will
have a catalog of products on your website. You won’t be able to accept
payments yet – that comes a little bit later in this book. But at least your
website will have the content and data laid out nicely for your editors, and
your website will have a custom search feature that you can adapt and adopt
for other parts of your future website, should you decide to modify your
website beyond what this book teaches.
All the file changes are available on GitHub in this pull request:
https://github.com/KalobTaulien/wagtail-cms-in-action/pull/4.
5.1 Creating the Products app
Let’s get started by creating a new app for your existing Wagtail website.
This app is different from the blog in enough ways that it warrants its own
app.
Note
Sometimes the lines between apps become blurry. This is normal. If you
can’t determine where to write a new function, for example, you can either
create a utils app or put your code in the app that it has the most in common
with.
First, open your command line and activate your virtual environment (or
enter your Docker container, ssh into Vagrant, or access your preferred layer
of abstraction). Then run the following command:
Here, you are telling Python to run the manage.py command which executes
the startapp management command with the last parameter being products
which is the name of your app. If you take a look at the folder structure in
your text editor, you will see a new folder called products/. By default,
apps are not installed in Django and that’s for security. To install, or activate,
this new app, open your mystore/settings/base.py file and add products to
your INSTALLED_APPS. Your list of installed apps should now look like this:
INSTALLED_APPS = [
'home',
'search',
'blog',
'products',
…
]
That is all you need to do to create and install a new app. Pretty easy, right?
Note
Next, I want you to create two new page types in products/models.py. One
should be called ProductIndexPage and the other should be called
ProductDetailPage. I’d like you to try adding these on your own. If you get
stuck or break something, you can always delete the products/ folder and
subfolders and start again. If you don’t know where to start with this,
continue reading and I’ll walk you through the steps to create a new page
type.
# products/models.py
from wagtail.models import Page
class ProductIndexPage(Page):
max_count = 1 #A
subpage_types = ['products.ProductDetailPage'] #B
class ProductDetailPage(Page):
parent_page_types = ['products.ProductIndexPage'] #C
subpage_types = [] #D
This is how you will create two new page types. Now keep in mind that the
code does not reflect changes in the database yet. Pages, just like Django
Models, need to have migrations created and applied. Your Wagtail Page
should always have a migration file that sets up your database properly to
store data. If you ever forget to run migrations, but you have this code and
you try to create a new page, you will likely see an error in the Wagtail
admin or a 404 Missing Page response. Go ahead and create a new migration
and then apply the changes to your database.
python manage.py makemigrations
python manage.py migrate
If you ever run into an issue where Django says there are no migrations to
create or apply, it’s likely that one of two things has happened: either your
database is completely up to date already, or you don’t have the new app
listed in your INSTALLED_APPS setting in base.py.
Note
Think of it like a shopping list. If you went to the grocery store with a list of
15 items you had to pick up, you might want to order them in your list so all
your fruits and vegetables are together. On paper you need to cross out the
item and rewrite it, or maybe even make a new list. Orderables allow you to
move your item up or down the list without having to rewrite anything.
Repeatable content is often similar in nature. Another example, as shown in
figure 5.3, are links to other pages, images or sometimes dedicated spots for
advertisements.
One thing to keep in mind as you learn about Orderables is their difference
to StreamFields. An Orderable is a strict structure of data whereas a
StreamField is fluid, like a stream of water, where you can mix and match
several different content types like images, titles, richtext, embeds, and
more. Unlike StreamFields, Orderables typically have a set of fields the
editor needs to fill in and always generate the same design in your templates.
For the ProductDetailPage, you may want to show several images that
highlight your product. One way to do this is to create a ForeignKey to a
Wagtail Image object for every image you want. But if you want up to 15
images, that means selecting up to 15 different images, and if you want to
reorder them, well, too bad, because those are hard coded. This is where an
Orderable comes in. Because you want several images and may want to
rearrange those images so the first image is the best, you can create an
Orderable with a ForeignKey to an image and set a minimum to 1 and a
maximum of 15, and these restrictions will be put in place for you. And in
this scenario, your marketing team will likely want to rearrange the images
so the first few are the best, and possibly replace them entirely at a later date.
Now, I realize at first this is tricky to wrap our minds around. So let’s create
an Orderable and learn by writing code. Open your products/models.py file
and add the following code:
class ProductDetailPage(Page):
…
content_panels = Page.content_panels + [ #A
InlinePanel( #B
"product_images", #B
max_num=5, #B
min_num=1, #B
label="Product Images" #B
), #B
]
class ProductImages(Orderable):
page = ParentalKey('products.ProductDetailPage',
related_name="product_images") #C
image = models.ForeignKey( #D
"wagtailimages.Image", #D
null=True, #D
blank=True, #D
on_delete=models.SET_NULL, #D
related_name="+" #D
) #D
alt_text = models.CharField(max_length=100, blank=False,
default='') #D
short_description = models.CharField(max_length=255, blank=True)
#D
panels = [
FieldPanel("image"), #E
FieldPanel("alt_text"), #E
FieldPanel("short_description"), #E
]
Take a second to read through this. I strongly believe that great developers
are taught to read through code, even if they don’t understand it at first.
Once you’re finished reading, let’s carry on by breaking this down.
First off, there are a bunch of new imports at the top of your file such as
Django models, a ParentalKey, InlinePanel and an Orderable.
Note
Figure 5.4 shows the source code for an Orderable, taken directly from the
Wagtail source code on GitHub. As you can see an Orderable is simply a
Django model with an IntegerField that sorts itself by the number in the
IntegerField. As a developer, you simply need to extend a Python class using
this Orderable class to gain the advantages of sorting. Wagtail will generate
the proper user interface for your editors and handle saving your Orderable
instances in the right order.
Then there is an
image which is a standard ForeignKey to
wagtailimages.Image, two CharField’s that are standard Django fields, and
lastly there are panels. These aren’t content_panels; they are just normal
panels.
Tip
A panel is how you get a field to display in the Wagtail admin so your
editors can make changes. Also note that the sort_order field from the
Orderable does not appear.
A page holds different panel types, such as settings panels, promote panels,
and content panels (those are the three default panel types). That will make
your content editable in different tabs while editing your page. However, an
Orderable does not come with tabs on a page; it’s simply just content, so it’s
called panels and it takes a list of FieldPanels.
Speaking of FieldPanels, now is a good time to mention that you can add
fields that are not editable by your content editors. This can be a really
useful technique as a developer when you want to store data but don’t want
your editors to see it or change it. An example would be an
original_published_time to track the very first time a Wagtail page was
published. This could be setup to automatically store the data when the page
is published the first time, and cannot be seen or edited by anyone. To
prevent users from changing content on your page simply do not add it to
your list of content_panels. Figure 5.5 demonstrates how to ignore a field
so that the content editors cannot access it, but developers are able to save
information whenever they need it.
Figure 5.5 Custom Page model fields are hidden by default unless put into a FieldPanel.
When you save all of this work, make your migration file(s) and then apply
your migrations you’ll be able to edit a ProductDetailPage and see that you
can add product images. Try and save your page with zero images, and it
won’t save because the minimum was set to one. Try to add more than five,
and Wagtail won’t let you - again because the maximum was set to five. And
your page will look something along these lines, in figure 5.6, (minus my
custom content):
Notice the arrow at the top right of the new Orderable area. No more
copying and pasting – simply click a button and it will reorder the content
for you. This is a fantastic way to manage your content! It’s so much easier
than deleting content, copying content, pasting content and re-selecting
images. Plus, you get the added benefit of setting a minimum and maximum
number of Orderables the editors can add.
Tip
If you run into an error that says “ 'NoneType' object has no attribute
'_inc_path'” that means Django Tree Beard, a package Wagtail relies on to
create its hierarchy of pages, has run into a problem. To solve this run the
following command python manage.py fixtree.
Now you need to be able to display these orderables on your page. This
requires a template loop and Wagtails built-in image cropping feature. But
first, the template files themselves don’t exist yet. If you try to preview your
page you should run into a big scary page that says TemplateDoesNotExist
at the top. On this page it will tell you where Django is looking for your
template file. Typically, you have two primary options as to where you
should put this file:
products/templates/products/product_detail_page.html, or
templates/products/product_detail_page.html
It’s up to you where you want to create the file. I personally enjoy keeping
my app templates in the app folder that was created when I run python
manage.py startapp {myappname}.
Note
You can always tell Wagtail to look in a different place for your template file
by setting template = "folder/custom_template_name.html" in your
custom Page model.
If at any time you can’t figure out how to create a new template file in the
right location, stop and ready through the problem that Django shows you.
An example of the standard error page Django will display can be seen in
figure 5.7.
Figure 5.7 A typical TemplateDoesNotExist error, with highlighted sections to pay attention to.
In figure 5.7 the error page is telling me:
To solve this problem, double check your template file exists in one of the
directories Django is looking in, that you don’t have any typos in your
template filename, and if all else fails you can always try stopping and
restarting your local Django server in your command line.
Now this was just an example and does not reflect the actual code in this
chapter, but I wanted to make sure you know exactly how to fix this problem
when it comes up. And this problem will pop up at some point in your
development career.
{% extends "base.html" %}
{% load wagtailimages_tags %} #B
{% block content %}
{% for product_image in page.product_images.all %} #A
{% image product_image.image fill-250x250 as img %} #B
<div>
<img src="{{ img.url }}" alt="{{ product_image.alt_text }}"><br>
#C
{% if product_image.short_description %} #D
{{ product_image.short_description }} #D
{% endif %} #D
</div>
{% endfor %} #A
{% endblock content %}
I threw you into the deep end with this example. This is a fairly advanced
Orderable. But what’s cool is, when you edit your page, you can rearrange
your Orderables and preview your changes and then the images show up in a
different order.
So how does this fit in with the e-commerce website you’re creating? Great
question! Imagine yourself shopping on Amazon and you come across a
product that looks good but only has one image. Chances are you’re not
buying that product if there is only one image.
The way I have you setting this up, so far, is perfect for adding a carousel of
images to your Wagtail Page so potential buyers can scroll through the
images on your page and evaluate your product. Should you ever need to
replace an image in one spot, you can do that easily now. And should you
ever need to rearrange images, well, that’s easy too! Figure 5.8 shows an
example of four images in a row, but highlights the problem when you need
to move an image from the first location to a different location.
Figure 5.8 Four images in a row highlighting the difficulty of swapping the first and third
images.
Let’s say, just for fun, you have four images on a page that you want to
display, like in figure 5.8. You have a few options you can take advantage of,
each with their own pros and cons:
The second and third options are usually the best options for a feature like
this, where you may want to display more than one image. The second
option, using a StreamField, isn’t necessarily a bad option, however,
StreamFields are usually reserved for a mixed bag of content and not
typically used for just one kind of content that repeats itself. That’s where
the third option comes in: Orderables.
With an Orderable (and StreamFields) you are able to simply move content
up or down while editing your page, making your content easy to rearrange.
If you take one thing away from this section, consider this: A Wagtail
Orderable is a feature that sits between hard coding several of the same field
type, and a StreamField. They should be kept relatively simple, and ideally
they should not have too many fields. They are great for adding related links
when reading an article, similar blog posts when reading a blog, adding
images for a carousel, and other features that are adjacent to those.
If you are still wondering what the difference between a StreamField and an
Orderable is then carry on to the next section where I’ll outline creating a
few custom StreamField blocks. By the end of the next section you should
have a healthy understanding of both features and when to use them.
5.3 StreamField Blocks
StreamFields are a vital part of Wagtail, and always have been. Unlike
Orderables, StreamFields allow developers to create a mix-and-match style
of content and were invented before WordPress created their own version of
StreamFields. In most cases, the content of your page is made up of different
blocks, which are the individual components that make up a StreamField,
and the field itself is a Wagtail-created field called a StreamField.
In the figure 5.9 you will see several components in the wireframe layout of
a website. It has a header (sometimes called a hero), 5 reusable components
on the right, which we assigned as Orderables in the last section, and the
highlighted section for the primary content. The highlighted sections would
be a StreamField made of 5 blocks.
Figure 5.9 Highlights the area that a StreamField typically occupies on a website using multiple
blocks.
StreamFields, unlike Orderables, are made up of a more fluid type of
content, as highlighted in figure 5.9. Rather than strictly using the same
content repeatedly, you can create all sorts of content and have it occupy a
large area of your page. Technically, StreamFields can replace Orderables,
and I’ve seen that method used before where one page uses two or more
StreamFields. However, the correct way is to use StreamFields and
StreamField blocks to create more dynamic content, while using Orderables
for more structured (and repeatable) content.
StreamFields are also harder to query data from your database as they are
typically stored in a single column as JSONified text. Whereas Orderables,
on the other hand, have their own dedicated table in the database and the
data can be queried quite easily.
Thought experiment
If you had a popular product you were selling and wanted to link to other
related products (other Wagtail Pages), which implementation technique do
you think would be best to use?
Keep in mind you may have zero or more related products to display on any
given ProductDetailPage and you may want to promote one product more
than another.
class YourCustomPage(Page):
body = StreamField([
("text_block", blocks.CharBlock()),
("image_block", ImageChooserBlock()),
], null=True, blank=True)
There are two main methods to add blocks to a StreamField. The first way
was covered when you created a blog in chapter 3 of Wagtail CMS in Action.
These are quite basic, yet still powerful. However, what do you do when you
want to add an image and some text with it? Or how would you choose a
page to link to with custom text for the button? This is where the basic
implementation of StreamField blocks start to fall apart, and why there is a
second method to creating highly customized StreamField blocks.
Usually I start a new app for just my StreamField blocks. It’s not strictly
required, but that’s an option if you absolutely require the cleanest code base
that either you, or your organization, prefer to have. For this section I’ll
bypass creating a new Django app and create a file in the products/ folder
called blocks.py
# products/blocks.py
class TitleAndSubtitleBlock(blocks.StructBlock):
title = blocks.CharBlock(max_length=100, required=True)
subtitle = blocks.CharBlock(max_length=250, required=False)
class Meta:
template = "blocks/title_and_subtitle_block.html"
icon = "edit"
label = "Title & Text"
In the sample StructBlock there are two fields: a title and a subtitle. The title
field is required whereas the subtitle is not required.
Note
When working with StreamField blocks you will use the keyword required
to force the field to be filled out when saving a draft or publishing the page.
With a standard Page, much like a standard Django Model, you would use
blank=False. The difference is that Wagtail blocks use Django Forms for
validation.
There is also a subclass called Meta, where developers prefer to put most of
their StreamField block settings. I’ve added three Meta settings for a
template path, icon and label on the TitleAndSubtitleBlock class. The
template path is, just like a Page, the path to which template should be
rendered when this block is used. The icon and label will show up in the
Wagtail admin when adding a StreamField block - which isn’t possible
because StreamField itself isn’t hooked up to any of the pages yet.
# …
class ProductDetailPage(Page):
# …
body = StreamField([
("title_and_subtitle", blocks.TitleAndSubtitleBlock()), #A
], null=True, blank=True)
content_panels = Page.content_panels + [
…
FieldPanel("body"),
]
Now that you’ve added a new field to your ProductDetailPage, you’ll need
to make a new migration file and apply the migration changes to your
database. This step tells Django to create a new column called body in your
database table. Think of it like adding a new column to a spreadsheet.
Tip
If you ever feel like exploring your local SQLite database, the tables and the
columns in each table you can always download SQLite Browser for free at
https://sqlitebrowser.org/. This was a tool that really helped me understand
the connection between Page model fields and how the data is stored in the
database.
Figure 5.10 A spreadsheet as a database table concept holding three Wagtail pages.
In figure 5.10 you’ll notice one row with three columns, which is an over
simplified version of what your database will store for your HomePage.
There’s typically only one row for the HomePage because your website
typically only has one home page, though that may be false if you are using
Wagtail’s multisite feature. The rows in Figure 5.10 store the page data, and
the columns will map to the fields in your Wagtail Page models. It’s good to
note at this point that Wagtail Pages come with a lot of other fields and I’ve
simplified these graphics to help maintain your sanity.
In Listing 5.6 I’ve added a new field called body to the ProductDetailPage
class. Your code is going to think your database is set up with a new column
called body, as you can see in the example table in figure 5.11.
Figure 5.11 Django and Wagtail will interpret your code by assuming the body column exists in
your database.
When you look at the table in figure 5.11 you’ll see a highlighted column
called Body. As of right now, Django and Wagtail are making the
assumption that there is a column in your database called body because you
have written the code for it in your ProductDetailPage class. I’d love to say
that’s all you need to do, but that’d be a lie. Aside from writing the code in
your Page model, you also need to make a migration file and apply those
migrations. This will tell your database table that your code has new changes
and those changes need to be applied to your database.
When the fields in your Page models match the columns in your database
table (or in this example, the hypothetical spreadsheet), then Django and
Wagtail will be happy. Make sure you make those migration files and apply
them!
A good rule of thumb is: whenever you see the word Field in one of your
Wagtail Page models, you need to make and apply migrations.
Note
Whenever you add new blocks to your StreamField and try to run
migrations, Django will always create a new file for you. Technically, it’s not
making any changes to your database but it’s a core feature in Django to
detect changes and try to match it up with the database. But StreamFields are
stored as giant TextFields filled with JSON data and don’t need to be
changed. Keep those migration files anyway.
You can see the StreamField takes a list of tuples, each with two values. The
first value is the name of the block and the second value is the StructBlock
that was imported from products.blocks. If keeping track of Wagtail
blocks and your own blocks.py file gets confusing, feel free to rename your
blocks.py file - just remember to change your imports to reflect the filename
change.
The StreamField also takes null=True and blank=True. The null keyword
means the database can be completely empty, allowing you to avoid setting a
default value. And blank means a page can be saved with no blocks. Think
of blank in a model and required in a block as doing the same job: making
sure the field is filled out, or not filled out, when saving the page.
Lastly, you need to tell your template to render the StreamField. This is as
easy as adding two lines of code to your page template. Open up
product_detail_page.html and add:
Listing 5.7 Looping through a StreamField in your template
{% extends "base.html" %}
{% load wagtailcore_tags %} #A
{% block content %}
…
{% include_block page.body %} #A
{% endblock content %}
Keep in mind that you need to load a Python file in your template before you
can use a function from that file.
Refresh your page and you should see absolutely nothing at all from this
block. That’s good, but sometimes confusing. It’s because you have the
template file but there’s nothing in it. If you open the new
title_and_subtitle_block.html file and add the following code you’ll see the
title and optional subtitle appear in your page:
The last thing you need to do is style the block so that it looks good to you
and fits into your vision of this website. I’d also like to challenge you to
create more blocks this way. As a secondary challenge, you can also use any
of the built-in block types that come with Wagtail by inheriting the block
name rather than a StructBlock. It’s a bit cleaner to have your blocks exist as
classes rather than one-liners in your page body.
class URLBlock(blocks.URLBlock):
class Meta:
template = …
icon = …
label = …
Go ahead and try those out. StreamFields and blocks are a vital part of the
Wagtail experience, so it’s best to get familiar with them sooner rather than
later.
5.4 Adding product search
Adding search to a website is often a tricky task and, sometimes, a difficult
one. Luckily, Wagtail comes with a search app built into the website for us.
It makes a few assumptions such as the query keyword in the URL, wanting
to record the queries users made and paginating the page by 10 if there are
more than ten results.
Note
Pagination, or the act of paginating, is when you create multiple pages to get
results. Think of page 2, 3, 4, etc. when you Google something - that is
pagination. Wagtail uses Django’s built-in pagination feature.
If you started your new Wagtail website using the wagtail start …
command, you’ll be pleasantly surprised that there is a folder called search.
It comes with a views.py file, an __init__.py file to tell Python this is a
folder with Python files, along with a single search template. The urls.py file
that your site came with also has a path for search. All you need to do is
open http://127.0.0.1:8000/search/ (or http://localhost:8000/search/) and
you’ll see a page render, without any styling, that immediately gives you
search functionality for your entire website. Try typing in a keyword used by
one of your other pages, like blog or in my case: sticker. You can see an
example of the generic search page in figure 5.12.
Figure 5.12 The default search page that Wagtail comes with.
The default search page, as seen in figure 5.12, comes with your Wagtail
website is unstyled and very basic. That’s on purpose because Wagtail and
the maintainers of Wagtail don’t know your website design plans but wanted
to give you a sample search page to adapt for your own purposes.
Without doing any extra work you now have search functionality on your
website. But let’s not leave it here. Instead, I’d like to dive a little deeper into
Wagtail's search functionality, and briefly talk about some of the additional
capabilities you can enable with Wagtail search.
def search(request):
search_query = request.GET.get("query", None) #B
page = request.GET.get("page", 1) #C
# Search
if search_query:
search_results = Page.objects.live().search(search_query) #BD
query = Query.get(search_query) #B
# Record hit
query.add_hit() #B
else:
search_results = Page.objects.none() #D
# Pagination
paginator = Paginator(search_results, 10) #D
try:
search_results = paginator.page(page) #C
except PageNotAnInteger:
search_results = paginator.page(1) #C
except EmptyPage:
search_results = paginator.page(paginator.num_pages) #C
return TemplateResponse( #E
request, #E
"search/search.html", #E
{ #E
"search_query": search_query, #E
"search_results": search_results, #E
}, #E
) #E
There are quite a few things going on here that I’d like to explain. Most of it
is self-explanatory, but it’s good to, at least, read through this code as a bare
minimum.
Skipping past the imports and the search function definition, I’m jumping
straight into search_query = request.GET.get(...). When you see
request.GET.get(...), this is split functionality between Django and
Python. Django will store the GET request and any GET parameters in the
request.GET dictionary. The .get() portion is Python trying to get the
keyword you used to look up the item in the dictionary. In this case, it’s
saying the default is None if there is no query keyword in the URL.
Tip
GET requests come with query parameters, which are keywords in the URL
that map to a value. Such as mysite.com/search/?query=stickers&page=2.
This maps URL parameters to Python code that looks like query =
"stickers" and page = 2.
Note
The code is trying to paginate by the ?page= value in the URL, which
defaults to 1. If page value is not an integer, default to 1 - this is to deal with
malicious users. Lastly, if there is an empty page simply use the default
num_pages that were set.
Lastly, and this is new to you at this point, return the TemplateResponse from
a function based view. Up until now, everything you’ve worked with like
Pages and StreamField blocks have been class-based. Functions are perfectly
acceptable in Django and Wagtail, and in this case it’s preferred because the
functionality isn’t tied to a specific page type or model. The
TemplateResponse class is returned with three positional arguments: the
request, the template to use (as a string), and the dictionary of context items
to add to the page.
Under the hood the search page isn’t doing a lot. But what Wagtail does
under that, the code we don’t see, is quite amazing. Search is a very big topic
to go over, so we won’t go through all of it. But if your website is utilizing
search functionality to its fullest extent, I highly recommend spending a little
time today reading through the Wagtail search documentation found here:
https://docs.wagtail.org/en/stable/topics/search/index.html. Search is
powerful stuff, and Wagtail packs their search functionality full of amazing
features, like hit logging and promoted search results.
There’s one problem. This will query the entire database for all published
page types. If you wanted to adjust the search functionality to only search
through your ProductDetailPages, you could swap out Page.objects. for
ProductDetailPage.objects. in the search/views.py file. Don’t forget to
import your page type too.
I’d like to challenge you at this point. Can you implement this search
functionality into your ProductIndexPage? You are already listing products
in that template, but now you can add search to it. This is more Python-based
work than frontend work. Alternatively, you can take the search.html
template and put it into your base.html file for a global search feature and
this is going to be more frontend work than backend (Python) work.
Once you’re finished adding search to your website, make sure you add
some basic styling to it. As I spoke about when you were creating your blog,
creating a decent style for your features as you build them will help you
maintain your sanity. It doesn’t need to be perfect, but it should, at least,
look more than plain text.
5.5 Summary