In this article we will add a StreamField to a Wagtail page, allowing us to input text and inline images via the CMS editor.
Say you have installed Wagtail and have created a very basic home page. By using Wagtail's StreamField we can add text and images to pages in a very flexible way. We will create an article page model which will give an overview of all articles. The articles will be in reverse chronological order. Since the Wagtail docs contain a very good tutorial, we will directly dive in a bit deeper.
In models.py
add a model ArticlePage
(if you already have some imports, remove duplicates):
from django.db import models
from django.utils.translation import gettext_lazy as _
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, StreamFieldPanel
from wagtail.core import blocks
from wagtail.core.fields import RichTextField, StreamField
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtailtrans.models import TranslatablePage
from .blocks import InlineImageBlock
class ArticlePage(TranslatablePage):
intro = RichTextField(blank=True)
image = models.ForeignKey(
'wagtailimages.Image', blank=True, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_("Image")
)
featured = models.BooleanField(default=False)
body = StreamField([
('paragraph', blocks.RichTextBlock()),
('image', InlineImageBlock()),
])
content_panels = TranslatablePage.content_panels + [
FieldPanel('intro'),
FieldPanel('featured'),
ImageChooserPanel('image'),
StreamFieldPanel('body'),
]
The image field is not a required field, therefore blank=True, null=True
and on_delete=models.SET_NULL
. We set related_name
equal to '+'
, because we don't want to create a backwards relation from the image to the page. The featured
field will be used to select articles for our home page. StreamField
allows mixing text, images, videos, quotes, code snippets etc. in one entity. It consists of (name, block_type) tuples. Wagtail provides a lot of blocks out of the box. The RichTextBlock
is standard Wagtail.
The InlineImageBlock
is a custom block that we are going to create, using StructBlock
. Although it is not strictly necessary, it is wise to put your StructBlock
models in a separate file; Wagtail's reference demo site does it like that. So create a blocks.py
in your app and add the following content:
from django.utils.translation import gettext_lazy as _
from wagtail.core import blocks
from wagtail.core.blocks import CharBlock
from wagtail.images.blocks import ImageChooserBlock
class InlineImageBlock(blocks.StructBlock):
image = ImageChooserBlock(label=_("Image"))
caption = CharBlock(required=False, label=_("Caption"))
float = blocks.ChoiceBlock(
required=False,
choices=[('right', _("Right")), ('left', _("Left")), ('center', _("Center"))],
default='right',
label=_("Float"),
)
size = blocks.ChoiceBlock(
required=False,
choices=[('small', _("Small")), ('medium', _("Medium")), ('large', _("Large"))],
default='small',
label=_("Size"),
)
class Meta:
icon = 'image'
All human readable strings are translated using gettext_lazy
. image
and caption
speak for themselves. The field float
will be used in the template to position the image on the right, left or center of the page; the parameter size
works the same way. Both are ChoiceBlock
s. The class Meta
allows setting the icon argument. You can view a list of all available icons in the Wagtail styleguide; if you don't want to install that, search Wagtail StreamField icons on the web. For our purpose, image
will do.
The model is ready, time to create the template. As we have seen earlier with the home page, the name of the template should be article_page.html
. You can view the contents on Github, or alternatively watch the video in this article. The usual elements such as loading libraries, page title, intro, date are done as in home_page.html
. The only new elements are the following:
{% include "streamfield.html" %}
<a href="{{ page.get_parent.url }}">{% trans "Return to articles" %}</a>
i.e. a template streamfield.html
and a link to the parent page (which we will create below). Both are wrapped in a container to make them look nicer on the page. The file streamfield.html
will be used every time a block needs to be rendered and has the following content:
{% load wagtailcore_tags wagtailimages_tags %}
{% for block in page.body %}
{% if block.block_type == 'image' %}
<div class="block-{{ block.block_type }}-{{ block.value.float }}">
<!-- make use of specific properties of Wagtail 'image' tag -->
{% if block.value.size == "small" %}
{% image block.value.image width-240 class="img-fluid" %}
{% elif block.value.size == "medium" %}
{% image block.value.image width-480 class="img-fluid" %}
{% else %}
{% image block.value.image width-2400 class="img-fluid" %}
{% endif %}
{{ block.value.caption }}
</div>
{% else %}
<div class="block-{{ block.block_type }}">
{% include_block block %}
</div>
{% endif %}
{% endfor %}
A distinction is made between the different types of blocks: image and other. We reference the individual fields of the StructBlock
s via the value
property; same for the block_type
property. For images we make use of the image
template tag and image rendering options of Wagtail. The img-fluid
class of Bootstrap makes all images fit nicely in the viewport. The include_block
tag takes care of the rendering of all other (non-image) blocks.
It's time to add some styling to our CSS (for the first time in our project, for those of you who have been following along). The essential styling elements for the templates above are (I left out some that are straightforward, e.g. block-paragraph
, block-image-right
/left
, block-video-right
/left
, block-small
/medium
/large
, they are in the repository):
.img-fluid {
max-width: 100%;
height: auto;
}
/* for the images and text inside streamfields */
.block-image-center {
display: grid;
/* justify-content works here because Wagtail creates an image of fixed dimensions */
justify-content: center;
overflow: hidden;
font: italic 12px Georgia, serif;
}
Centering an image works best with justify-content
because Wagtail creates an image of fixed dimensions which then is centered within the div
with the justify-content
property.
Our article page is ready now. Now we create an index page containing all the articles that we intend to write, in our models.py
:
class ArticleIndexPage(TranslatablePage):
intro = RichTextField(blank=True)
# Specifies that only ArticlePage objects can live under this index page
subpage_types = ['ArticlePage']
# A method to access and reorder the children of the page (i.e. ArticlePage objects)
def articlepages(self):
return ArticlePage.objects.child_of(self).live().order_by('-first_published_at')
def featured_articlepages(self):
return self.articlepages().filter(featured=True)
content_panels = TranslatablePage.content_panels + [
FieldPanel('intro', classname='full'),
]
The parameter subpage_types
makes sure that we only create ArticlePage
instances under this index page. The method articlepages
selects the live child pages of this index page and reorders them anti-chronologically. The field first_published_at
is a standard field of the Page
model. The method featured_articlepages
is a subset: only those that are marked featured
for the home page. We use the method articlepages
in our template article_index_page.html
; the interesting part of the template is:
{% for childpage in page.articlepages %}
<div class="col-auto mb-3">
<div class="card article">
<a href="{{ childpage.url }}">
{% if childpage.specific.image %}
{% image childpage.specific.image fill-320x240 class="img-front rounded" %}
{% else %}
<img alt="" src="{% static 'images/transparent.png' %}" width="320" height="240" class="img-default rounded">
{% endif %}
<img alt="" src="{% static 'images/transparent.png' %}" width="320" height="240" class="img-background rounded">
<div class="card-img-overlay">
<h5 class="card-title">{{ childpage.title }}</h5>
<p class="card-subtitle">{{ childpage.specific.intro|striptags|safe|truncatewords:15 }}</p>
<p class="card-text small">{{ childpage.specific.first_published_at }}</p>
</div>
</a>
</div>
</div>
{% endfor %}
For each child page a link is created in the form of a 320x240 pixel image. We use Bootstrap's classes for cards, such as card-img-overlay
. On the image is the title of the page plus the first 15 words of the intro and the date. We use Django's filter striptags
to strip away any html tags in the title, the filter safe
to prevent escaping quotes in the text and the filter truncatewords
to truncate the intro after 15 words. As explained in the Wagtail docs we have to use the method specific
to reference the fields of the children of the ArticlePage
instance.
If the child page contains an image then this image is used, if not then the image images/transparent.png
is used. This is a fully transparent image (compare it to a glass sheet) that is easy to create or download; put it in the subdirectory images of cms/static/cms
and add this directory to STATICFILES_DIRS
in settings. By using the background-color
property we can turn it into any color; the class image-default
in our CSS settings will make it grey. The second image with the class img-background
is an overlay that changes color when hovering over it with the mouse. Image overlay is achieved by the combination of the position: relative
and position: absolute
declarations. We use Bootstrap's class rounded
to round the corner of the images. The CSS settings are as follows:
.article, .theme {
width: 320px;
height: 240px;
}
.img-front {
position: relative;
max-width: 100%;
}
.img-default {
position: relative;
max-width: 100%;
background-color: grey;
}
.img-background {
position: absolute;
max-width: 100%;
max-height: 100%;
left: 0;
background-color: navy;
opacity: 0;
transition: opacity 300ms;
}
a:hover .img-background {
opacity: 0.6;
}
Now we want to have a link to the article index page plus the featured articles on our home page. To the HomePage
model add:
article_section_title = models.CharField(
null=True,
blank=True,
max_length=255,
help_text=_("Title to display above the article section"),
)
article_section_intro = RichTextField(blank=True)
article_section = models.ForeignKey(
TranslatablePage,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text=_("Featured articles for the homepage"),
verbose_name=_("Article section"),
)
Add the imports MultiFieldPanel
and PageChooserPanel
and to the content_panels
of the HomePage
model add:
MultiFieldPanel([
FieldPanel('article_section_title'),
FieldPanel('article_section_intro', classname='full'),
PageChooserPanel('article_section'),
], heading=_("Article section"), classname='collapsible'),
This allows us to create an article section with a title, introduction and a link to a TranslatablePage
; in admin we will assign the article index page to this, so that we will have access to it in our template. In there, we add a piece of html which very much looks like what we have just created for our article_page.html
, only the loop over all articles is different:
{% for childpage in page.article_section.specific.featured_articlepages %}
{% endfor %}
We need to load wagtailimages_tags
and static
because we use Wagtail's image
tag and the image transparent.png
in our static directory.
Phew, we're done! Migrate the database to incorporate all the model changes. Then go to Wagtail's admin, to Pages and to our home page and create a child page with the model ArticleIndexPage
. Give it a title and type some text in the intro field; publish it. Then create a child page of this index page (it will automatically use the model ArticlePage
), add text, upload and insert images, and publish. Create another article page. View the image links on the home page and index page. Obviously feel free to change the styling.
With this the basis for putting content in our site is done. You can read more on how to embed video, add themes and navigation.
Comment on this article (sign in first or confirm by name and email below)