Wagtail Headless CMS Workshop (with Vue.js)

This video features Tom Dyson at Wagtail CMS 2023 .

Wagtail Headless CMS Workshop (with Vue.js)
0:59:03
Published November 29, 2023
18,637 views
299 likes

All code is available at https://wagtail.io/headless-workshop/
Wagtail for Beginners Course: https://learnwagtail.com/wagtail-for-beginners/

Learn how to create a Headless Wagtail CMS website and have Vue.js consume data from the API.

In this workshop we will:

  • Create a new Wagtail website
  • Create a custom Wagtail Page
  • Enable the v2 API
  • Explore Vue.js, Vue Components, Vue Routing, and Axios
  • Consume API data with Vue.js
  • Work with a custom Image Rendition serializer
  • Serialize StreamFields
  • Install and enable headless preview

All code is available at https://wagtail.io/headless-workshop/

Extra resources:
Other Headless Tutorials: https://learnwagtail.com/search/?q=headless
Headless Preview: https://github.com/torchbox/wagtail-headless-preview
Vue Cheat Sheet: http://codepop.com/Vue-Essentials-Cheat-Sheet.pdf
Vue Mastery: https://www.vuemastery.com/

Big thank you to Torchbox and Tom Dyson for all the work you've put into making this workshop possible, and a reality.

#wagtail #api #headless

Summary

Tom Dyson walks through building a headless Wagtail site with Vue.js. He creates a News page model, exposes it through Wagtail’s v2 API, uses Axios and Vue components to render listing and detail pages, adds client-side routing, and configures CORS for a separately served frontend. He also shows how to expose an image through a custom 300×300 rendition, before the transcript cuts off while adding that image to the Vue detail page.

Key takeaways

  • A Wagtail project can serve content through its v2 API without rendering Wagtail templates.
  • Custom page fields must be added to the model’s API fields before they appear in API responses.
  • Vue and Axios can fetch Wagtail JSON and render listing and detail views in a separate frontend.
  • The frontend and backend need CORS configuration when they run on different origins.
  • Wagtail’s image rendition API can return a resized image URL, such as a 300×300 thumbnail.

Summarised automatically from the transcript.

Transcript

10,831 words · auto-generated Show

Automatically transcribed, so expect mistakes in names and technical terms.

0:00

Hello and welcome. We are going to be creating a headless Wagtail website using view. js in this video. We're going to go over a lot of different things. Including how to set up the Wagtail V2 API, dealing with things like uh cores, cross-origin problems. Create a custom image rendition, and we're going to get into all sorts of other things. So if you are interested in creating a headless Wagtail website with a technology like View or React or something like that. Then this is definitely a video that you were going to want to watch. Now I'm going to be using view. js, but all these concepts can be applied to React as well. I'll just be using view. js for my JavaScript. framework, whereas you might be using something else.

0:45

And before we get started, I actually cannot take any credit for really anything in this video. All I did was make the video. Our friends over at Torchbox did a lot of work with the whole headless stuff, and it was actually Tom Dyson who created the original tutorial for this. All I did was turn it into a video. So thank you Torchbox and thank you Tom. And without further ado, let's go ahead and get started. So first things first, we are going to need to create a brand new website So I'm going to create a new directory, mkdire, call it temp because it's just a temporary directory for me. And I am going to cd into temp and Do an LL or ls dash la and see that there's nothing in there. Now we need to create a new

1:32

virtual environment. We can do this several different ways. We do PipEnv, Venv, VirtualEnv, if you want to use Docker, you can do that, or Vagrant, or Vagrant. Should I be able to spell that right? You could use Vagrant as well. There's lots of different ways you could do it. I'm going to go with what I think is the easiest way. I'm going to be using Pip Env. So Pip Env. And let's just create a brand new project for this. And in here, I'm gonna do Python-free. You can see that I'm using Python 3. 7. 4 So now we need to do Wagale Start and I'm going to create a new project called backend And now I'm going to CD into backend because that's where my Wagtail project is, ls-la. We can see I've got a manage.

2:18

py file in there. And what I can do now is simply migrate, run server, and create super user. Now before we do that we need to pip install-r our requirements. And if I do pip show wagtail We can see that I'm using Wagtail 2. 7. 1, and if I do pipshow Django, I'm using 2. 2. 9. So now I want to do python manage. py, migrate. And this is just going to set up my database as you know you normally would set up your database for any project. And then we could do run server in here. And just to make sure that this is all working, let's open up our browser and go to localhost

3:03

8000 and we can see that this is working. And let's go to our admin interface. Well we need an admin user. So let's cancel our server and create super user. Python manage. py create super user. My username is going to be Caleb, Caleb at learnwagtail. com. My password is just a regular localhost password, nothing fancy. Python manage. py run server on port 8000, and now I can do KLUB and My password. And voila, we are inside of Wagtow. Now that's all good, but we actually don't have anything we can do in here. So if we check out our homepage and we go. . add child page we can really only add another home page we don't want that we want to create a new application called news so I'm going to cancel my server

3:51

Clear that out and do python manage. py startapp news. And let's go ahead and restart the server And I'm going to actually I gotta cancel this for a second. I'm gonna open this up in VS Code and Sublime. So currently I'm just going to open this up in VS Code and I'll reposition this so you can actually see it. And now we want to open up our base. py. That is just 10 degrees too large. Okay, so in our installed apps, we have home and search by default, and that matches what we have. In our regular Wagtail install. We've got home, search, and we just create a new one in here called news. So let's go ahead and add news in here. Now we haven't really done anything with this yet, so we don't need to run migrations, we don't need to do anything like that, but we do need to create a new Wagtail page.

4:42

