wagtail-headless.jpg

Recipes when building a headless CMS with Wagtail's API

4 years ago 6617 views
3 min read

Recently I built a headless CMS using Wagtail's API as a backend with NextJS/React/Redux as a frontend. Building the API I ran into some small issues with Image URL data, the API representation of snippets and creating a fully customized page representation. I'll show some simple recipes which will hopefully simplify it for anyone encountering these issues.

Images in the API

I create a ImageChooserBlock with a custom API representation. You can use the function get_rendition() to fetch all attributes as e.g the full URL to the image. It's possible to pass arguments suiting your needs as you can when using Wagtail images with Django templates, meaning you can pass max,min,original,fill etc.

from wagtail.images.blocks import ImageChooserBlock as DefaultImageChooserBlock

class ImageChooserBlock(DefaultImageChooserBlock):
    def get_api_representation(self, value, context=None):
        if value:
            return {
                "id": value.id,
                "title": value.title,
                "original": value.get_rendition("original").attrs_dict,
                "thumbnail": value.get_rendition("fill-120x120").attrs_dict,
            }

Then in your StreamBlock just reference the new ImageChooserBlock.

from .common_blocks import ImageChooserBlock

class MyBlock(StructBlock):
    image = ImageChooserBlock()

Custom representations of a page

I use a PageChooserPanel in my header snippet to make it easy for the user to add links to other pages in the header.

@register_snippet
class Header(models.Model):

    links = StreamField(
        [("link", PageChooserBlock(page_type="api.PortfolioPage"))],
        null=True,
        blank=True,
    )

Since the snippet extends the django.db model I won't be able to customize the get_api_representation() method so I use a custom SnippetChooserBlock with a custom api representation.

class HeaderChooserBlock(DefaultSnippetChooserBlock):
    def get_api_representation(self, value, context=None):
        if value:
            links = []
            for page in value.links:
                links.append({"url": page.value.url, "title": page.value.title})

            return {
                "logo": value.logo.get_rendition("original").attrs_dict,
                "links": links,
                "header_links_color": value.header_links_color,
            }

I loop through all the links in the StreamField and then I can access all the Page fields and add any fields to the API that I would like. Then you can reference your own HeaderChooserBlock when you want a page to have an specific header and you can customize all the fields you'd like it to return from the chosen page from the PageChooserBlock.

Snippets /w SnippetChooserPanel

If we continue from the above example of a HeaderChooserBlock, we will add a similar FooterChooserBlock. We use different blocks due to the varying API representation (reach out to me if you have a less repetitive solution).

class FooterChooserBlock(DefaultSnippetChooserBlock):
    def get_api_representation(self, value, context=None):
        if value:
            return {
                "copyright": value.copyright,
                "social_links": value.social_links.get_prep_value(),
            }

Remember you can use get_prep_value() to fetch all the fields for nested blocks within Streamfields. Remember that you will not receive any URLs but an ID of the image or page(when using e.g PageChooserBlock or ImageChooserBlock) which you can then use to make a second API call. I do not prefer to make several API calls therefore I customize the exact values returned to include page/image URLs and not IDs.

class LayoutBlock(StreamBlock):
    header = HeaderChooserBlock("api.Header")
    footer = FooterChooserBlock("api.Footer")

    class Meta:
        icon = "snippet"

I wrap then both layout fields (header, footer) in a custom LayoutBlock and then you can use it in any page you'd like.

snippets = StreamField(
    [
        (
            "layout",
            LayoutBlock(
                block_counts={"header": {"max_num": 1}, "footer": {"max_num": 1}}
            ),
        )
    ]
)

content_panels = Page.content_panels + [
    StreamFieldPanel("snippets"),
    ...
]

Summary

I played around first with Wagtail GraphQL as a headless CMS with Gatsby as a frontend, but I did not enjoy the query language therefore I opted for NextJS+Wagtail's API. I found it great, I can customize all API calls however I want and more so it has great defaults for almost all blocks/fields so you do not have to tinker with the data returned.

If you have any questions or better solutions, feel free to get in touch. I had some issues as I did not find a large community behind the Wagtail API, thus answers to beginner questions were hard to find, hope this helps.


Similar Posts

4 years ago
mailgun statuscake terraform cloudflare devops s3 rds django

Kickstarting Infrastructure for Django Applications with Terraform

8 min read

When creating Django applications or using cookiecutters as Django Cookiecutter you will have by default a number of dependencies that will be needed to be created as a S3 bucket, a Postgres Database and a Mailgun domain.


4 years ago
django a-b-testing google-optimize web-analytics google-analytics

Django A/B testing with Google Optimize

8 min read

Struggling to determine your product’s future path? A/B testing helps you decide if you should take the road less traveled by. To understand your users and their needs, start by creating two product variations and collecting data points. Google Optimize …


10 months ago
celery json structlog opentelemetry tracing django logging

Django Development and Production Logging

12 min read

Django comes with many great built-in features, logging is one of them. Pre-configured logging setups that vary between development (debug mode) and production environments. Easy integration to send you emails on errors, out-of-the-box support for various settings as log levels …