Headless Wagtail CMS: Serializing Child Pages (Serializing a QuerySet)
Published November 29, 2023
This video features Kalob Taulien at Wagtail CMS 2023 .
Tutorial: https://learnwagtail.com/tutorials/headless-cms-serializing-richtext-blocks/
Wagtail for Beginners Course: https://learnwagtail.com/wagtail-for-beginners/
Git Commit: https://github.com/CodingForEverybody/learn-wagtail/commit/1f4551a8696f277931e71dc5d0ace3365a79f285
Don't forget, pudb is you're best friend when interactively exploring your code
For more free tutorials: https://learnwagtail.com/tutorials/
Don't forget to subscribe to this channel and follow me on Twitter at https://twitter.com/kalobtaulien
#Wagtail #Django #Python
Wagtail stores rich text in a custom HTML-like format, including links and embeds represented by IDs and metadata rather than normal HTML elements. To make a rich text StreamField block usable in a headless API, define its `get_api_representation(self, value, context=None)` method and pass `value.source` through Wagtail’s `rich_text` function from `wagtail.core.templatetags.wagtailcore_tags`. This applies the same parsing used by Wagtail templates, converting embedded images and page links into regular HTML that API consumers and browsers can use.
Summarised automatically from the transcript.
Automatically transcribed, so expect mistakes in names and technical terms.
Hello and welcome back to another lesson on learning Wagtail. In this video, we are going to talk about some more headless CMS stuff. And actually what we're going to do is we're going to take a look at serializing a rich text stream field or a rich text block technically is what it's called. Now as an example, I have a page here. Actually that's not going to work. I need to start my server first. Now as an example, I can go to pages. I have a homepage, I have a blog, and I have two blog article pages here. So if I edit this one article page, I've already pre-populated this with some data. And so down here, if I go down to the bottom, I have some content. I have these these blocks, these stream blocks that I can add. Title and Text, Full Rich Text, Simple Rich Text, Staff Cards, and Call to Action. Now the problem is
with a rich text field, Wagtail actually doesn't store this data as simple HTML. It's actually a little bit different. So if we open up our API, we can actually see this. stored somewhat differently. What page was this? I am editing page number five, so I'm just going to go straight to the detail page of page number five. And the content down here is if we take a look at this. It's a paragraph. Hello, Rich Text World. The second paragraph has a link, AID of four link type is equal to a page. So it's got some weirdness in there. And I also added an image. This little image right there, just a simple gradient. And here it is. So this is our image. It's an embed element.
With an alternative text of gradient. png, embed type is an image, the format is full width, and the ID is one. In regular HTML, that actually makes no sense. That's not an image tag. That's not even close to an image tag. But when Wagtail goes to parse this in its template, it'll look for every embed element and it'll say, oh, this embed element has an ID of one, and its embed type is an image. So look up image number one and format it full width. So it does all that behind the scenes, but in our serializer we we don't really have access to that. Now before we get started with this, you are going to want your Wagtail V2 API enabled. If you don't have it enabled, I have a video on that to show you exactly how to enable it so that you can see all the greatness that the Wagtail V2 API can give you.
You're going to need that before moving forward though, so make sure you have that enabled. Don't forget you can subscribe and click the notification icon down below on YouTube to make sure that you always get updates whenever I release a new Learn Wagtail video. So in my code I'm going to open up blogmodels. py and I have a detail page in here. And this detail page has content. and our full text block in here. And that simply matches our stream field in here. Or our stream block rather called full rich text. Full rich text. So let's jump on over there. And to create a serialized sort of output for this is actually super, super simple.
Okay, so when it comes to creating a more customized API representation of your code. And this is not going to show up in your Django templates. This is going to show up in your API representation. We have a simple method. So in the previous videos, we've been using this method called to represent And then it took self and then whatever the value was. Now this one's actually not too different, really. So this one's going to be called something different, but it sort of works the exact same way. And this one's going to be called getAPI representation. Thank you, VS Code, for auto-filling that for me. And it takes self value and context by default is going to be none. Now we're not going to do anything with that context.
You can safely ignore that for the time being, but let's go ahead and return literally anything I want. And what this is going to do is take this rich text code Remember that broken HTML that we saw with like an embedded element that Wagtail sort of parses and magically unravels in the template? We're going to overwrite all that HTML with literally anything I want. So let's go back to our API and when I refresh, it says literally anything I want. Now that's cool, because to get our value, all we have to do is return the value. And the problem with returning our value Is that it's no longer serializable, at least not in this instance. So let's go ahead and take a look at this. Uh let's see what is being passed in here and what exactly we can work with.
So I have a program called PUDB. It's a Python interactive debugger. It's a lot like PDB, which comes with Python, but it's a little more visual. So I'm just going to save that. And all I'm doing is import PDB, or PUDB rather, PU. db, and that's going to open up a debugger for me. Now if you don't already have this, I love this tool and you can get it by simply doing pip installpudb. You don't need to add it to your Django installed apps or anything like that. It's just install PUDB and then add this line. So now I'm going to refresh this page and when I open up my terminal, you'll see this is probably going to load forever, but when I open up my terminal, I have PUDB in here. We've got context, and in this context, we've got the request, we've got the view, we've got a router, and we've got our base query set, which is pretty cool.
And in self, we have our rich text block. And it comes with a bunch of other stuff in there, we're not going to go through all of that. And then the value itself is the rich text And we can actually see that it wasn't serializable because it's not called value. Behind the scenes it's called value. source. So we're getting that right here, the dot source. And you can see that it has our broken or not really broken, but our custom Wagtail HTML in there. So we need to serialize that. So to get out of that, I just pressed Q and I'm gonna delete that, resave, wait for the server to kickstart, and there we go, restart. And again we're gonna see that this is not going to work, but if we did return value. source, which is what we saw in PUDB.
We can refresh the page and it's going to work for us. And there we have it, we have our regular rich text. Now effectively we have done nothing. We've just gone back to the exact start of this video. We haven't done anything. We need to parse this. Now if you're familiar with rich text in the template, Let's go ahead and look at this richtext block. html and that was a terrible example. Let's look for somewhere in here. Ah, here it is. Okay, this is a good one. Is this a better one? Yeah, this is a shorter one. So we have some text in this particular stream field, and it's using this rich text filter. And we know by using it on the templates that it automatically parses everything we need for us.
It does all the magic for us. So all we need to do is use this particular filter in Python rather than in our HTML or our Our template. So let's close that down. Close that down. And all we have to do is use that rich text function. Now we don't have that rich text function imported. We're going to need to import that. Luckily that import is really easy and what I'm going to do is I'll just import it right here and then I'll move it to the top of the page so you can sort of see it all in one view. So uh I'm gonna do from Wagtail dot core dot template tags dot wagtail core underscore tags import rich text Now if at any point in time you're like, oh wow, Caleb, where the heck did you get that?
What I did to find that The very first time I actually did this, uh I that was this one, right? Yeah. So I have this rich text tag, and I know that every template tag is a function in Django. So what I did was a global search through my project. or through the Wagtail source code for def rich text. And this is not going to find it in here because I don't actually have the source code in here, or possibly because VS Code is being a little too No, not git keep. Git ignore. Git ignore. Venv, where are you? Let's go ahead and comment that out. And that complaint is fine. And there we go. Okay, so V
my VS Code, anyways, was smart enough to say, oh, this is gitignored, don't do any searches in there. So I just Comment that that out and I could see the code in here. So this is where it is in our Wagtail source code. And so simply I just did from Wagtail dot core dot template tags dot WagtailCore tags import this function. That's literally all I had to do. It was just a little bit hard to find that at first, but that's all I had to do for that. I'm gonna undo that because I don't need that. uh being committed to my repo. And so from Wagtail Core template tags, WagtailCore tags import rich text. Then I wrapped the value source that odd HTML that we saw. I wrapped rich text around that And if I go back here, we're going to see that this actually works perfectly. It does the exact thing that it does on your template.
So it gives you a div class with rich text. regular paragraph in here. That link is now a href is equal to blog. And let's go look at that image. That image was over here. That image used to be called embed. It is now an image tag with an alt, with a class, with a height. With a custom source and a whip. Now you can use CSS to overwrite any of that if you wanted to. You could use Beautiful Soup to go in there and grab that image tag and do anything else you want to with it. The world is literally your oyster. But at this point in time, our rich text block is officially being encoded into proper HTML. So it's going from whatever Wagtail saves it as to actual HTML that your browser can use.
Now as a quick little recap, literally all we had to do was add a function in here, or a method rather, called getAPI representation. It pass pass in self value. You can play around with the context if you need any sort of extra context in there like the page request. And we said, hey, use that rich text filter, but use it in a Python way instead of a template way, and pass in the value. source. And that is all we had to do. Don't forget, the source code is going to be available in the commit link down below. If you're interested in learning more about Headless Wagtail, head on over to learnwagtail. com. Type in headless in the search bar, and you're going to find all sorts of videos in here. Last but not least, my name is Caleb Tollin. I have been the voice behind the video. You can subscribe to the channel if you like.
Or if you want, you can come follow me on Twitter as well at CalebTollin. Thanks for tuning in and I'll see you in the next video.
Wagtail stores rich text in its own markup, including elements such as page links and image embeds with IDs and formatting metadata. That markup is transformed into ordinary HTML when Wagtail renders it in a template, but it is not automatically transformed in the API serializer.
Discussed at 0:46Define a `get_api_representation(self, value, context=None)` method on the rich text block. The method controls what the block returns in the API, rather than changing how it appears in Django templates.
Discussed at 3:03Import Wagtail’s `rich_text` function from `wagtail.core.templatetags.wagtailcore_tags` and return `rich_text(value.source)`. This applies the same conversion used by the template rich-text filter, turning links and embeds into usable HTML.
Discussed at 6:59Note: We understand that names change, people change, and bodies change. We respect each individual's journey and privacy. If you have any concerns about a video or need us to remove content, please don't hesitate to contact us. We will handle your request with care and promptly address any issues.
Published September 19, 2026
Published July 9, 2026
Published May 20, 2026
Published April 16, 2026
Published April 1, 2026
Published March 10, 2026