So let's do a little pseudocoding here. So let's do class news page. And by the way, I just opened up the news app models. py and I want to create a new Wagtail page. Now at this point you should be pretty familiar with creating a new Wagtail page. and sort of how this all all works. So I'm doing some pseudocode intro. I want uh intro to be models. char field and a max length of Something probably a little too absurd, I guess. I don't know, 250 characters. And the body is actually going to be a Wagtail field, a rich text field. And we're going to say that this is allowed to be blank. Now we need to import a bunch of this stuff. So let's go from Wagtail. core. models import page, and that gives us our page from

5:28

Wagtail. core. fields. Import rich text field, and that gives us our rich text field. And then we're going to need one more. I haven't added it yet, but we can see down here we've got our content panels. We're going to need to add intro and body in there. So we're going to need to add a field panel. So from Wagtail. admin. edit handlers import a field panel. We haven't got to this yet, but we're getting there in just a second. So now we want our content panels to expose the intro and body fields. And by default, Wagtail doesn't know that you wanted it exposed or not exposed, so we're going to expose it. So we're going to say content panels is equal to page. content panels plus a list with a field panel in here, and we're going to do that twice.

6:13

That first one is going to be intro, and that second one is going to be body. Just like that. Now let's go ahead and let's see if we can make our migrations. Make migrations Nope, I have a typo in there, uh core models. There we go. This will always complain if an import isn't correct. And Python migrate to make that change. Let's restart our server with python manage. py run server at 0. 0. 0. 0 port 8000. And now when I refresh this, I can add a new page and create a new a new news page. So at this point I'm going to create a brand new news page. And there's not going to be a lot of detail on here, but it's going to be I don't know. It'll be something at least.

7:00

So let's do this. Breaking news Wagtail is the best CMS in the world. Obviously, we all know that. And I'm gonna fill the intro with some Lorem Ipsum in there, and the body's also going to have some Lorem Ipsum. Just three paragraphs of Lorem Ipsum and maybe a pair not a paragraph in h3 that just says hello world. Let's go ahead and save that. So at this point, we're still just using Wagtail in a in a normal manner, but if we view this page, we're gonna see that the template does not exist. And Because we're using this in a headless manner, or we're going to be using this in the headless manner, we're not going to be creating this template at all Now at this point we need to enable our V2 API. That's the Wagtail API that allows us to basically serialize things like our page.

7:48

So what I'm gonna do here is just open up the docs because the docs will always show you exactly how to do this. And we need to, first things first, grab wagtel. api. v2. Open up our base. html, nope, base. py is what I meant to say. Open up our base. py and just throw that in there as well. Not double quoted, just regular quotes. And if we go back to the documentation, we also need to configure a bunch of endpoints in here. Now there is a note in here that I'm going to bring up in Wagtail version 2. 7, these are called endpoints. So we are importing from endpoints. In the latest version, which currently is 2. 8 going into 2. 9, We can see that these are called views, so you're going to see something different.

8:36

So always make sure you're referencing the right version of the docs. 2. 7 is the current stable release right now, and as of recording, 2. 8 is on its way within just a few weeks. So just remember that this code, what you're seeing here, the dot endpoints, might be different for you depending on the version of Magdal that you're using. So always reference the docs. So I just copied all of that, and what I need to do here is in our settings, not our settings, well sort of our settings, in our URLs here, I'm going to create a new file called api. py And I just put that right beside my URLs file. So they're in the same folder. And all this is going to do is set up our pages API. endpoint, images, and documents API endpoints, and then they're going to register those as proper endpoints

9:24

that we can basically turn all of our our pages, all the content that we're creating into And one last thing we need to do here is we need to add this URL and import API router from API. So let's open up our URLs once more. Let's go in here. From. api import API router. And we want to make sure that This URL, this URL that we're adding in here, the API V2, is above our Wagtail URLs. So go ahead and save that. And let's open up a new tab and go to localhost port 8000 slash API slash v2 slash pages. Now if you see this it's because Wagtail comes with its own sort of way of serializing a bunch of stuff, but it piggybacks off of

10:12

Django rest framework. So what we need to do here is go back to our base. py and really anywhere in here, we just need to put rest underscore framework. And assuming my server has restarted itself, this will work. There we go. So we have two pages. We've got a home page and we've got a news page. And we've got a title in here. Breaking news. Wait, I'll make that a little bigger. Breaking news. Wagtail is the best CMS in the world. Now we can also filter this. There's a lot of different ways we can filter and select certain data. The way we're going to is say We're going to say type is equal to news. newspage. And where we're getting that from is in our models. py, it's the news app and it's the news page class.

10:57

So news. Dot news page, that's how we got that. And now it just shows us our news pages Now by default the API is really only going to expose common fields like your title or you know up the slog or the f first published at date, things like that. That comes with every single Wagtail page. At this point we actually want to expose our intro and our body. So we need to do two things here. We need to say API fields is going to be An API, nope, I did that wrong. API field, and that's going to be a string with intro and body. And intro just matches up here as well as there. And body just matches up here.

11:43

Now this doesn't have to necessarily match what's in your content panels, but if you want it to be editable in your page, then it should be in there as well. Now API field does not currently exist in our import, so let's import that from Wagtail. api import our API field. Now if we go back here, we're not going to see anything different. But in our URL, if we say and fields is equal to intro, comma, body We can now see that we've got intro and body in here. And so really all that's saying is, just like our content panels, this is exposing it to the Wagtail admin area where you actually edit your content, The API fields is exposing your intro

12:29

and your body. It's exposing those fields into the actual API. Alright, let's go ahead and get started with uh some view stuff. Now I'm going to use a different editor for this Just so that this stays a little cleaner. I think working with the front end stuff and the back end stuff since it's already decoupled sort of makes sense for me to uh work in two different editors just for the purpose of this video. So I'm gonna open up another editor here and I'm gonna create a new file in my desktop. And a folder in here, I'm gonna call this folder front end. And the reason I'm putting this on my desktop, by the way, instead of in the Wagtail application that we've created here, is because I want this far away from this code so that you know that

