How to use StreamField in Wagtail

Table of Contents

Wagtail Tutorial Series:

To get the latest learning resource for Wagtail 4, please check Build Blog With Wagtail CMS (4.0.0)

  1. Create Wagtail Project
  2. Dockerizing Wagtail App
  3. Add Blog Models to Wagtail
  4. How to write Wagtail page template
  5. Add Bootstrap Theme to Wagtail
  6. How to use StreamField in Wagtail
  7. Wagtail Routable Page
  8. Add pagination component to Wagtail
  9. Customize Wagtail Page URL
  10. Add Full Text Search to Wagtail
  11. Add Markdown Support to Wagtail
  12. Add LaTeX Support & Code Highlight In Wagtail
  13. How to Build Form Page in Wagtail
  14. How to Create and Manage Menus in Wagtail
  15. Wagtail SEO Guide
  16. Source code: https://github.com/AccordBox/wagtail-tailwind-blog

Wagtail Tips:

  1. Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
  2. Wagtail Tip #2: How to Export & Restore Wagtail Site

Write style in Wagtail:

  1. How to use SCSS/SASS in your Django project (Python Way)
  2. How to use SCSS/SASS in your Django project (NPM Way)

Other Wagtail Topics:

  1. How to make Wagtail project have good coding style
  2. How to do A/B Testing in Wagtail CMS 
  3. How to build a landing page using Wagtail CMS 
  4. How to support multi-language in Wagtail CMS 

More Wagtail articles and eBooks written by me

Objectives

By the end of this chapter, you should be able to:

  1. Understand how StreamField works
  2. Use StreamField to store body value of the PostPage.
  3. 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.

  1. Basic block, which is similar with Django model field types For example, CharBlock, TextBlock, ChoiceBlock
  2. Chooser Block, which is for object selection. For example, PageChooserBlock, ImageChooserBlock.
  3. StructBlock, which works like dict (Object in js), which contains fixed sub-blocks.
  4. StreamBlock, ListBlock, which works like list (Arrays in js), which contains no-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())
  1. ImageText inherits from StructBlock, it has three sub-blocks, we can only set values to reverse, text and image.
  2. BodyBlock inherits from StreamBlock, we can add more than one sub-blocks because StreamBlock 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"),
    ]
  1. import BodyBlock from ./blocks
  2. Define body body = StreamField(BodyBlock(), blank=True)
  3. 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()
  1. For basic block, the value is usually number and string.
  2. For chooser block, the value is the primary key of the selected object.
  3. For StructBlock, the value is a Python dict
  4. For StreamBlock and ListBlock, the value 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:

  1. 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:

  1. Here we use Django for-loop to iterate the body, and retuen different HTML based on the block_type
  2. The only option in the include template tag, would make sure no other context variables are available in the template Django include
  3. 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:

  1. 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
  2. In this carousel template, we use block.id to help us make the prev and next button work on the correct carousel element.
  3. 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:

  1. Solution 1 and 2 can both work with include_block from Wagtail.
  2. In the template, we need to access the block value using code like this {{ value.text|richtext }}
  3. 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 %}
  1. include a is built-in template tag from Django, and I with people to understand include better here (include_block seems like a syntax sugar)
  2. The template info is stored in parent template instead of the model

Wagtail Tutorial Series:

To get the latest learning resource for Wagtail 4, please check Build Blog With Wagtail CMS (4.0.0)

  1. Create Wagtail Project
  2. Dockerizing Wagtail App
  3. Add Blog Models to Wagtail
  4. How to write Wagtail page template
  5. Add Bootstrap Theme to Wagtail
  6. How to use StreamField in Wagtail
  7. Wagtail Routable Page
  8. Add pagination component to Wagtail
  9. Customize Wagtail Page URL
  10. Add Full Text Search to Wagtail
  11. Add Markdown Support to Wagtail
  12. Add LaTeX Support & Code Highlight In Wagtail
  13. How to Build Form Page in Wagtail
  14. How to Create and Manage Menus in Wagtail
  15. Wagtail SEO Guide
  16. Source code: https://github.com/AccordBox/wagtail-tailwind-blog

Wagtail Tips:

  1. Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
  2. Wagtail Tip #2: How to Export & Restore Wagtail Site

Write style in Wagtail:

  1. How to use SCSS/SASS in your Django project (Python Way)
  2. How to use SCSS/SASS in your Django project (NPM Way)

Other Wagtail Topics:

  1. How to make Wagtail project have good coding style
  2. How to do A/B Testing in Wagtail CMS 
  3. How to build a landing page using Wagtail CMS 
  4. How to support multi-language in Wagtail CMS 

More Wagtail articles and eBooks written by me

Launch Products Faster with Django

SaaS Hammer helps you launch products in faster way. It contains all the foundations you need so you can focus on your product.

Michael Yin

Michael is a Full Stack Developer from China who loves writing code, tutorials about Django, and modern frontend tech.

He has published some ebooks on leanpub and tech course on testdriven.io.

He is also the founder of the AccordBox which provides the web development services.

Django SaaS Template

It aims to save your time and money building your product

Learn More

This book will teach you how to build a SPA (single-page application) with React and Wagtail CMS

Read More
© 2018 - 2023 AccordBox