wagtail-headless.jpg

Recipes when building a headless CMS with Wagtail's API

4 weeks ago

NEW!

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 Blogs