13:15

it's not really related at all and that's going to make some sort of leap from one server to another in just a little bit. So we're going to do a new file call it index. html. And so I just pasted in some HTML in here. So we've got HTML5 doc type, uh the HTML tag itself, head, uh a title. uh character set of UTF 8 and we're saying load up view. Now this is just letting us use view uh from a content delivery network so that we don't actually have to worry about using Webpack or parcel or anything like that to compile our JavaScript. In a proper project, you'll probably want to do it the other way and compile it yourself. But this is just the quick way that we can get up and running with Vue right away. So in my body I've got a title in here called my first view

14:00

app, and I've got a message and an app in here. Now this divid app. matches this in here and this is where Vue is going to get mounted. Vue is going to do all of its business, all of its dirty work in this section that matches this app in here Then in our script we're saying app is equal to a new view, the element is going to be the app, that's where it's going to mount itself, and the message is going to be hello view. So if we open up a new tab and I'm just going to open this. It says Hello View, let's make that bigger. Hello View, despite the actual source saying message. So that's pretty cool. We can also, if we really wanted to, we could

14:46

play around in here a little bit. App. message is equal to Changed message. Just like that. Now the reason that that works is because app . message is mounted in here. So anytime we change this value, doesn't matter if it's in our console or in a code anywhere else, it's going to change this in here. And you notice that this syntax looks a lot like a Django template already. It looks a lot like Wagtail templates too. So that's already pretty nice. Now we can actually get a little fancier with this if we wanted to. And we could add filters, just like Django. So we do filters. We're going to give this a JavaScript object. We're going to call this upper. Function is going to uh it's going to take a value, whatever that value is going to be, and we're just going to return the value dot

15:35

to uppercase. Now this is just plain JavaScript, nothing fancy about this. And because this is a filter, just like in Django, we use a pipe. So now this really looks like Django, doesn't it? So if we go ahead and refresh our page. That's all uppercase now. Let's go ahead and make this a little more interesting. Let's add an input in here and let's do some binding V model is equal to, and whatever this model is going to be, it's just called message, and that just matches that little data point right there, this one as well, so it's all sort of mounted and working together. When I refresh this page, we can see it says hello view. And as I type, it just adds more and more text in here. So that's kind of cool. And you can also tell that whenever I type in lowercase, it automatically makes it uppercase, and it's super fast.

16:26

So that's a quick little demo of how View works. Now let's go ahead and I'm gonna resave this as a file called people. html and actually what I'm going to do is open up this whole folder. Now in this people. html file, what I'm going to do is just replace this. And let's switch that from HTML Django to just regular HTML. And in this one we're adding axios. So we've had view before, now we're adding axios. And axios is a way of really dealing with promises. Or a fancy way of saying, hey, we're going to use Ajax. So this is an Ajax library. That's an easier way of thinking about it. We have an app ID of app. That indenting did not work as I had intended, but that's fine.

17:14

And so all of our view stuff is going to be mounted inside of this div. Because that's what we're telling it to do. We're saying, hey, on this page there is some data in here, and that data is, well, really there's only people in here, and that's a list, or in JavaScript it's called an array. And then once view is mounted, we're going to say axios. get this URL, and we're going to put all the people that come back from this URL, whatever is in there, that response. data, we're going to put it into this. people. Then in our template we can say, for every person in people, we can put or print the person 's name So let's take a look at what this looks like real quick. This is how our data is currently formatted.

18:02

Let's do raw data. Nothing fancy, regular JSON. We've got names in here, and it's just really a list of dictionaries. Or in JavaScript, it's an array of objects with a name in there as the key. So let's save that and let's go ahead and open people. html. Okay, we've got a list of people in here. It's fairly small, but it's a list. We can do app. people. And just again where I'm getting app. people from is in here. So it's going to wait till View is mounted, and then it's going to create an Ajax request to go and get this information, whatever that That JSON data that we saw, it's going to grab all that and it's going to throw it into people, and then it's going to automatically loop through all of that for us and put a person's name in an LI

18:48

element. Now if I wanted to, I could get rid of it. I could get rid of people one at a time. I could do app. people. pop. And as I do this, we are just getting rid of people. Oh, I went one too far there. And just getting rid of people. And we can also do app dot people, not not add. We're doing it the JavaScript way, called push. This is going to take an object. We know that that key was named because we looked at that once already. And we're gonna say Caleb Tulling. That's just my name. And it adds it in there. 2 2 2 2 2. We do it again. 3-3-3-3-3. And we can add it. So on and so on. So view is nice and responsive in that manner. Now let's go ahead and create a new file in here called newslisting. html.

19:33

And we're going to start to actually get into some of this, how this actually relates back to our Wagtail website. So I've pasted some more code in here. Again, we're just using View and Axios. We have a view app in here. So again, that div with a view app. That's where all of our view stuff is going to live. And for every item in news, we're going to add an H2 and a paragraph. In our app, we also have news in here, so we're going to need to, once this is mounted. Fetch some data and assign news as an array to whatever comes back from our API response. Now I'm gonna grab this URL and I'm just gonna throw this. into an unused tab. And we can see here that there's only really one page. Let's go ahead and create one more page in here

20:20

Let's make this fun. Goats are the best. And we're gonna say goat. Greatest of all time. And we'll just say goat. And we'll just copy that over and over and over and over again Publish a page and let's check out okay and now our API is saying that there's two pages in here. So that's good. That's exactly what we want. Every time we add a new Wagtail page, our API is going to say, hey, there's another page. One thing to take note of here actually is while we're here you can see it's http localhost slash, but it actually doesn't match up with the URL that we're using. So if you see that and that ever turns out to be a problem for you, you can just go into your site.

21:06

And localhost is our default site and just change that port to 8000 or whichever port you end up using. And when we change this, it'll add that port in for us. Now let's go ahead and open up our newslisting. html page So let's go to this unused tab and open up news listing. html and we're going to see we get a network error in here. And if we turn on warnings, let's get rid of that. If we turn on our warnings in here, we can see that we're getting a cross-origin request is blocked. The same origin policy disallows reading the remote resource from lochost127. 0. 0. 1 port 8000 slash API, blah blah blah blah blah One other note as well. I'm using 127. 0. 0. 1 as my localhost IP.

21:53

If this doesn't work for you, if you're on one of those computers where that just doesn't work or if it's bound to something else. you can always switch out that 127. 0. 0. 1 for localhost. Or if your computer is the opposite for whatever reason and localhost doesn't work, whenever you see me type localhost, you can always You can always swap that out for 127. 0. 0. 1. So I'm gonna try to make this somewhat consistent and just use localhost. So the problem here basically is We are not allowed to fetch this data because if this was on its own server and we're trying to access another server, we could be stealing content. We could be trying to perform some sort of attack on another server. And we're not, but By default, security doesn't know that, so it's going to assume that, you know, things should be locked down. Now, I'm actually going to do something a little bit different, so

22:41

I'm gonna Open up a news tab. Actually I could keep that running. Open up a new tab in here. And I'm going to go to my desktop and go into frontend. And I'm going to start a new server in here. I'm going to do Python-m http server on port 8000 and 1. And you just gotta wait for that to boot up, and now I can go to localhost support 8000N1. This shows me the index page. We just worked on the people page. And now let's look at the Newsitem. html page. And that is not what I called it. It's called news listing. News item is coming up later.

23:26

News listing. html. And we can see that we're still getting this cross-origin request blocked error. Now with that Python command, basically all we did was We said, hey, make this folder basically act as if it's its own server on our computer without having to run Django and Wagtail and all that stuff. It's just a regular HTTP Python server. And we told it to run on port 8001. So moving forward, we can really just assume that this is sort of like another server, which is how you would have your headless site set up anyways. You would have. all of your front end code hosted somewhere and all of your back end code hosted somewhere else most likely. Now how do we get around this cross-origin request blocked issue? Well we have to install a new package to manage our core's headers.

24:13

So I'm gonna open up a new tab. Which one don't I need here? I don't need this one. And we are going to install Django Core's headers. So I'm just gonna go down here, Django's Core's headers, do do do do do pip install Django Core's headers. And go back to my Wagtail server and simply pip install Django course headers. That was done very quickly. Let's open up our Wagtail site Go to base. py and put in course headers in our installed apps. We also need to add some middleware in here, so I'm going to grab this middleware And find my middleware and I don't know, I'll just throw it like right there.

25:00

That's good enough. And we need one more thing. Cross origin, allow all. Now typically you would say there's a whitelist, and that whitelist would look something like this. It would be yourwebsite. com or localhost port 8000 or in this case 8080. I'm not going to worry about that right now because I don't need to. This is just my local work for the time being So I'm gonna go back into my settings, my base. py, and I don't know, somewhere in here, just put cross origin allow all is equal to true and restart our server. And let's go ahead and refresh. And look at that. This actually works now.

25:45

So all that was really saying was Hey, by the way, we know that other websites, other servers are going to try to access our data, and with our cross-origin settings, that's actually okay. We we don't mind if people do that. So that's what we're doing with our course headers. Now with our API, just taking a quick step back here, we have these different items. We've got idea 4, and this is the breaking news way kills the best CMS in the world. And we've got ID5. Uh goats are the best. Let's do five. Now if we wanted to get to a detail page, we can actually see that the detail page is here. And if I wanted to, I could simply click that. And this will show me all the data that I want to see. So this is our detail view. This is like editing your

26:33

news page that was called goats are the best, but in this case it's just goats are the best. And if we go back to the this page, we can see multiple pages in here. We've got ID4, ID5. This is our listing page. So this is going to give us all the pages we want to put in a listing page and our detail pages where we're going to end up getting more data. Now right now it's very, very similar, but that's going to change in a little bit. Now let's go ahead and create a new page in here. Let's call this newsitem. html. Now I just pasted some code in here, and again, as always, I'll give you access to the code that you see in this video. And that link will actually be down in the comment section down below, or in the description section down below rather. So in this page, we're adding uh view. js, we're adding Axios, which we've already dealt with.

27:18

Uh we've mounted to our app element. We've got an item title, item intro, and item body. We're saying as soon as view is mounted, go ahead, axiose this particular page. Actually I don't want to use page ID four, I want to use detail page five, goats are the best. Because I love goat so much, it's ridiculous. And so we're gonna go and get that information from the goats page that Last page that we saw in the API. We're going to put all that information to item and then it's going to update this in our document object model, our DOM. So we can go back here and instead of news listing we can do news item and look at that it gives us our title, it gives us That little subtitle that we created, and

28:04

we can actually see that you know it's got our HTML in here. That's not quite what we want. It's super close, but not quite what we want. If we added more HTML in here, we would see more HTML tags. And as an example, I think I actually set up the other page a little better for this. So let's change that back to four. And yeah, here we go. You can see there's an H3 and a few paragraphs in there. And that's not quite what we want. We want this to actually render as HTML, not just as HTML entities. Now the way we can do that with view is really really easy. We can just simply say v HTML is equal to the item Let's go ahead and get rid of this.

28:52

And haha, look at that. We've got a title and we've got separated paragraphs in here. So things are actually working quite well now. Now let's play with View's router system a little bit. So let's go ahead and create a new file in here. called routing. html. So I just pasted in some code here and you're gonna actually see that I've added one new thing in here, the view router, and this is going to let us basically change what's on the page based on the URL that's being served. So as always, we have our app being mounted in here. We've got a new element in here called ViewRouter. We've got one more in here called View Link, and this is going to go to slash foo, and this one's going to go to slash bar. And then in our code we're getting a little more complicated.

29:38

So we've got constfoo is equal to template, and it just says foo in there And said let's make that say foo123 and bar four five six just so we know that it's not just showing foo or bar from the URL. Then we've got uh routes, and this is just uh an array, or in Python a list. And it's a list of dictionaries or it's an array of objects, the path to where it's supposed to go, and the component that it's supposed to render, that template that it's supposed to render Then we create a new uh view router with the routes in it. And then we create a new view app with the router in it, and we mount it to our app. Now if that sounds like a lot, it's because it sort of is a lot at this point. And if you don't know all this, that's okay. There's lots of time to learn, view, or react, or anything like that.

30:24

If you're feeling lost at this point, I would say just carry on with me. You'll get through this, and it will actually start making more sense the more we dive into this. So we need to create, uh not create, we need to navigate to the file called routing. html. We have these two links in here, go to foo, go to bar, and this goes to bar 456 and foo123. And so that's view routing in a nutshell. Now at this point we need to pretty much put all of this together in one page. So I'm going to go back to this index. html page. That we created at the very beginning. And I just pasted it in some new code here. Now again, this is using the view router. It's using Axios and View. As always, it's got our app mounted in here.

31:10

We have a const API root, again, that 127. 0. 0. 1. If that doesn't work for you, you can just put localhost. And this is going to be our regular listing page. So this is where all of our pages end up coming through our API. So if we go to this exact page. Nope, that wasn't it. If we go to this exact page, there we go. Nailed it. We have three pages in here, total count three. We've got home page, we've got breaking news, Wagtail is the best CMS in the world, and goats are the best. Now we're getting even more involved with View. So now we have a new constant here called news listing. And in here we have a template And this template is using a back tick, by the way, that's very important. Because that means this is using a JavaScript template literal, which allows us to use

31:59

String formatting inside of a regular JavaScript string. We actually don't need the date. I've purposely left that out of this tutorial. In this news listing, we have some data in here in this particular component called news. And we're going to loop through every news item. It's just going to be called item. We're going to create a router link to slash news slash whatever that item ID is. And inside of that link is going to be an H2 with a title, and outside of that link is going to be the item intro. Now at this point if you're wondering like, hey Caleb, where's that intro coming from? Yeah, it's coming from our Wagtail intro here. And then we're going to say as soon as this is mounted, go ahead and grab the type is equal or well, our API root. It's gonna grab that first, this URL here, and then it's gonna say, hey, append type is equal to news

32:46

dot news page, and the fields is equal to intro body and date. Again, we don't need date And so if we want to see what this is actually doing, let's just go ahead and append that in here. We can see what this is doing. This really just filtered out the homepage at this point, but if there were other page types, it would filter out all the other ones too, except our particular news page. And then once that's done, it's going to get all those those items and stored into news. And those items come from in here, right here. That's a list. We can see that's a list in Python or JavaScript or JSON that's called an array. And we're just going to make sure that news is equal to that list or that array. Now we've got another uh component in here. News item. Again, there's a template, there's a router link back home, that's interesting.

33:33

This is more of our detail view, the other one is more of our listing view. And this one's just going to go back to home. But in here, this is our detail view, we're going to have a title, and we're also going to render out our HTML body. The only thing that's really different in here. . . is in mounted, we're saying this. getnews and we're using a method. And the reason we did that was really just to keep our code a little cleaner. So if we had more methods in the future, we could just append them. to the mounted method instead of having to write this long method and try to figure out what's going on and just makes it a little easier if we as humans can break things down into smaller problems. Then we have our roots and we've mapped this. Path, this is going to be our home. So our component is going to be our new listing page. This is our home, that's basically what the slash just means.

34:20

Just like a regular website where slash goes nowhere, just goes to your homepage. And we've got path in here, slash news, slash colon ID. And this ID is going to match. This ID right up here. And that's going to go to our detail view, which is going to then render out the news item component, which we just took a look at right there. So let's go ahead and save this and let's open it up. Now because this is an index page, we don't actually have to specify anything in here. Browsers always just assume index. html are the default pages. Or index. php or index. py or index. whatever. It always just assumes index is the main page that it wants to render out. And now we can click on this top one.

35:05

Look at that, breaking news, Wagkill's the best CMS in the world, obviously. If we go back, goats are the best. And you can actually see the URL changing at the top there as I change this URL or as I click around, you can see it changing. So now at this point we really just have a Wagtail website where we have a bunch of pages in here, or we have two pages, but we could have more pages in here. And we have really another server that's just running HTML, CSS, and JavaScript, and it's able to grab all of this information for us and render it out. Now granted this is actually a pretty ugly page, so we should go ahead and make that a little bit nicer. So I'm going to just paste this in here. And what this is going to do is add responsiveness. That's what the meta tag is for this one here. And

35:50

this link here is going to add tachyons. css. And it's just a way of styling. So you could add bootstrap or UI kit or foundation or anything like that. That's totally up to you. We're just going to be using tachyons for this. Now, the body, I'm going to add a bunch of classes in there. That h1, I'm going to add a bunch of classes in here. Not really changing the content, I'm just changing the classes. And let's change our router links as well. Let's add a class in here, router links. Class to do to do do do. And one there. Let's go ahead and refresh our page, see what this looks like. Hey look at that, that looks a little better, hey? Not bad, not bad at all. Okay. So officially we do have a headless site. Now this is very limited and we haven't really worked with too much else other than a couple of regular Wagtail fields.

36:39

