In this tutorial we'll explain how you create a Wagtail page model, how to include a custom field and then finally how to build out the react frontend.
It consists of three parts:
This document also provides info on the following topics:
The project backend, based on Wagtail and the CMS Wagtail is located in /src
, here's an overview of its content:
├── customdocument # Extends Wagtails Document model
├── customimage # Extends Wagtails Image model
├── customuser # Extends the Django User model
├── main # The primary app where we store models and pages
│ ├── blocks # Put your custom block/stream field blocks here
│ ├── factories # Put your test factories
│ ├── middlewares # Application middlewares
│ ├── migrations # Database migrations
│ ├── mixins.py # Put your model and view mixins here
│ ├── models.py # Put your Django models here
│ ├── pages # Put your Wagtail pages here
│ ├── serializers.py # Put your non-Wagtail page serializers here
│ ├── templates # Put all your templates here
│ ├── tests # Put your tests here
│ └── views # Put your Django and DRF views here
├── manage.py # Django admin tool with addons for loading .env files
├── nextjs # Enables communication between Next.js and Wagtail
├── pipit # This is a bootstrap app that contains settings, translations and routing
│ ├── context_processors.py # Provides templates with values
│ ├── locale # Translation files
│ ├── management # Contains Pipit specific management commands
│ ├── settings # Contains all app settings
│ │ ├── base.py # Put all your mandatory configuration here
│ │ ├── local.py # Put your local configuration here
│ │ ├── prod.py # Put your production configuration here
│ │ ├── stage.py # Put your stage configuration here
│ │ └── test.py # Put your test configuration here
│ ├── templates # Contains Pipit specific templates (like the page scaffolder)
│ ├── test_runner.py # Lets us use pytest as default test runner
│ ├── urls.py # The entrypoint for all routing
│ ├── wagtail_hooks.py # Contains pipit specific Wagtail overrides
│ └── wsgi.py # Default Django wsgi configuration
├── pytest.circleci.ini # Custom Pytest configuration for Circle CI
├── pytest.ini # Pytest configuration
├── requirements # Contains pip requirements
│ ├── base.txt # Put any mandatory requirements here (example wagtail)
│ ├── local.txt # Put local environment requirements here (example django-debug-toolbar)
│ ├── prod.txt # Put production only requirements here (example boto)
│ ├── stage.txt # Put stage only requirements here (example boto)
│ └── test.txt # Put requirements only used in test here (example factory-boy)
├── sitesettings # Contains site customizations
├── utils # Contains global python utility functions
Make sure you follow the install instructions in the project README.md, in short, it boils down to this:
- Set up env vars:
cp docker/config/python.example.env docker/config/python.env
- Include the domain in your hosts file.
127.0.0.1 blog.acme.com.test
To run docker, run docker-compose up
from the project root (the folder contains a docker-compose.yml
file).
This will create the following docker containers:
├── web # Contains the web server we make requests to, it will either direct requests to the python container or our locally running Next.js application
├── python # Contains python and runs the django runserver development server
├── db # Contains PostgreSQL with the PostGIS extension
When all the containers are running, open your browser and navigate to either:
http://blog.acme.com.test:8081/wt/cms
to access the Wagtail adminhttp://blog.acme.com.test:8081/wt/admin
to access the Django admin.
/wt
is short for /WagTail
and is where the Django/Wagtail app is hosted. Requests to anything else (such as /
or /my-very-excellent-path
) are passed to our Next.js app. The proxy server in the web
container does this for us.
Start by generating a new page in Wagtail, we have a management command to simplify the process called new_page
.
docker-compose exec python ./manage.py new_page --name=About
This will create the following files:
main
├── factories
│ ├── about_page.py
├── pages
│ ├── about.py
│ ├── about_serializer.py
├── tests
│ ├── test_about_page.py
Contains factory-boy factories for the page model, we use it to simplify data creation when testing.
The code that creates the about page Wagtail model.
Holds logic for transforming the page model into json data, used by our Next.js frontend.
Tests to make sure the data transformation is done correctly. It's also a good place to put future business logic tests related to the page model.
Modify main/pages/about.py
and include company_name
both as a model field and as a content panel, it should look like this:
from django.db import models
from django.utils.translation import gettext_lazy as _
from wagtail.admin.panels import FieldPanel
from wagtail_headless_preview.models import HeadlessPreviewMixin
from .base import BasePage
class AboutPage(HeadlessPreviewMixin, BasePage):
company_name = models.CharField(
max_length=250,
blank=True,
null=True,
verbose_name=_("Company name"),
)
content_panels = BasePage.content_panels + [
FieldPanel("company_name"),
]
extra_panels = BasePage.extra_panels
serializer_class = "main.pages.AboutPageSerializer"
class Meta:
verbose_name = _("About")
After adding our field we need to create a new database migration.
docker-compose exec python ./manage.py makemigrations
We also need to run the migration so database changes are applied.
docker-compose exec python ./manage.py migrate
Now login to the Wagtail cms at http://blog.acme.com.test:8081/wt/cms
using:
Username: admin
Password: admin
Then choose to create a new page of type "About" by going to your Home Page at http://blog.acme.com.test:8081/wt/cms/pages/3/
and pressing "Add subpage".
You should see a company_field
in the admin.
Name the page "My about page" and in the field "Company Name" write "Acme Inc", then hit publish.
Modify main/tests/test_about_page.py
and include this test case.
def test_that_company_name_are_retuned(self):
page = AboutPageFactory.create(title="About", company_name="Acme", parent=self.root_page)
data = page.get_component_data({})
self.assertEqual(data["component_props"]["company_name"], "Acme")
Now run it.
docker-compose exec python pytest
Oh no - It fails. But that's all right, that means we need to add company_name
to our serializer.
Modify main/pages/about_serializer.py
from .base_serializer import BasePageSerializer
from . import AboutPage
class AboutPageSerializer(BasePageSerializer):
class Meta:
model = AboutPage
fields = [
"company_name",
] + BasePageSerializer.Meta.fields
Now run the tests again.
docker-compose exec python pytest
Tests pass.
This is pretty much it on the backend, the serializer is invoked any time a request is made to our page and will transform it to json.
If you are curious on how the data that will be served to the frontend will look like, do the following:
curl 'http://blog.acme.com.test:8081/wt/api/nextjs/v1/page_by_path/?html_path=/my-about-page'
{
"component_name": "AboutPage",
"component_props": {
"company_name": "Acme Inc",
"title": "My about page",
...
}
}
A couple of things are going on here, the api will retrive the page by path and return enough information for our frontend to know which container to use and what data it should contain.
We are now done with the backend and are halfway there, now it's time to add frontend. The frontend steps are described in more detail frontend-developer-guide.md, so we won't go into too much explanation here, but rather what commands and what code to write.
First make sure you have your frontend installed:
cd frontend
npm i
Then create a container component representing our About page by using our cli:
npm run new:container AboutPage
Now modify the newly created container `containers/AboutPage/AboutPage.js" and include our new field in the container component.
import React, { PureComponent } from 'react';
// import i18n from '../../i18n';
import PropTypes from 'prop-types';
import { basePageWrap } from '../BasePage';
import s from './AboutPage.module.css';
class AboutPage extends PureComponent {
state = {};
static defaultProps = {
companyName: '',
};
static propTypes = {
companyName: PropTypes.string,
};
render() {
const { companyName } = this.props;
return (
<div className={s['AboutPage']}>
<p>Company name: {companyName}</p>
</div>
);
}
}
export default basePageWrap(AboutPage);
Then, to expose your container to Next.js you also need to put your container in our container register: containers/LazyContainers.js
import dynamic from 'next/dynamic';
export default {
// Other containers
...
AboutPage: dynamic(() => import('./AboutPage')),
};
The last step is to start your Next.js app (npm run dev
) and open http://blog.acme.com.test:8081/my-about-page
You should now have a working page here that displays Acme Inc as a company name.
$ ./manage.py new_page --name=name
This command scaffolds a Wagtail page and generates a model, serializer, test factory and a set of tests.
Options:
- name: This is the name for your page. Do not include "Page" in the name as it will be included as a suffix. Example "About" will generate the model "AboutPage".
Although a Wagtail page is also a model, we have taken the decision to separate models and page models, because of this they live in their own directory.