Recipes when building a headless CMS with Wagtail's API
Tags: cms, wagtail, headless, django
Published: 2019-11-11 15:52:21

#### 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 get 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.

```python
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.

```python
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.

```python
@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.

```python
from wagtail.snippets.blocks import SnippetChooserBlock as DefaultSnippetChooserBlock

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).

```python
from wagtail.snippets.blocks import SnippetChooserBlock as DefaultSnippetChooserBlock

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.

```python
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.

```python
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 GraphQL 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.