Now let's go ahead and add an image in here. How do we add an image? It's not quite the same as adding a text field, because an image is actually a foreign key. So if we open up our Wagtail site here and we add an image, image is equal to models. foreign key. And this is going to go to Wagtel images dot image. That's just the default one. We're gonna say this can be null. We're going to say that this can be blank. And we're going to say onDelete is equal to models. setinall. We also need to import a thing, which we're going to use in just a second. So uh Wagtail dot images dot edit handlers

37:24

import image chooser panel And this one is actually different from the field panel, just notice that this is Wagtail. image, and this one up here is Wagtail. admin. And now that we have an image in here, we can expose it in our admin using the image chooser panel. And we're not going to do anything with this API field yet. Not yet, anyways. We're going to in just a second. And it's not wagt till image, it's wagt till images. Let's go ahead make migrations. Let's go ahead and migrate and restart our server Now when we go to edit one of these pages, let's say we're going to edit goats are the best.

38:11

We now have an image that we can select. So I'm going to upload an image Of this cute little goat. Yeah, look at that little guy. Okay, save that. And you know when we view our detail page of Goats are the best, it's not going to show up. It's because it's not actually even exposed at all in our API. So if we go to our detail view here in our API. . It's not in here. We've got title, intro, body, but we don't have an image in there Now let's go ahead and expose this as a regular API field, and we're going to see that we're actually going to be quite disappointed with these results So here we can see we've got image, image ID, that's great.

38:57

If we want to look up the image detail view, we could go to API slash v2 slash images slash one, that's the ID, and it'll give us some image details. Uh but we don't actually need that. We don't need the type, we don't need the detail URL, download URL, a title, we don't need any of that stuff. What we actually want in this particular case is an image rendition. And what I mean by that is maybe we don't want this image to be 3,000 by 2000, because that's actually what it was. If I go back to this detail view here, we can see that the width is 3000 and the height is 2000. Maybe we don't want that. Maybe instead, we actually just want an image rendition that's like two hundred or three hundred pixels wide and tall, so like a perfect square. Now we can do that by specifying a custom serializer. So we do serializer

39:43

is equal to An image rendition, rendition field. And we have not imported this yet, but we will. We're going to fill this with, let's say, 300 by 300. And the source is going to come from our image. And this image comes from that name right there. So right in one view, those two connect. Now that image rendition field is not imported yet, so let's go ahead and import that. From Wagtail. images. api. fields import image rendition field. And you can find all this stuff in the documents as well. So now we've got an API field using image with a custom serializer. So let's go ahead and take a look at this

40:32

And that's actually not going to work because that's the same name. There is a naming conflict here. So the source is image and it's basically thinking that it's itself. So it's and let's not call it image. Let's call it. Image thumbnail, because technically that's what it is. It's a fairly large thumbnail, but it is still a thumbnail. So if you ever see that It is redundant error that we just saw for a couple seconds there. Just always take a look at that API field. If that API field is called image, which is usually, it's usually called whatever your model field is called. And the source is the same, then chances are there's a naming conflict in there, and you have to change one of those names. I usually just change the name on the API field. Seems the easiest to me to do. Now if I go back here and I scroll down, we have an image thumbnail.

41:18

That's pretty cool. That matches this name here, that API field. We've got a URL. of this image rendition with a width and a height of 300. So let's go ahead and let's just put that URL in here. And look at that! A little 300 by 300 square image of a baby goat. Now that's all fun, but hey, you know what? If we come back to this go to the best page, it's not going to show up. It's not here. We need this to show up. Or we need it to try to show up at least. So let's open up our app in here. And let's find our news item because that was our detail page. So we've got a news item in here. And underneath this paragraph where we have all of our HTML, let's go ahead and add an image in here. Image. V if, and we can say if there is an

42:03

item dot image thumbnail And we can say there's a source of uh something in there. Let's get back to that in a sec. We can say there's a width of something in there. We'll get back to that in a sec. And we can also say that there's going to be a height. So we're just adding image attributes at this point. Now we can do image. image thumbnail. I'm going to copy that because I'll need that a couple times. Image image thumbnail dot width. And if you're asking yourself, hey Caleb, where where are you getting that from? I'm getting that from. This whole thing is called an item, that's what we named it anyways, as an item. Uh image thumbnail, and then we're getting the width and the height. And so it looks a lot like Python

42:50

classes where it's using that sort of dot notation or a JavaScript object where it's also using that dot notation. Now the source is going to be an interesting one. We need to put that URL in there, but We also need to do something that looks a little weird. We need to open and close this in regular string concatenation. So we're gonna do HTTP. HTTP localhost port 8000 because that's our Wagtail server. And then the image URL in there. And then lastly, let's just make sure we close that image. And let's refresh this page. Nothing? Nope, something broke. Image is undefined.

43:35

And that is because it's not image. image. If you're screaming at your screen saying, hey Caleb! Yeah, good call. It's item. image. I even walked both of us through that. But that happens from time to time. And there we go. So we've got an image in there And so really all that code was saying was, hey, you know what, if there is an image thumbnail, set that source to be the base source and then whatever that thumbnail URL is, and also set the width and the height on the image attribute. And that's all it did. Now if we go back to this other page, the breaking news, there's no image in there, so no image shows up. But there is an image in here, so image does show up. Now let's take a look at adding a stream field in here. We haven't done this yet. We've only worked with Wagtail

44:20

and Django fields or model fields, but let's go ahead and actually change out our body. So it's not just rich text, so that it can actually be a bunch of stream fields. Now I'm going to speed this up just a little bit because we should be fairly familiar with this part already. So what I'm going to do here is in the fields, where am I? Fields. Rich text field, we need a stream field. We probably actually don't even need a rich text field anymore, but I'll leave that in there for the sake of being lazy. We need from Wagtail. core import blocks. These are going to be our stream field blocks that we use. Admin edit handlers instead of a field pan just a field panel. We also want the stream field panel. And we also want do do do do do

45:06

from Wagtail dot images dot blocks import image chooser. block. And so we're just importing stream fields directly into this page. Now, with larger applications, you usually have like a blocks. py file, like your own blocks. py file where where you'll make your own custom stream fields. But we're not going to do that here. We're just going to Basically inject stream fields directly into our model. So I'm gonna take this body and I'm going to delete it. And we're gonna throw a new body in here Body is going to be equal to a stream field with a char block, a paragraph with a rech text block, and an image with an image chooser block. And that body now needs to change in here as well. So it needs to be a stream field panel.

45:54

And let's go ahead and make some migrations. Make migrations. And migrate. And start server. Let's go ahead and see what this looks like in our admin. Let's edit go to the best. And we no longer have that body rich text in there, we have stream fields. So paste that in there, greatest of all time. Paragraph Put one lorem in there, put a smaller lorem in there. Goats are awesome. Has an H2 and I don't know, maybe in here we'll say. Wagtails are awesome as well. And let's add an image. And this is a stream field image, so it's different from our regular image down here, but I'm going to use the exact same image anyways.

46:42

And publish this. Let's go ahead and see what our API is returning in our detail view. So we're looking at page number five And we can see in our body we now have a list or an array of items in here. We've got uh type is equal to heading, paragraph, and image, one of each, and that matches heading, paragraph, and image. We've got the value of it. So in this case we've got rich text value. In this case we just have regular text value. And in the image one, we actually just only have the number one. And that's because if we go into the detail view for images, number one, this is the image that we chose. Now let's go ahead and open up this news item and I'm just gonna resave

47:29

this as news itemstreamfield. html And where we have this in here, the VHML is equal to item body, I'm actually going to revert this back to what we originally had, item dot body. And let's go ahead and take a look at this. So it's the news itemstreamfield. html file that we're looking at. So news itemstreamfield. html And here we can see we have basically all the JSON returning in here. Now that's great, we're getting the response that we want, but this is super ugly. This does not work for single page application, and no user is gonna look at this and go, oh yeah, this is obviously something that I want to read. So we're going to have to work with this a little bit. Now thankfully this is actually super easy to do.

48:16

Now instead of having item. body, what we can do is We can say for every block in item. body, let's go back to our API here and reference that. For every block, so we've got a block here, we've got a block here, and we've got a block here. We're going to call it block and we're going to then verify that the type is equal to a heading, paragraph, or an image By saying V if block type is equal to heading, uh else if block type is equal to image, else if block type is equal to paragraph, two different things. So if there's a heading, throw it in an H2. If it's an image, throw it in an H2, but just show the image value in there. We'll get to that one a little bit. That's going to be a little trickier. And then we're also going to say if that block type is a paragraph.

49:03

Do the V dash HTML is equal to the block dot value. So now if we save this page and refresh. Hey look at that! So this is actually turning out pretty well. The one little caveat here is this fella. This is your image ID. Now I'm actually going to leave that up to you to do. Whether you want to do that in Vue or React or Angular or any other service or front-end framework that you want to use, by all means go for it. But that is your image ID. Now that ID matches this value right here, that number one. And again, if you go to slash API slash v2 slash images slash one, again, that's I'm just getting that number one from this value down here. You can see that there's actually more in here.

49:49

And so if you wanted to, you could put uh the download URL, you could create another image rendition, you could do all sorts of stuff in there. Don't forget that the Wagtail V2 API actually piggybacks off of Django REST Framework quite a bit. So you can use your custom serializers as well. So if you're already familiar with Django REST Framework with DRF, you can create your own custom serializers. Now at this point we actually have one major problem. It's actually a huge problem. Is when we go to edit this, let's go and preview our page. As content editors, we want to preview our page. We want to make sure our content looks nice. We want to make sure we're using the right images. And when it comes to a single page application, There are no templates. Not available anyways, because a template should ideally well a Django template or a Wagtail template should ideally be on the same server that's being accessed

50:39

to edit your page. Well in this case that doesn't exist and we're not going to make it exist. Instead what we're going to do is we are going to use A different service. Now we're going to get about halfway there and I'm going to leave the uh remainder of that up to you, the the front-end stuff. I'm not going to go into all the front-end stuff, uh, just because that could be it's own series on its own really, because there's just so much that we could go over. But there's this package here called Wagtail Headless Preview. And it's by your friends over at Torchbox who have very kindly open sourced this for everybody. So what we're going to do here is we're going to pip install Wagtail Headless Preview. So I'm going to cancel my server, pip install Wagtail Headless Preview.

51:26

And I'm really just going to follow these setup steps pretty much word for word. So I'm going to go to base. py. And install wake dial headless preview. Now you're gonna want to read through this as well. So it does say that we need to make migrations, so let's go ahead and make migrations. It comes from a package, so we don't need to make our migrations. We just need to apply migrations rather. We also have headless preview clients, so we have a single site in here. Now this says it has to run on port 8020, but it doesn't actually have to. What we could do do do do do do is we could say this could run on port 8000 and one, but it doesn't really matter. I'm gonna keep it as 8020 just for the sake of being a little bit different from the rest of the video, but also staying more in sync with what this package is asking us to do.

52:19

Now if we have multiple sites, we would set up different the different sites here. So we'd have like site1. example. com would map to localhost 8020 or 8021 or anything like that. If you want headless preview live, you could do that as well. It does have a little note here that it might require something like Django channels or some other WebSocket or async libraries. We're not going to get into that because we don't want to get into that quite yet. Now for usage, we have to add this to all of our classes, all of our pages. So we go back into news models dot pie and let's just import that and we're gonna use that mix in Then if we quickly read through the rest of this, this is basically just saying, hey, make sure you have your Wagtail API

53:09

v2 application installed. That's important. Create a new api. py file. We're going to need to actually edit this. Our API file? We already have one, but it's not quite everything we need. We do need this content type in here. We're going to need the page preview and the rest framework response as well. So let's go ahead and import both of those. And really all we have to do is do do do do do let's go ahead copy all that stuff. And I just throw that at the bottom of the page. So what this is doing is it's registering a brand new API endpoint called page underscore preview. And it's going to run this class called Page Preview API Endpoint.

53:58

It's going to say, I understand that there are query parameters that you want, but also token and content type are going to be allowed as well. It's going to go through a bunch of other stuff which I don't think we really need to go into too much just for the sake of this video. If you want to definitely explore it and also explore the repo itself. It's actually a really well-made repo. And tons of people are using it, so I highly, highly suggest using this one. So I'm just gonna save that, keep on scrolling down, keep on scrolling down. We want to make sure that we have API slash v2 in our URLs. We already have that. And lastly, it says we want to add a file called Client slash index. html in our project root with this stuff in here. So let's go ahead and copy that.

54:44

And this is our project root. This is our Wagtail site So I'm going to nope, that's not right. I'm going to create a new file called client slash index. html. And it would be really great if I didn't typo that. There we go, it's in its own folder, and just paste that stuff in there. Let's go ahead and rerun our server. There's a couple more notes in here as well. Core 's origin allow all true. It wants us to set the URLs regex. We might as well set that as well. That doesn't hurt to have that in there. So let's do our base. py file. Throw that in there. And the last thing it's saying here is, A, we need to run our Django Wagtail website, which we just did, and then we need to serve the front end.

55:35

And we need to actually serve it from the client folder itself So what I'm gonna do here is cancel this. I'm gonna go to sites C D backend. Hello, C D where are you? Are you in there? There you are, client. And we've got our index file in there. So if I do cat index. html, we can see that's exactly what I pasted in there. Now it wants us to run python-m http. dot server on port 8020. Now that 8020 is important. That's exactly what we specified. We said that in our settings here. That's what it is using. So now we have a front end server running on port 8020 and we've got a back end

56:22

server running port 8000. So let's go ahead. I'm just gonna refresh this page and let's click preview. And you're gonna see that we no longer get that template does not exist there. We're going to get a ton of JSON and this is actually good. This means that at this point, as a back-end developer, your job is done. And really you just need a front-end developer, or if you're a front-end developer, you just need to take all this JSON and make it look more like what we had seen before. So now your front-end developer, or maybe that's you, uh you have all the data that you need to make this look nice. And all you really have to do is edit your client slash index. html file. Make sure you take a read through this though.

57:07

There's some pretty interesting stuff going on here. It's really, really nice and simple. It's super super short. But at the same time, it's also using something very important, such as your content type and your token. So that is how you create a headless Wagtail website using view. js. You can also use React or Angular or any other front-end framework. I just used view. js because it is the simplest to get started with and the simplest to make the most progress with right away without getting into too many complexities. Now I can't actually take credit for any of this tutorial. All I did was make the video. Our Waketail friend Tom Dyson over at Torchbox actually made this entire tutorial. He wrote it out. I'll leave the link to this tutorial in the description down below. And so at any point in time you're like, oh, you know what?

57:54

I don't remember how to do X or Y or What was that little bit of code that Caleb wrote? You can just reference Tom's code and it's almost identical to what I showed you in this video. A couple additional resources. If you are somewhat new to learning Wagtail with the whole headless thing, go to learnwagtail. com and you can type in headless in search and you'll get All of these search results for headless CMS. So this will teach you things like fetching data from the API, image rendition field, custom Wagtail image serializer, which is something that you might actually want to take a look at. exposing orderable data and stream fields and things like that. So definitely take a look at that. Also if you want to uh work on your view mastery you can actually go to

58:40

Viewmastery. com Take a look at that site. That is a cool looking website Or if you need a quick little view cheat sheet, here's a good one. Codepop. com slash viewessentials cheat sheet. pdf. Again, I'll leave these links in the description down below for you as well.

Questions this talk answers

How do I enable the Wagtail v2 API?

Add the Wagtail API v2 app, configure the pages, images, and documents API endpoints, register the API router in your URLs, and include Django REST framework. The API can then be queried at an endpoint such as `/api/v2/pages`.

Discussed at 7:48

How do I expose custom Wagtail page fields in the API?

Add the fields to the page model’s `api_fields` using `APIField`, then request them with the API’s `fields` query parameter, such as `fields=intro,body`.

Discussed at 10:57

How do I fetch Wagtail API data in a Vue.js app?

Use Axios after the Vue app mounts, assign the response data to a Vue data property, and loop over that property in the template to render the results. The talk demonstrates this with a news listing and a detail page.

Discussed at 17:14

How do I fix CORS errors when a Vue frontend accesses a Wagtail API?

Install and configure `django-cors-headers`, add its app and middleware to Django, and allow the frontend origin. For local testing, the workshop uses `CORS_ORIGIN_ALLOW_ALL = True`, while noting that a production site should use an origin whitelist.

Discussed at 24:13

How do I render Wagtail rich text as HTML in Vue?

Bind the returned body content with Vue’s `v-html` directive instead of normal text interpolation, so the HTML markup is rendered rather than displayed as escaped entities.

Discussed at 28:44

How do I add routing for a headless Wagtail site built with Vue?

Use Vue Router with route definitions for the listing page and a parameterized detail path such as `/news/:id`. Router links navigate between the pages, and the detail component uses the ID to request the corresponding Wagtail API record.

Discussed at 33:12

How do I return a resized Wagtail image rendition through the API?

Expose the image with an `APIField` using Wagtail’s `ImageRenditionField`, specifying a rendition such as `fill-300x300` and the image field as its source. The API then returns a URL for the generated 300-by-300 rendition.

Discussed at 38:57

Presenters

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

More videos by Tom Dyson

More videos from Wagtail CMS