Co-authored-by: Thibaud Colas <thibaudcolas@gmail.com>
Also includes:
- Further tone updates based on Vale
- Address other feedback to earlier parts of the tutorial
Wagtail is an open-source content management system (CMS) that is built on [Django](https://www.djangoproject.com/), a popular Python web framework. It has gained popularity among developers and content editors for its powerful features and intuitive interface, providing a streamlined editing experience. Wagtail offers a comprehensive toolkit for content creation and management, including a rich text editor with formatting options, image and document management, version control, workflows and content scheduling.
Wagtail is an open-source content management system (CMS) that's built on [Django](https://www.djangoproject.com/), a popular Python web framework. It has gained popularity among developers and content editors for its powerful features and intuitive interface, providing a streamlined editing experience. Wagtail offers a comprehensive toolkit for content creation and management, including a rich text editor with formatting options, image and document management, version control, workflows, and content scheduling.
Developers appreciate Wagtail's highly customizable and modular architecture, which includes built-in support for Django's app structure. This allows them to easily create and integrate custom functionality, making Wagtail suitable for projects of any size. Wagtail excels in handling complex content structures, offering features like hierarchical page organization, robust search capabilities, and content localization.
Wagtail stands out as the preferred choice for tens of thousands of organizations globally, including renowned names like Google, NASA, and the British NHS. It has proven scalability, capable of handling high volumes of traffic from millions of visitors every month. What sets Wagtail apart is its ability to extend beyond traditional content management, providing seamless integration with data tools and rich data visualizations.
Wagtail stands out as the preferred choice for tens of thousands of organizations globally, including renowned names like Google, the National Aeronautics and Space Administration (NASA), and the British National Health Service (NHS). It has proven scalability and is capable of handling high volumes of traffic from millions of visitors every month. What sets Wagtail apart is its ability to extend beyond traditional content management, providing seamless integration with data tools and rich data visualizations.
For more information about Wagtail and the guiding principles for building websites with it, read [the Zen of Wagtail](the_zen_of_wagtail).
For more information about Wagtail and the guiding principles for building websites with it, read [The Zen of Wagtail](the_zen_of_wagtail).
Add a `STATIC_ROOT` setting, if your project does not have one already:
Add a `STATIC_ROOT` setting, if your project doesn't have one already:
```python
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
```
Add `MEDIA_ROOT` and `MEDIA_URL` settings, if your project does not have these already:
Add `MEDIA_ROOT` and `MEDIA_URL` settings, if your project doesn't have these already:
```python
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
@ -78,21 +78,15 @@ urlpatterns = [
]
```
The URL paths here can be altered as necessary to fit your project's URL scheme.
You can alter URL paths here to fit your project's URL scheme.
`wagtailadmin_urls` provides the admin interface for Wagtail. This is separate from the Django admin interface (`django.contrib.admin`); Wagtail-only projects typically host the Wagtail admin at `/admin/`, but if this would clash with your project's existing admin backend then an alternative path can be used, such as `/cms/` here.
`wagtailadmin_urls` provides the [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface) for Wagtail. This is separate from the Django admin interface, `django.contrib.admin`. Wagtail-only projects host the Wagtail admin at `/admin/`, but if this clashes with your project's existing admin backend then you can use an alternative path, such as `/cms/`.
`wagtaildocs_urls` is the location from where document files will be served. This can be omitted if you do not intend to use Wagtail's document management features.
Wagtail serves your document files from the location, `wagtaildocs_urls`. You can omit this if you do not intend to use Wagtail's document management features.
`wagtail_urls` is the base location from where the pages of your Wagtail site will be served. In the above example, Wagtail will handle URLs under `/pages/`, leaving the root URL and other paths to be handled as normal by your Django project. If you want Wagtail to handle the entire URL space including the root URL, this can be replaced with:
Wagtail serves your pages from the `wagtail_urls` location. In the above example, Wagtail handles URLs under `/pages/`, leaving your Django project to handle the root URL and other paths as normal. If you want Wagtail to handle the entire URL space including the root URL, then place `path('', include(wagtail_urls))` at the end of the `urlpatterns` list. Placing `path('', include(wagtail_urls))` at the end of the `urlpatterns` ensures that it doesn't override more specific URL patterns.
```python
path('', include(wagtail_urls)),
```
In this case, this should be placed at the end of the `urlpatterns` list, so that it does not override more specific URL patterns.
Finally, your project needs to be set up to serve user-uploaded files from `MEDIA_ROOT`. Your Django project may already have this in place, but if not, add the following snippet to `urls.py`:
Finally, you need to set up your project to serve user-uploaded files from `MEDIA_ROOT`. Your Django project may already have this in place, but if not, add the following snippet to `urls.py`:
Note that this only works in development mode (`DEBUG = True`); in production, you will need to configure your web server to serve files from `MEDIA_ROOT`. For further details, see the Django documentation: [Serving files uploaded by a user during development](https://docs.djangoproject.com/en/stable/howto/static-files/#serving-files-uploaded-by-a-user-during-development) and [Deploying static files](django:howto/static-files/deployment).
Note that this only works in development mode (`DEBUG = True`); in production, you have to configure your web server to serve files from `MEDIA_ROOT`. For further details, see the Django documentation: [Serving files uploaded by a user during development](https://docs.djangoproject.com/en/stable/howto/static-files/#serving-files-uploaded-by-a-user-during-development) and [Deploying static files](django:howto/static-files/deployment).
With this configuration in place, you are ready to run `python manage.py migrate` to create the database tables used by Wagtail.
## User accounts
Wagtail uses Django’s default user model by default. Superuser accounts receive automatic access to the Wagtail admin interface; use `python manage.py createsuperuser` if you don't already have one. Custom user models are supported, with some restrictions; Wagtail uses an extension of Django's permissions framework, so your user model must at minimum inherit from `AbstractBaseUser` and `PermissionsMixin`.
Wagtail uses Django’s default user model by default. Superuser accounts receive automatic access to the Wagtail [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface); use `python manage.py createsuperuser` if you don't already have one. Wagtail supports custom user models with some restrictions. Wagtail uses an extension of Django's permissions framework, so your user model must at minimum inherit from `AbstractBaseUser` and `PermissionsMixin`.
## Start developing
You're now ready to add a new app to your Django project (via `python manage.py startapp` - remember to add it to `INSTALLED_APPS` in your settings.py file) and set up page models, as described in [Your first Wagtail site](/getting_started/tutorial).
You're now ready to add a new app to your Django project through `python manage.py startapp`. Remember to add the new app to `INSTALLED_APPS` in your settings.py file and set up page models, as described in [Your first Wagtail site](/getting_started/tutorial).
Note that there's one small difference when not using the Wagtail project template: Wagtail creates an initial homepage of the basic type `Page`, which does not include any content fields beyond the title. You'll probably want to replace this with your own `HomePage` class - when you do so, ensure that you set up a site record (under Settings / Sites in the Wagtail admin) to point to the new homepage.
Note that there's one small difference when you're not using the Wagtail project template: Wagtail creates an initial homepage of the basic type `Page`, which doesn't include any content fields beyond the title. You probably want to replace this with your own `HomePage` class. If you do so, ensure that you set up a site record (under Settings / Sites in the Wagtail admin) to point to the new homepage.
Wagtail has been born out of many years of experience building websites, learning approaches that work and ones that don't, and striking a balance between power and simplicity, structure and flexibility. We hope you'll find that Wagtail is in that sweet spot. However, as a piece of software, Wagtail can only take that mission so far - it's now up to you to create a site that's beautiful and a joy to work with. So, while it's tempting to rush ahead and start building, it's worth taking a moment to understand the design principles that Wagtail is built on.
Wagtail is born out of many years of experience building websites, learning approaches that work and ones that don't, and striking a balance between power and simplicity, structure and flexibility. We hope you find that Wagtail is in that sweet spot. However, as a piece of software, Wagtail can only take that mission so far. It's up to you to create a site that's beautiful and a joy to work with. So, while it's tempting to rush ahead and start building, it's worth taking a moment to understand the design principles that Wagtail is built on.
In the spirit of ["The Zen of Python"](https://www.python.org/dev/peps/pep-0020/), The Zen of Wagtail is a set of guiding principles, both for building websites in Wagtail, and for the ongoing development of Wagtail itself.
## Wagtail is not an instant website in a box.
You can't make a beautiful website by plugging off-the-shelf modules together - expect to write code.
You can't make a beautiful website by plugging off-the-shelf modules together. You should expect to write code.
## Always wear the right hat.
The key to using Wagtail effectively is to recognise that there are multiple roles involved in creating a website: the content author, site administrator, developer and designer. These may well be different people, but they don't have to be - if you're using Wagtail to build your personal blog, you'll probably find yourself hopping between those different roles. Either way, it's important to be aware of which of those hats you're wearing at any moment, and to use the right tools for that job. A content author or site administrator will do the bulk of their work through the Wagtail admin interface; a developer or designer will spend most of their time writing Python, HTML or CSS code. This is a good thing: Wagtail isn't designed to replace the job of programming. Maybe one day someone will come up with a drag-and-drop UI for building websites that's as powerful as writing code, but Wagtail is not that tool, and does not try to be.
The key to using Wagtail effectively is to recognize that there are multiple roles involved in creating a website: the content author, site administrator, developer and designer. These may well be different people, but they don't have to be. If you're using Wagtail to build your personal blog, you'll probably find yourself hopping between those different roles. Either way, it's important to be aware of which of those hats you're wearing at any moment, and to use the right tools for that job. A content author or site administrator does the bulk of their work through the Wagtail [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface). A developer or designer spends most of their time writing Python, HTML or CSS code. This is a good thing. Wagtail isn't designed to replace the job of programming. Maybe one day someone will come up with a drag-and-drop UI for building websites that's as powerful as writing code, but Wagtail is not that tool, and does not try to be.
A common mistake is to push too much power and responsibility into the hands of the content author and site administrator - indeed, if those people are your clients, they'll probably be loudly clamouring for exactly that. The success of your site depends on your ability to say no. The real power of content management comes not from handing control over to CMS users, but from setting clear boundaries between the different roles. Amongst other things, this means not having editors doing design and layout within the content editing interface, and not having site administrators building complex interaction workflows that would be better achieved in code.
A common mistake is to push too much power and responsibility into the hands of the content author and site administrator. Indeed, if those people are your clients, they are probably loudly clamouring for that. However, the success of your site depends on your ability to say no. The real power of content management comes not from handing control over to CMS users, but from setting clear boundaries between the different roles. Amongst other things, this means not having editors doing design and layout within the content editing interface, and not having site administrators building complex interaction workflows that you can better achieve in code.
## A CMS should get information out of an editor's head and into a database, as efficiently and directly as possible.
Whether your site is about cars, cats, cakes or conveyancing, your content authors will be arriving at the Wagtail admin interface with some domain-specific information they want to put up on the website. Your aim as a site builder is to extract and store this information in its raw form - not one particular author's idea of how that information should look.
Whether your site is about cars, cats, cakes or conveyancing, your content authors will be arriving at the Wagtail [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface) with some domain-specific information they want to put up on the website. Your aim as a site builder is to extract and store this information in its raw form, and not one particular author's idea of how that information should look.
Keeping design concerns out of page content has numerous advantages. It ensures that the design remains consistent across the whole site, not subject to the whims of editors from one day to the next. It allows you to make full use of the informational content of the pages - for example, if your pages are about events, then having a dedicated "Event" page type with data fields for the event date and location will let you present the events in a calendar view or filtered listing, which wouldn't be possible if those were just implemented as different styles of heading on a generic page. Finally, if you redesign the site at some point in the future, or move it to a different platform entirely, you can be confident that the site content will work in its new setting, and not be reliant on being formatted a particular way.
Keeping design concerns out of page content has numerous advantages. It ensures that the design remains consistent across the whole site, not subject to the whims of editors from one day to the next. It allows you to make full use of the informational content of the pages. For example, if your pages are about events, then having a dedicated "Event" page type with data fields for the event date and location allows you to present the events in a calendar view or filtered listing, which wouldn't be possible if those were just implemented as different styles of heading on a generic page. Finally, if you redesign the site at some point in the future, or move it to a different platform entirely, you can be confident that the site content will work in its new setting, and not be reliant on being formatted in a particular way.
Suppose a content author comes to you with a request: "We need this text to be in bright pink Comic Sans". Your question to them should be "Why? What's special about this particular bit of text?" If the reply is "I just like the look of it", then you'll have to gently persuade them that it's not up to them to make design choices. (Sorry.) But if the answer is "it's for our Children's section", then that gives you a way to divide the editorial and design concerns: give your editors the ability to designate certain pages as being "the Children's section" (through tagging, different page models, or the site hierarchy) and let designers decide how to apply styles based on that.
Suppose a content author comes to you with a request: "We need this text to be in bright pink Comic Sans". Your question to them should be "Why? What's special about this particular bit of text?" If the reply is "I just like the look of it", then you'll have to gently persuade them that it's not up to them to make design choices. But if the answer is "it's for our Children's section", then that gives you a way to divide the editorial and design concerns. Then you can give your editors the ability to designate certain pages as being "the Children's section" through tagging, different page models, or the site hierarchy, and let designers decide how to apply styles based on that.
## The best user interface for a programmer is usually a programming language.
@ -28,6 +28,6 @@ A common sight in content management systems is a point-and-click interface to l

It looks nice in the sales pitch, but in reality, no CMS end-user can realistically make that kind of fundamental change - on a live site, no less - unless they have a programmer's insight into how the site is built, and what impact the change will have. As such, it will always be the programmer's job to negotiate that point-and-click interface - all you've done is taken them away from the comfortable world of writing code, where they have a whole ecosystem of tools, from text editors to version control systems, to help them develop, test and deploy their code changes.
While the sales pitch may make it appear appealing, the truth is that the average user of a content management system (CMS) would find it practically impossible to make such a fundamental change. This holds especially true for a live website, as it requires a deep understanding of programming and an awareness of the potential consequences of the change. As such, it will always be the programmer's job to negotiate that point-and-click interface. All you've done is take them away from the comfortable world of writing code, where they have a whole ecosystem of tools, from text editors to version control systems, to help them develop, test, and deploy their code changes.
Wagtail recognises that most programming tasks are best done by writing code, and does not try to turn them into box-filling exercises when there's no good reason to. Likewise, when building functionality for your site, you should keep in mind that some features are destined to be maintained by the programmer rather than a content editor, and consider whether making them configurable through the Wagtail admin is going to be more of a hindrance than a convenience. For example, Wagtail provides a form builder to allow content authors to create general-purpose data collection forms. You might be tempted to use this as the basis for more complex forms that integrate with (for example) a CRM system or payment processor - however, in this case there's no way to edit the form fields without rewriting the backend logic, so making them editable through Wagtail has limited value. More likely, you'd be better off building these using Django's form framework, where the form fields are defined entirely in code.
Wagtail recognizes that most programming tasks are best done by writing code, and doesn't try to turn them into box-filling exercises when there's no good reason to. Likewise, when building functionality for your site, you should keep in mind that some features are destined to be maintained by the programmer rather than a content editor, and consider whether making them configurable through the Wagtail [Admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface) is going to be more of a hindrance than a convenience. For example, Wagtail provides a form builder to allow content authors to create general-purpose data collection forms. You might be tempted to use this as the basis for more complex forms that integrate with a CRM system or payment processor for instance. However, in this case, there's no way to edit the form fields without rewriting the backend logic. So making them editable through Wagtail has limited value. More likely, you'd be better off building these using Django's form framework, where the form fields are defined entirely in code.
This tutorial shows you how to build a blog using Wagtail. This tutorial gives you hands-on experience with some of Wagtail's features.
This tutorial shows you how to build a blog using Wagtail. Also, the tutorial gives you hands-on experience with some of Wagtail's features.
A basic knowledge of Python programming and the Django framework will help you follow this tutorial.
To complete this tutorial, we recommend that you have some basic programming knowledge, as well as an understanding of web development concepts. A basic understanding of Python and the Django framework ensures a more grounded understanding of this tutorial, but it's not mandatory.
```{note}
If you'd like to add Wagtail to an existing Django project instead, see [](integrating_into_django).
If you want to add Wagtail to an existing Django project instead, see [](integrating_into_django).
```
## Install and run Wagtail
@ -14,7 +14,7 @@ If you'd like to add Wagtail to an existing Django project instead, see [](integ
Wagtail supports Python 3.7, 3.8, 3.9, 3.10, and 3.11.
To check if you have an appropriate version of Python 3, run the following commmand:
To check if you have an appropriate version of Python 3, run the following command:
```sh
python --version
@ -24,29 +24,26 @@ python3 --version
py --version
```
If these commands do not return a version number, or return a version lower than 3.7, then [install Python 3](https://www.python.org/downloads/).
```{note}
Before installing Wagtail, it's necessary to install the **libjpeg** and **zlib** libraries, which provide support for working with JPEG, PNG, and GIF images through the Python **Pillow** library.
The way to do this varies by platform. See Pillow's
If none of the preceding commands return a version number, or return a version lower than 3.7, then [install Python 3](https://www.python.org/downloads/).
(virtual_environment_creation)=
### Create and activate a virtual environment
We recommend using a virtual environment, which isolates installed dependencies from other projects.
This tutorial recommends using a virtual environment, which isolates installed dependencies from other projects.
This tutorial uses [`venv`](https://docs.python.org/3/tutorial/venv.html), which is packaged with Python 3.
**On Windows** (cmd.exe), run the following commands:
```doscon
py -m venv mysite\env
# Then:
# then
mysite\env\Scripts\activate.bat
# If mysite\env\Scripts\activate.bat does not work, run:
# if mysite\env\Scripts\activate.bat doesn't work, run:
mysite\env\Scripts\activate
```
@ -67,7 +64,7 @@ You must exclude the `env` directory from any version control.
### Install Wagtail
Use pip, which is packaged with Python, to install Wagtail and its dependencies:
To install Wagtail and its dependencies, use pip, which is packaged with Python:
```sh
pip install wagtail
@ -75,11 +72,9 @@ pip install wagtail
### Generate your site
Wagtail provides a `start` command similar to `django-admin startproject`.
Running `wagtail start mysite` in your project generates a new `mysite` folder with a few Wagtail-specific extras, including the required project settings, a "home" app with a blank `HomePage` model and basic templates, and a sample "search" app.
Wagtail provides a `start` command similar to `django-admin startproject`. Running `wagtail start mysite` in your project generates a new `mysite` folder with a few Wagtail-specific extras, including the required project settings, a "home" app with a blank `HomePage` model and basic templates, and a sample "search" app.
Because the folder `mysite` was already created by `venv`, run
`wagtail start` with an additional argument to specify the destination directory:
Because the folder `mysite` was already created by `venv`, run `wagtail start` with an additional argument to specify the destination directory:
```sh
wagtail start mysite mysite
@ -100,10 +95,6 @@ mysite/
<!-- Generated with: tree -a -L 1 -F -I env mysite -->
```{note}
Generally, in Wagtail, each page type, or content type, is represented by a single app. However, different apps can be aware of each other and access each other's data. All of the apps need to be registered within the `INSTALLED_APPS` section of the `base.py` file in the `mysite/settings` directory. Look at this file to see how the `start` command has listed them in there.
```
### Install project dependencies
```sh
@ -111,8 +102,8 @@ cd mysite
pip install -r requirements.txt
```
This ensures that you have the relevant versions of Wagtail, Django, and any other dependencies for the project that you create.
The `requirements.txt` file contains all the dependencies needed in order to run the project.
This ensures that you have the relevant versions of Wagtail, Django, and any other dependencies for the project that you've just created.
The `requirements.txt` file contains all the dependencies needed to run the project.
### Create the database
@ -122,7 +113,7 @@ By default, your database is SQLite. To match your database tables with your pro
python manage.py migrate
```
This command ensures that the tables in your database match the models in your project. Every time you alter your model, for example, if you add a field to a model, then you must run the `python manage.py migrate` command to update your database.
This command ensures that the tables in your database match the models in your project. Every time you alter your model, then you must run the `python manage.py migrate` command to update the database. For example, if you add a field to a model, then you must run the command.
### Create an admin user
@ -130,7 +121,7 @@ This command ensures that the tables in your database match the models in your p
python manage.py createsuperuser
```
This will prompt you to create a new admin user account with full permissions. It's important to note that for security reasons, the password text won’t be visible while typing.
This prompts you to create a new admin user account with full permissions. It's important to note that for security reasons, the password text won’t be visible while typing.
### Start the server
@ -140,10 +131,10 @@ python manage.py runserver
After the server starts, go to <http://127.0.0.1:8000> to see Wagtail’s welcome page:


```{note}
This tutorial uses `http://127.0.0.1:8000` as the URL for your development server but depending on your setup, this could be a different IP address or port. Please read the console output of `manage.py runserver` to determine the correct url for your local site.
This tutorial uses `http://127.0.0.1:8000` as the URL for your development server but depending on your setup, this could be a different IP address or port. Please read the console output of `manage.py runserver` to determine the correct URL for your local site.
```
You can now access the [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface) by logging into <http://127.0.0.1:8000/admin> with the username and password that you entered while creating an admin user with `createsuperuser`.
@ -173,8 +164,7 @@ class HomePage(Page):
```
`body` is a `RichTextField`, a special Wagtail field. When `blank=True`,
it means the field is not required and you can leave it empty. You can use any of the [Django core fields](https://docs.djangoproject.com/en/stable/ref/models/fields). `content_panels` define the capabilities and the layout of the editing interface. Adding fields to `content_panels` enables you to edit them in the Wagtail [Admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface).
[You can read more about this on Page models](../topics/pages).
it means the field isn't mandatory and you can leave it empty. You can use any of the [Django core fields](https://docs.djangoproject.com/en/stable/ref/models/fields). `content_panels` define the capabilities and the layout of the editing interface. Adding fields to `content_panels` enables you to edit them in the Wagtail [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface). You can read more about this on [Page models](../topics/pages).
You **must** run the above commands each time you make changes to the model definition. Here is the expected output from the terminal:
You must run the preceding commands each time you make changes to the model definition. Here is the expected output from the terminal:
```txt
Migrations for 'home':
@ -198,7 +188,7 @@ Running migrations:
Applying home.0003_homepage_body... OK
```
You can now edit the homepage within the Wagtail [Admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface) (on the side bar go to **Pages** and click edit beside **Homepage**) to see the new body field.
You can now edit the homepage within the Wagtail [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface). On your [Sidebar](https://guide.wagtail.org/en-latest/how-to-guides/find-your-way-around/#the-sidebar), go to **Pages** and click edit beside **Home** to see the new body field.

@ -207,7 +197,7 @@ Enter the text "Welcome to our new site!" into the body field, and publish the p
You must update the page template to reflect the changes made
to the model. Wagtail uses normal Django templates to render each page
type. By default, it looks for a template filename formed from the app and model name,
separating capital letters with underscores. For example, HomePage within the 'home' app becomes
separating capital letters with underscores. For example, `HomePage` within the "home" app becomes
`home/home_page.html`. This template file can exist in any location that
[Django's template rules](https://docs.djangoproject.com/en/stable/intro/tutorial03/#write-views-that-actually-do-something) recognize. Conventionally, you can place it within a `templates` folder within the app.
@ -231,12 +221,12 @@ Edit `home/templates/home/home_page.html` to contain the following:
Also, you must load `wagtailcore_tags` at the top of the template and provide additional tags to those provided by Django.


### Wagtail template tags
In addition to Django's [template tags and filters](django:ref/templates/builtins),
Wagtail provides a number of its own [template tags & filters](template_tags_and_filters)
Wagtail provides a number of its own [template tags & filters](template_tags_and_filters),
which you can load by including `{% load wagtailcore_tags %}` at the top of
your template file.
@ -255,8 +245,7 @@ Produces:
```
**Note:** You must include `{% load wagtailcore_tags %}` in each
template that uses Wagtail's tags. Django throws a `TemplateSyntaxError`
if the tags aren't loaded.
template that uses Wagtail's tags. If the tags aren't loaded, Django throws a `TemplateSyntaxError`.
## A basic blog
@ -282,9 +271,13 @@ INSTALLED_APPS = [
]
```
```{note}
You must register all apps within the `INSTALLED_APPS` section of the `base.py` file in the `mysite/settings` directory. Look at this file to see how the `start` command lists your project’s apps.
```
### Blog index and posts
Let's start with creating a simple index page for our blog. Edit `blog/models.py` to include:
Start with creating a simple index page for your blog. Edit `blog/models.py` to include:
```python
from django.db import models
@ -303,15 +296,15 @@ class BlogIndexPage(Page):
]
```
A new model has been added, so we need to create and run a database migration:
Since you added a new model to your app, you must create and run a database migration:
```sh
python manage.py makemigrations
python manage.py migrate
```
Since the model is called`BlogIndexPage`, the default template name,
unless overridden, is `blog_index_page.html`. Django looks for a template whose name matches the name of your Page model within the templates directory in your blog app folder. You can override this default behaviour if want to. To create a template for the
Also, since the model name is`BlogIndexPage`, the default template name,
unless you override it, is `blog_index_page.html`. Django looks for a template whose name matches the name of your Page model within the templates directory in your blog app folder. You can override this default behaviour if you want to. To create a template for the
`BlogIndexPage` model, create a file at the location `blog/templates/blog/blog_index_page.html`.
```{note}
@ -341,10 +334,11 @@ In your `blog_index_page.html` file enter the following content:
{% endblock %}
```
Most of this should be familiar from our previous work with the `home_page.html` template, but we'll explain `get_children` a bit later.
If you have a Django background, then you can notice that the `pageurl` tag is similar to Django's `url` tag, but takes a Wagtail Page object as an additional argument.
Other than using `get_children`, the preceding `blog_index_page.html` template is similar to your previous work with the `home_page.html` template. You will learn about the use of `get_children` later in the tutorial.
Now you can create a new page. Here is how you can create one from the Wagtail admin interface:
If you have a Django background, then you will notice that the `pageurl` tag is similar to Django's `url` tag, but takes a Wagtail Page object as an additional argument.
Now that this is complete, here is how you can create a page from the Wagtail [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface):
1. Go to <http://127.0.0.1:8000/admin> and sign in with your admin user details.
2. In the Wagtail [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface), go to Pages, then click Home.
@ -352,16 +346,15 @@ Now you can create a new page. Here is how you can create one from the Wagtail a
4. Choose **Blog index page** from the list of the page types.
5. Use "Our Blog" as your page title, make sure it has the slug "blog" on the Promote tab, and publish it.
You can now access the url, <http://127.0.0.1:8000/blog> on your site. This gives you an error page showing "TemplateDoesNotExist" because you are yet to create a template for the new page. Also, note how the slug from the Promote tab defines the page URL.
You can now access the URL, <http://127.0.0.1:8000/blog> on your site. This gives you an error page showing "TemplateDoesNotExist" because you are yet to create a template for the new page. Also, note how the slug from the Promote tab defines the page URL.
Now create a model and template for your blog posts. Edit `blog/models.py` to include:
```python
from django.db import models
from wagtail.models import Page
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel
# add this:
from wagtail.search import index
@ -421,18 +414,22 @@ URL of the blog this post is a part of.
Now, go to your [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface) and create a few blog posts as children of `BlogIndexPage` by following these steps:
1. Click **Pages** from the Wagtail [sidebar](https://guide.wagtail.org/en-latest/how-to-guides/find-your-way-around/#the-sidebar), and then click **Home**
1. Click **Pages** from the Wagtail [Sidebar](https://guide.wagtail.org/en-latest/how-to-guides/find-your-way-around/#the-sidebar), and then click **Home**
2. Hover on **Our blog** and click **Add child page**.

3. Select the page type, **Blog page**.
Select the page type, **Blog page**:

4. Populate the fields with content of your choice. To add a link, hightlight the text you want to attach the link to. You can now see a pop-up modal which has several actions represented by their icons. Click on the appropriate icon to add a link. You can also click the **+** icon, which appears at the left-hand side of the RichText `Body` field to get similar actions as those shown in the pop-up modal. To add an image, press enter to move to the next line the RichText `Body` field. Then click the **+** icon, which appears at the left-hand side of the RichText `Body` field. Select **Image** from the list of actions to add an image.
Populate the fields with the content of your choice:
.

To add a link from your rich text **Body** field, highlight the text you want to attach the link to. You can now see a pop-up modal which has several actions represented by their icons. Click on the appropriate icon to add a link. You can also click the **+** icon, which appears at the left-hand side of the field to get similar actions as those shown in the pop-up modal.
To add an image, press enter to move to the next line in the field. Then click the **+** icon and select **Image** from the list of actions to add an image.
```{NOTE}
Wagtail gives you full control over the kind of content you can create under
@ -440,17 +437,16 @@ various parent content types. By default, any page type can be a child of any
other page type.
```
5. Publish each blog post when you are done editing.
Publish each blog post when you are done editing.
Congratulations! You now have the beginning of a working blog.
Go to <http://127.0.0.1:8000/blog> and you can see all the posts that you created by following the previous steps:
Congratulations! You now have the beginnings of a working blog. If you go to
<http://localhost:8080/blog> in your browser, you can see all the posts that you created by following the preceding steps:

Titles should link to post pages, and a link back to the blog's
homepage should appear in the footer of each post page.
Titles should link to post pages, and a link back to the blog's homepage should appear in the footer of each post page.
### Parents and Children
### Parents and children
Much of the work in Wagtail revolves around the concept of _hierarchical tree structures_ consisting of nodes and leaves. You can read more on this [Theory](../reference/pages/theory). In this case, the `BlogIndexPage` serves as a _node_, and individual `BlogPage` instances represent the _leaves_.
@ -465,19 +461,15 @@ Take another look at the guts of `blog_index_page.html`:
```
Every "page" in Wagtail can call out to its parent or children
from its own position in the hierarchy. But why do you have to
from its position in the hierarchy. But why do you have to
specify `post.specific.intro` rather than `post.intro`?
This has to do with the way you define your model:
`class BlogPage(Page):`
The `get_children()` method gets you a list of instances of the `Page` base class.
This has to do with the way you define your model, `class BlogPage(Page)`. The `get_children()` method gets you a list of instances of the `Page` base class.
When you want to reference properties of the instances that inherit from the base class,
Wagtail provides the `specific` method that retrieves the actual `BlogPage` record.
While the "title" field is present on the base `Page` model, "intro" is only present
on the `BlogPage` model, so you need `.specific` to access it.
on the `BlogPage` model. So you need `.specific` to access it.
To simplify template code like this, use the Django `with` tag:
You can simplify the template code by using the Django `with` tag. Now, modify your `blog_index_page.html`:
```html+django
{% for post in page.get_children %}
@ -489,7 +481,7 @@ To simplify template code like this, use the Django `with` tag:
{% endfor %}
```
When you start writing more customised Wagtail code, you'll find a whole set of QuerySet
When you start writing more customized Wagtail code, you'll find a whole set of QuerySet
modifiers to help you navigate the hierarchy.
```python
@ -506,28 +498,26 @@ somepage.get_descendants()
somepage.get_siblings()
```
For more information, see: [Page QuerySet reference](../reference/pages/queryset_reference)
For more information, see [Page QuerySet reference](../reference/pages/queryset_reference)
### Overriding Context
With a keen eye, you may have noticed problems with the `Our blog` page:
1. Our blog orders the post in chronological order, generally blogs display content in _reverse_ chronological order.
2. All content is currently displayed, we want to make sure only _published_ content is displayed.
1. `Our blog` orders posts in chronological order. Generally blogs display content in _reverse_ chronological order.
2. `Our blog` displays all content. You want to make sure that it displays only _published_ content.
To accomplish these, you need to do more than grab the index
page's children in the template. Instead, you want to modify the
QuerySet in the model definition. Wagtail makes this possible via
the overridable `get_context()` method.
Modify your `BlogIndexPage` model like this:
Modify your `BlogIndexPage` model:
```python
class BlogIndexPage(Page):
intro = RichTextField(blank=True)
# add the get_context method:
def get_context(self, request):
# Update context to include only published posts, ordered by reverse-chron
context = super().get_context(request)
@ -538,28 +528,26 @@ class BlogIndexPage(Page):
# ...
```
Here is a quick breakdown of the changes we made:
Here is a quick breakdown of the changes that you made:
1. Retrieve the original context.
2. Create a custom QuerySet modifier.
3. Add it to the retrieved context.
4. Return the modified context back to the view.
1. You retrieved the original context.
2. You created a custom QuerySet modifier.
3. You added the custom QuerySet modifier to the retrieved context.
4. You returned the modified context to the view.
You also need to modify your `blog_index_page.html` template slightly. Change:
`{% for post in page.get_children %}` to `{% for post in blogpages %}`
Now, unpublish one of your posts. The unpublished post should disappear from your blog's index page. The remaining posts should be sorted with the most recently published posts coming first.
Now, unpublish one of your posts. The unpublished post should disappear from your blog's index page. Also, the remaining posts should now be sorted with the most recently published posts coming first.
### Images
Let's add the ability to attach an image gallery to your blog posts. While it's possible to simply insert images into the rich text `body` field, there are several advantages to setting up our gallery images as a new dedicated object type within the database. This way, you have full control over the layout and styling of the images on the template, rather than having to lay them out in a particular way within the rich text field. It also makes it possible for you to use the images elsewhere, independently of the blog text. For example, displaying a thumbnail on the blog's index page.
The next feature that you need to add is the ability to attach an image gallery to your blog posts. While it's possible to simply insert images into the rich text `body` field, there are several advantages to setting up your gallery images as a new dedicated object type within the database. This way, you have full control over the layout and styling of the images on the template, rather than having to lay them out in a particular way within the field. It also makes it possible for you to use the images elsewhere, independently of the blog text. For example, displaying a thumbnail on the blog's index page.
Add a new `BlogPageGalleryImage` model to `blog/models.py`:
Now modify your `BlogPage` model and add a new `BlogPageGalleryImage` model to `blog/models.py`:
```python
from django.db import models
# New imports added for ParentalKey, Orderable, InlinePanel
@ -605,21 +594,19 @@ class BlogPageGalleryImage(Orderable):
Run `python manage.py makemigrations` and `python manage.py migrate`.
There are a few new concepts here, so let's take them one at a time:
There are a few new concepts here:
1. Inheriting from `Orderable` adds a `sort_order` field to the model, to keep track of the ordering of images in the gallery.
1. Inheriting from `Orderable` adds a `sort_order` field to the model to keep track of the ordering of images in the gallery.
2. The `ParentalKey` to `BlogPage` is what attaches the gallery images to a specific page. A `ParentalKey` works similarly to a `ForeignKey`, but also defines `BlogPageGalleryImage` as a "child" of the `BlogPage` model, so that it's treated as a fundamental part of the page in operations like submitting for moderation, and tracking revision history.
3. `image` is a `ForeignKey` to Wagtail's built-in `Image` model, which stores the actual images. This appears in the page editor as a pop-up interface for choosing an existing image or uploading a new one. This way, you allow an image to exist in multiple galleries. This creates a many-to-many relationship between pages and images.
4. Specifying `on_delete=models.CASCADE` on the foreign key means that if the image is deleted from the system, the gallery entry is deleted as well. (In other situations, it might be appropriate to leave the entry in place - for example, if an "our staff" page included a list of people with headshots, and one of those photos was deleted, we'd rather leave the person in place on the page without a photo. In this case, we'd set the foreign key to `blank=True, null=True, on_delete=models.SET_NULL`.)
4. Specifying `on_delete=models.CASCADE` on the foreign key means that deleting the image from the system also deletes the gallery entry. In other situations, it might be appropriate to leave the gallery entry in place. For example, if an "our staff" page includes a list of people with headshots, and you delete one of those photos, but prefer to leave the person in place on the page without a photo. In this case, you must set the foreign key to `blank=True, null=True, on_delete=models.SET_NULL`.
5. Finally, adding the `InlinePanel` to `BlogPage.content_panels` makes the gallery images available on the editing interface for `BlogPage`.
After editing `blog/models.py` you should see a _Gallery images_ field with the option to upload images and provide a caption for it when editing a blog page in your Wagtail [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface).
After editing your `blog/models.py`, you should see **Images** in your [Sidebar](https://guide.wagtail.org/en-latest/how-to-guides/find-your-way-around/#the-sidebar) and a **Gallery images** field with the option to upload images and provide a caption for it in the [Edit Screen](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#edit-screen) of your blog posts.
Edit your blog page template `blog_page.html` to include the images section:
```html+django
{% extends "base.html" %}
<!-- Load the wagtailimages_tags: -->
{% load wagtailcore_tags wagtailimages_tags %}
@ -652,16 +639,14 @@ Here, you use the `{% image %}` tag, which exists in the `wagtailimages_tags` li

Since your gallery images are database objects in their own right, you can now query and re-use them independently of the blog post body. Now, define a `main_image` method, which returns the image from the first gallery item or `None` if no gallery items exist:
Since your gallery images are database objects in their own right, you can now query and re-use them independently of the blog post body. Now, define a `main_image` method in your `BlogPage` model, which returns the image from the first gallery item or `None` if no gallery items exist:
```python
class BlogPage(Page):
date = models.DateField("Post date")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
# Add the main_image method:
def main_image(self):
gallery_item = self.gallery_images.first()
if gallery_item:
@ -682,7 +667,7 @@ class BlogPage(Page):
]
```
This method is now available from our templates. Update `blog_index_page.html` to include the main image as a thumbnail alongside each post:
This method is now available from your templates. Update `blog_index_page.html` to load the `wagtailimages_tag` and include the main image as a thumbnail alongside each post:
```html+django
<!-- Load wagtailimages_tags: -->
@ -693,6 +678,7 @@ This method is now available from our templates. Update `blog_index_page.html` t
{% with post=post.specific %}
<h2><ahref="{% pageurl post %}">{{ post.title }}</a></h2>
<!-- Add this: -->
{% with post.main_image as main_image %}
{% if main_image %}{% image main_image fill-160x100 %}{% endif %}
{% endwith %}
@ -705,45 +691,46 @@ This method is now available from our templates. Update `blog_index_page.html` t
(tutorial_categories)=
### Categories
### Authors
Let's add a category system to our blog. Unlike tags, where a page author can bring a tag into existence simply by using it on a page, our categories will be a fixed list, managed by the site owner through a separate area of the admin interface.
You probably want your blog posts to have authors, which is an essential feature of blogs. The way to go about this is to have a fixed list, managed by the site owner through a separate area of the [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface).
First, we define a `BlogCategory` model. A category is not a page in its own right, and so we define it as a standard Django `models.Model` rather than inheriting from `Page`. Wagtail introduces the concept of "snippets" for reusable pieces of content that need to be managed through the admin interface, but do not exist as part of the page tree themselves; a model can be registered as a snippet by adding the `@register_snippet` decorator. All the field types we've used so far on pages can be used on snippets too - here we'll give each category an icon image as well as a name. Add to `blog/models.py`:
First, define an `Author` model. This model isn't a page in its own right. You have to define it as a standard Django `models.Model` rather than inheriting from `Page`. Wagtail introduces the concept of **Snippets** for reusable pieces of content which don't exist as part of the page tree themselves. You can manage snippets through the [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface). You can register a model as a snippet by adding the `@register_snippet` decorator. Also, you can use all the fields types that you've used so far on pages on snippets too.
To create Authors and give each author an author image as well as a name, add the following to `blog/models.py`:
```python
# Add this to the top of your blog/models.py file
# Add this to the top of the file
from wagtail.snippets.models import register_snippet
# ... Keep BlogIndexPage, BlogPage, BlogPageGalleryImage, and then add the BlogCategory category:
# ... Keep BlogIndexPage, BlogPage, BlogPageGalleryImage models, and then add the Author model:
@register_snippet
class BlogCategory(models.Model):
class Author(models.Model):
name = models.CharField(max_length=255)
icon = models.ForeignKey(
author_image = models.ForeignKey(
'wagtailimages.Image', null=True, blank=True,
on_delete=models.SET_NULL, related_name='+'
)
panels = [
FieldPanel('name'),
FieldPanel('icon'),
FieldPanel('author_image'),
]
def __str__(self):
return self.name
class Meta:
verbose_name_plural = 'blog categories'
verbose_name_plural = 'Authors'
```
```{note}
Note that we are using `panels` rather than `content_panels` here - since snippets generally have no need for fields such as slug or publish date, the editing interface for them is not split into separate 'content' / 'promote' / 'settings' tabs as standard, and so there is no need to distinguish between 'content panels' and 'promote panels'.
Note that you are using `panels` rather than `content_panels` here. Since snippets generally have no need for fields such as slug or publish date, the editing interface for them is not split into separate 'content' / 'promote' / 'settings' tabs. So there is no need to distinguish between 'content panels' and 'promote panels'.
```
Migrate this change by running `python manage.py makemigrations` and `python manage.py migrate`. Create a few categories through the Snippets area which now appears in the admin menu.
Migrate this change by running `python manage.py makemigrations` and `python manage.py migrate`. Create a few authors through the **Snippets** area which now appears in your Wagtail [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface).
We can now add categories to the `BlogPage` model, as a many-to-many field. The field type we use for this is `ParentalManyToManyField` - this is a variant of the standard Django `ManyToManyField` which ensures that the chosen objects are correctly stored against the page record in the revision history, in much the same way that `ParentalKey` replaces `ForeignKey` for one-to-many relations.To add categories to the `BlogPage`, modify `models.py` in your blog app folder:
You can now add authors to the `BlogPage` model, as a many-to-many field. The field type to use for this is `ParentalManyToManyField`. This field is a variation of the standard Django `ManyToManyField` that ensures the selected objects are properly associated with the page record in the revision history. It operates in a similar manner to how `ParentalKey` replaces `ForeignKey` for one-to-many relations. To add authors to the `BlogPage`, modify `models.py` in your blog app folder:
```python
# New imports added for forms and ParentalManyToManyField, and MultiFieldPanel
@ -751,23 +738,25 @@ from django import forms
from django.db import models
from modelcluster.fields import ParentalKey, ParentalManyToManyField
from wagtail.models import Page, Orderable
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
# ... modify your BlogPage model
from wagtail.search import index
from wagtail.snippets.models import register_snippet
Here we're making use of the `widget` keyword argument on the `FieldPanel` definition to specify a checkbox-based widget instead of the default multiple select box, as this is often considered more user-friendly.
In the preceding model modification, you used the `widget` keyword argument on the `FieldPanel` definition to specify a more user-friendly checkbox-based widget instead of the default multiple select boxes. Also, you used a `MultiFieldPanel` in `content_panels` to group the `date` and `Authors` fields together for readability.
Finally, update the `blog_page.html` template to display the categories:
Finally, migrate your database by running `python manage.py makemigrations` and `python manage.py migrate`. After migrating your database, update the `blog_page.html` template to display the Authors:
<p><ahref="{{ page.get_parent.url }}">Return to blog</a></p>
{% endblock %}
```

Now go to your [admin interface](https://guide.wagtail.org/en-latest/concepts/wagtail-interfaces/#admin-interface), in the [Sidebar](https://guide.wagtail.org/en-latest/how-to-guides/find-your-way-around/#the-sidebar), you can see the new **Snippets** option. Click this to create your authors. After creating your authors, go to your blog posts and add authors to them. Clicking on your blog posts from your blog index page should now give you a page similar to this image:
### Tag Posts

### Tag posts
Let's say you want to let editors "tag" their posts, so that readers can, for example,
view all bicycle-related content together. For this, we'll need to invoke
view all bicycle-related content together. For this, you have to invoke
the tagging system bundled with Wagtail, attach it to the `BlogPage`
model and content panels, and render linked tags on the blog post template.
Of course, we'll need a working tag-specific URL view as well.
Of course, you'll also need a working tag-specific URL view as well.
First, alter `models.py` once more:
@ -826,9 +834,7 @@ from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
from wagtail.search import index
# ... Keep the definition of BlogIndexPage
# ... Keep the definition of BlogIndexPage model and add a new BlogPageTag model
class BlogPageTag(TaggedItemBase):
content_object = ParentalKey(
'BlogPage',
@ -837,21 +843,21 @@ class BlogPageTag(TaggedItemBase):
Run `python manage.py makemigrations` and `python manage.py migrate`.
Summarising the changes:
The changes you made can be summarized as follows:
- New `modelcluster` and `taggit` imports
- Addition of a new `BlogPageTag` model, and a `tags` field on `BlogPage`.
@ -890,15 +896,12 @@ To render tags on a `BlogPage`, add this to `blog_page.html`:
{% endwith %}
```
Notice that we're linking to pages here with the builtin `slugurl`
tag rather than `pageurl`, which we used earlier. The difference is that `slugurl` takes a
Page slug (from the Promote tab) as an argument. `pageurl` is more commonly used because it
is unambiguous and avoids extra database lookups. But in the case of this loop, the Page object
isn't readily available, so we fall back on the less-preferred `slugurl` tag.
Notice that you're linking to pages here with the builtin `slugurl`
tag rather than `pageurl`, which you used earlier. The difference is that `slugurl` takes a `Page` slug (from the Promote tab) as an argument. `pageurl` is more commonly used because it's unambiguous and avoids extra database lookups. But in the case of this loop, the `Page` object isn't readily available, so you fall back on the less-preferred `slugurl` tag.
With the modifications we've made so far, visiting a blog post with tags will display a series of linked buttons at the bottom, one for each tag associated with the post. However, clicking on a button will result in a 404 error page, as we have not yet defined a "tags" view.
With the modifications that you've made so far, visiting a blog post with tags displays a series of linked buttons at the bottom, one for each tag associated with the post. However, clicking on a button will result in a **404** error page, as you are yet to define a "tags" view.
Return to `blog/models.py` and add:
Return to `blog/models.py` and add a new `BlogTagIndexPage` model:
```python
class BlogTagIndexPage(Page):
@ -921,10 +924,10 @@ Wagtail ecosystem, so that you can give it a title and URL in the
admin, and so that you can manipulate its contents by returning
a QuerySet from its `get_context()` method.
Migrate this by running `python manage.py makemigrations` and then `python manage.py`. After migrating the new changes, create a new `BlogTagIndexPage` in the admin interface. To create the `BlogTagIndexPage`, follow the same process you followed in creating the `BlogIndexPage` and give it the slug "tags" on the Promote tab. This means the `BlogTagIndexPage` is a child of the home page and parallel to `Our Blog` in the admin interface
Migrate this by running `python manage.py makemigrations` and then `python manage.py`. After migrating the new changes, create a new `BlogTagIndexPage` in the admin interface. To create the `BlogTagIndexPage`, follow the same process you followed in creating the `BlogIndexPage` and give it the slug "tags" on the Promote tab. This means the `BlogTagIndexPage` is a child of the home page and parallel to `Our Blog` in the admin interface.
Access `/tags` and Django will tell you what you probably already knew:
you need to create a template `blog/template/blog/blog_tag_index_page.html`:
Access `/tags` and Django will tell you what you probably already knew.
You need to create the template, `blog/templates/blog/blog_tag_index_page.html` and add the following content to it:
```html+django
{% extends "base.html" %}
@ -941,9 +944,6 @@ you need to create a template `blog/template/blog/blog_tag_index_page.html`:
@ -953,14 +953,9 @@ you need to create a template `blog/template/blog/blog_tag_index_page.html`:
{% endblock %}
```
We're calling the built-in `latest_revision_created_at` field on the `Page`
model - handy to know this is always available.
In the preceding `blog_tag_index_page.html` template, you're calling the built-in `latest_revision_created_at` field on the `Page` model. It's handy to know this is always available.
We haven't yet added an "author" field to our `BlogPage` model, nor do we have
a Profile model for authors - we'll leave those as an exercise for the reader.
Clicking the tag button at the bottom of a BlogPost should now render a page
something like this:
Clicking the tag button at the bottom of a blog post renders a page like this:
