Wagtail Tutorial Series:
To get the latest learning resource for Wagtail 4, please check Build Blog With Wagtail CMS (4.0.0)
- Create Wagtail Project
- Dockerizing Wagtail App
- Add Blog Models to Wagtail
- How to write Wagtail page template
- Add Bootstrap Theme to Wagtail
- How to use StreamField in Wagtail
- Wagtail Routable Page
- Add pagination component to Wagtail
- Customize Wagtail Page URL
- Add Full Text Search to Wagtail
- Add Markdown Support to Wagtail
- Add LaTeX Support & Code Highlight In Wagtail
- How to Build Form Page in Wagtail
- How to Create and Manage Menus in Wagtail
- Wagtail SEO Guide
- Source code: https://github.com/AccordBox/wagtail-tailwind-blog
Wagtail Tips:
- Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
- Wagtail Tip #2: How to Export & Restore Wagtail Site
Write style in Wagtail:
- How to use SCSS/SASS in your Django project (Python Way)
- How to use SCSS/SASS in your Django project (NPM Way)
Other Wagtail Topics:
Objectives
By the end of this chapter, you should be able to:
- Understand how
StreamField
works - Use
StreamField
to storebody
value of the PostPage. - Write template to display the
StreamField
What is StreamField
StreamField
provides a flexible way for us to construct content.
The StreamField
is a list which contains the value
and type
of the sub-blocks (we will see it in a bit). You can use the built-in block shipped with Wagtail or you can create your custom block.
Some block can also contains sub-block so you can use it to create a complex nested data structure, which is powerful
.
Block
From my understanding, I'd like to group the Wagtail built-in blocks in this way.
- Basic block, which is similar with Django model field types For example,
CharBlock
,TextBlock
,ChoiceBlock
- Chooser Block, which is for
object selection
. For example,PageChooserBlock
,ImageChooserBlock
. StructBlock
, which works likedict
(Object injs
), which containsfixed
sub-blocks.StreamBlock
,ListBlock
, which works likelist
(Arrays injs
), which containsno-fixed
sub-blocks.
Body
Next, let's use StreamField
to define the PostPage.body
It is recommended to put blocks in a separate file to keep your model clean.
blog/blocks.py
class ImageText(StructBlock):
reverse = BooleanBlock(required=False)
text = RichTextBlock()
image = CustomImageChooserBlock()
class BodyBlock(StreamBlock):
h1 = CharBlock()
h2 = CharBlock()
paragraph = RichTextBlock()
image_text = ImageText()
image_carousel = ListBlock(ImageChooserBlock())
thumbnail_gallery = ListBlock(ImageChooserBlock())
ImageText
inherits fromStructBlock
, it has three sub-blocks, we can only set values toreverse
,text
andimage
.BodyBlock
inherits fromStreamBlock
, we can add more than one sub-blocks becauseStreamBlock
behaves like list.
Update blog/models.py
from wagtail.core.fields import StreamField
from .blocks import BodyBlock
class PostPage(Page):
header_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
body = StreamField(BodyBlock(), blank=True)
tags = ClusterTaggableManager(through="blog.PostPageTag", blank=True)
content_panels = Page.content_panels + [
ImageChooserPanel("header_image"),
InlinePanel("categories", label="category"),
FieldPanel("tags"),
StreamFieldPanel("body"),
]
- import
BodyBlock
from./blocks
- Define body
body = StreamField(BodyBlock(), blank=True)
- Remember to update
content_panels
so you can edit in Wagtail admin.
Migrate the db
$ docker-compose run --rm web python manage.py makemigrations
$ docker-compose run --rm web python manage.py migrate
Now login Wagtail admin and add some content to the body.
Dive Deep
Let's run some code to learn more about StreamField and Django shell.
# please run code in new Django shell if you change something
$ docker-compose run --rm web python manage.py shell
>>> from blog.models import PostPage
>>> page = PostPage.objects.first()
>>> page.body.stream_data
[{'type': 'h1', 'value': 'The Zen of Wagtail', 'id': '7dd3beab-4fe9-49f9-8137-832379ed6252'}, {'type': 'paragraph', 'value': '<p>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.</p>', 'id': 'f21647d5-a393-4f48-b627-0598002618a0'}, {'type': 'image_carousel', 'value': [2, 1], 'id': '817f9fd9-c9e7-4153-a1d6-9904011c7081'}, {'type': 'image_text', 'value': {'reverse': False, 'text': '<p>Wagtail is not an instant website in a box.</p><p>You can’t make a beautiful website by plugging off-the-shelf modules together - expect to write code.</p>', 'image': 3}, 'id': 'cfa116ef-566a-47c9-9bd3-06e18a1ba70c'}, {'type': 'image_text', 'value': {'reverse': True, 'text': '<p><b>A CMS should get information out of an editor’s head and into a database, as efficiently and directly as possible.</b></p>', 'image': 1}, 'id': '70fec173-10a5-4711-a848-b33dbf25fa78'}]
# let's make the data look more clear
>>> import pprint
>>> pprint.pprint(page.body.stream_data)
[{'id': '7dd3beab-4fe9-49f9-8137-832379ed6252',
'type': 'h1',
'value': 'The Zen of Wagtail'},
{'id': 'f21647d5-a393-4f48-b627-0598002618a0',
'type': 'paragraph',
'value': '<p>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.</p>'},
{'id': '817f9fd9-c9e7-4153-a1d6-9904011c7081',
'type': 'image_carousel',
'value': [2, 1]},
{'id': 'cfa116ef-566a-47c9-9bd3-06e18a1ba70c',
'type': 'image_text',
'value': {'image': 3,
'reverse': False,
'text': '<p>Wagtail is not an instant website in a box.</p><p>You '
'can’t make a beautiful website by plugging off-the-shelf '
'modules together - expect to write code.</p>'}},
{'id': '70fec173-10a5-4711-a848-b33dbf25fa78',
'type': 'image_text',
'value': {'image': 1,
'reverse': True,
'text': '<p><b>A CMS should get information out of an editor’s '
'head and into a database, as efficiently and directly as '
'possible.</b></p>'}}]
>>> exit()
- For basic block, the
value
is usually number and string. - For chooser block, the
value
is theprimary key
of the selected object. - For
StructBlock
, thevalue
is a Python dict - For
StreamBlock
andListBlock
, thevalue
is a Python List.
I also recommend you to run some code on your local env and check the data. This can help better understand the data structures of SteramField.
Templates
Next, let's try to display the StreamField value in the Django template.
Update wagtail_bootstrap_blog/templates/blog/post_page.html
{% extends "base.html" %}
{% load wagtailcore_tags wagtailimages_tags blogapp_tags %}
{% block content %}
{% image page.header_image original as header_image %}
<img src="{{ header_image.url }}" class="img-fluid">
<h1>{{ page.title }}</h1>
<p>
{% post_categories_list %}
</p>
{# body #}
{% include "blog/components/streamfield.html" %}
<hr>
{% post_tags_list %}
<hr>
{% endblock %}
Notes:
- Here we include
blog/components/streamfield.html
to display the body value.
Create wagtail_bootstrap_blog/templates/blog/components/streamfield.html
{% load static wagtailcore_tags %}
{% with blocks=page.body %}
{% for block in blocks %}
{% if block.block_type == 'h1' %}
<h1>{{ block.value }}</h1>
{% elif block.block_type == 'h2' %}
<h2>{{ block.value }}</h2>
{% elif block.block_type == 'paragraph' %}
{{ block.value|richtext }}
{% elif block.block_type == 'image_text' %}
{% include 'blog/blocks/image_text.html' with block=block only %}
{% elif block.block_type == 'image_carousel' %}
{% include 'blog/blocks/image_carousel.html' with block=block only %}
{% else %}
<section class="block-{{ block.block_type }}">
{{ block }}
</section>
{% endif %}
{% endfor %}
{% endwith %}
Notes:
- Here we use Django for-loop to iterate the
body
, and retuen different HTML based on theblock_type
- The
only
option in theinclude
template tag, would make sure no other context variables are available in the template Django include - Please note I did not use
include_block
or{{ block }}
here and I will explain in a bit.
Create wagtail_bootstrap_blog/templates/blog/blocks/image_text.html
{% load wagtailcore_tags wagtailimages_tags %}
<div class="py-4">
<div class="align-items-center row {% if block.value.reverse %}flex-row-reverse{% endif %}">
<div class="col-md-5 col-12">
<div>
{{ block.value.text|richtext }}
</div>
</div>
<div class="col-md-7 col-12">
{% image block.value.image width-400 as img %}
<img class="img-fluid border" alt="" src="{{ img.url }}">
</div>
</div>
</div>
Create wagtail_bootstrap_blog/templates/blog/blocks/image_carousel.html
{% load wagtailcore_tags wagtailimages_tags %}
<div id="carouselExampleIndicators_{{ block.id }}" class="carousel slide" data-ride="carousel">
<ol class="carousel-indicators">
{% for item in block.value %}
<li data-target="#carouselExampleIndicators_{{ block.id }}" data-slide-to="{{ forloop.counter0 }}" class="{% if forloop.counter0 == 1 %}active{% endif %}"></li>
{% endfor %}
</ol>
<div class="carousel-inner">
{% for item in block.value %}
<div class="carousel-item {% if forloop.counter0 == 1 %}active{% endif %}">
{% image item width-400 as img %}
<img class="d-block w-100" src="{{ img.url }}" >
</div>
{% endfor %}
</div>
<a class="carousel-control-prev" href="#carouselExampleIndicators_{{ block.id }}" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#carouselExampleIndicators_{{ block.id }}" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
Notes:
- Sometimes, we need to differentiate the component with other components (so the JS code can work on the correct element),
block.id
can help us solve the problem - In this
carousel
template, we useblock.id
to help us make theprev
andnext
button work on the correctcarousel
element. - Wagtail use
uid.uuid4()
to make sure each block id is unique and you can check Github source code to learn more
include VS include_block
In Wagtail, if you want to specify template for some block, there are some different ways.
Solution 1
blocks.ImageText(template='image_text.html')
Solution 2
class ImageText(StructBlock):
reverse = BooleanBlock(required=False)
text = RichTextBlock()
image = CustomImageChooserBlock()
class Meta:
template = 'image_text.html'
Notes:
- Solution 1 and 2 can both work with
include_block
from Wagtail. - In the template, we need to access the block value using code like this
{{ value.text|richtext }}
- And I highly recommend you to read this section of Wagtail doc: BoundBlocks and values
Solution 3
{% include 'blog/blocks/image_text.html' with block=block only %}
include
a is built-in template tag from Django, and I with people to understandinclude
better here (include_block
seems like asyntax sugar
)- The
template
info is stored in parent template instead of themodel
Wagtail Tutorial Series:
To get the latest learning resource for Wagtail 4, please check Build Blog With Wagtail CMS (4.0.0)
- Create Wagtail Project
- Dockerizing Wagtail App
- Add Blog Models to Wagtail
- How to write Wagtail page template
- Add Bootstrap Theme to Wagtail
- How to use StreamField in Wagtail
- Wagtail Routable Page
- Add pagination component to Wagtail
- Customize Wagtail Page URL
- Add Full Text Search to Wagtail
- Add Markdown Support to Wagtail
- Add LaTeX Support & Code Highlight In Wagtail
- How to Build Form Page in Wagtail
- How to Create and Manage Menus in Wagtail
- Wagtail SEO Guide
- Source code: https://github.com/AccordBox/wagtail-tailwind-blog
Wagtail Tips:
- Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
- Wagtail Tip #2: How to Export & Restore Wagtail Site
Write style in Wagtail:
- How to use SCSS/SASS in your Django project (Python Way)
- How to use SCSS/SASS in your Django project (NPM Way)
Other Wagtail Topics: