Blog Listing Page, Blog Detail Pages, and Adding Custom Context to your Wagtail CMS Site

This video is from Wagtail CMS 2023 .

Blog Listing Page, Blog Detail Pages, and Adding Custom Context to your Wagtail CMS Site
0:31:36
Published November 29, 2023
17,540 views
252 likes

In this (long) lesson we're going to cover three main topics. They all go hand-in-hand with Wagtail CMS so it makes sense to learn them all together. The three topics are:

  1. Listing Pages
  2. Detail Pages
  3. Adding Custom Context

Tutorial: https://learnwagtail.com/tutorials/how-add-blog-listing-page-blog-detail-page-and-custom-context/

GitHub Commit: https://github.com/CodingForEverybody/learn-wagtail/commit/f184c8e6ebcd5f9769aed8bbc9aee3149f520667

Learn Wagtail from scratch with the official Wagtail for Beginners Course
https://learnwagtail.com/wagtail-for-beginners/

Used in this video: Wagtail 2.4, Python 3.7, Django 2.1.5 #Wagtail #Django #Python

Summary

The speaker builds a Wagtail blog using two page types: a listing page that contains blog detail pages and a detail page with a title, image, and StreamField content. They create a blog app, define the page models and admin panels, run migrations, and add templates for displaying published, public posts with links, image renditions, and rich content. The listing page adds a custom `posts` queryset to its template context, illustrating how to extend Wagtail’s default context. They also show basic navigation, template debugging with Django Debug Toolbar, and the control Wagtail provides over queries, rendering, and performance.

Key takeaways

  • Create separate Wagtail page models for a blog listing page and individual blog detail pages.
  • Use `get_context` and a live, public queryset to add blog posts to the listing template.
  • Build templates that link to each detail page and render image renditions with Wagtail image tags.
  • Use StreamField blocks to display rich text and other flexible content on detail pages.
  • Run migrations before using new page models, and use Django Debug Toolbar to inspect templates, context, queries, and timing.

Summarised automatically from the transcript.

Transcript

5,308 words · auto-generated Show

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

0:00

Hello, welcome back to another episode of Learn Wagtail. We are going to speed things up a little bit. And we are going to be creating a blog page, so a blog detail page. We're going to be creating a blog listing page. And we're also going to learn how to throw some extra content into uh well the context of a listing page so we're basically going to create a listing page with all the detail pages inside of it And well that's about it. So this might be a little bit faster, and this might possibly get split up into two or three videos depending on how long this goes. But remember at the end of this you will be able to reference the code directly on GitHub, so either way you're going to have example code that you can tinker with.

0:47

Now I'm already inside of my pipenv. All I need to do is run python3 manage. py run server, and that's going to run my server for me. In the last lesson, we created a subscribers model for our blog so that people could actually subscribe to it. But in this lesson, we're going to take a little bit of a turn and instead of working with Django models, we're going to work with Wagtail pages. So under our homepage here, we only have the about page, but we want to add a blog listing page, and then underneath that we also want a blog detail page. Now we are going to have to create a brand new app because we don't have one in here called blog. So let's go ahead and create our blog. And all we have to do for this is type python3 manage. py start app blog.

1:34

And I'm going to run my server. And now you can see that we have a new directory in here called blog. Naturally we have to add that into our installed apps, so let's add that in here. We'll call that blog. And we can close down base. py probably indefinitely for the duration of this course, or this lesson rather, not this course. And here is where we're going to have a blog listing page. So we have, let's do some pseudocode here. We have a blog listing page. We know that's going to be a Wagtail page. And that's going to be a class. We also have a blog detail page, which is where blog pages are actually going to live, and that's how they're going to exist. That's also going to be a class. And in here, we are going to have some sort of way of getting

2:20

all the blog detail pages and putting them into the context of this template. Basically that is it. In a nutshell, it's really really easy. In real life, it's really really easy. So let's go ahead and uh we're going to want to keep this one because we're gonna want fields. But let's also go ahead and add our Wagdle Core models and we're going to import page. And so that allows us to create a blog listing page. Now I'm going to get rid of that and I'm going to comment this because before anything we are going to need Well, we're going to need a listing page. So listing page lists all the pages Nah, it's vague

3:05

blog detail pages. So at this point there are there is no pagination. There's not going to be anything like that. It's just going to list the well all of the blog pages that we have. They don't currently exist, but they will. So what does this page need? What does a typical blog listing page need? Well a blog listing page usually comes with a list of all the blog detail pages. uh tags it can come with categories blog authors these things are extendable we're not going to cover all of that in this lesson It also needs a title, but it also needs to be able to get all the blog detail pages. So just because I don't want this to be completely blank, let's add a custom title, and this is going to be models. charfield.

3:52

Max length is equal to max length is gonna be a hundred. Blank is equal to false, null is equal to false, so there always has to be a custom title in here, and the help text is overwrite the default title. Just something like that. And then we're going to want to add some context. Now we can't really add context yet because we don't have a blog detail page, but we can get this set up. So we can write def Get context, and this takes self as usual because it's an object. It also takes request, all of our args, and all of our keyword args. And

4:39

Adding custom stuff to our context. We're gonna leave it there. It's gonna be kind of vague. We might come back and actually adjust that doc string in a little bit First thing we need to do is we need to grab that context and so we grab the context by bubbling up. So basically we're going to run this function called super dot get context and we're going to pass in request. We never have to pass in self because self is always assumed So we're gonna pass in request, we're gonna pass in our args, and we're gonna pass in our keyword args. And then we're just going to return context. And that's it. And then if we ever wanted to add anything extra, we could do uh extra is equal to read all

5:25

about it. Something like that, which we're gonna come back to in just a little bit. So that is our blog listing page. Now we need a detail page. Detail page usually has a lot more on it. For example, a detail page will often have stream fields. A detail page will have a banner image. It will have all these different things. So the first thing we want to do is maybe let's add a banner image. Let's add a custom title. And maybe let's get set up with a couple stream fields that we already have existing. So the first one was a custom title, but let's always add a doc string in here. Blog detail page. I'm not going to write custom title again, that already exists. I'm going to copy that. Now let's say there needs to be a blog banner image or some sort of blog image to signify that there is a blog in here.

6:14

It just needs to be some sort of image. So let's add an image in here. It's called blog image because image seems to be quite vague. And let's do models. foreign key. And this is going to be a foreign key to Wagtail Images. image. Blank is equal to false. Null is equal to false The related name is going to be plus because we don't need to use that related name. And lastly, on delete is equal to models. set null, I guess. So we have a custom title, we've got a blog image, and then we need some stream fields. Let's just open up our homodels. py because there's a stream field in here. Actually, better yet, let's open up flexmodels. py because there's a lot of stream fields in here.

7:00

So let's go and copy all of those. And I'm simply copying and pasting because I'm going to be using all the exact same stream fields. And then we need content panels. We also need that on our listing page. That's something I forgot. So we have content panels is equal to the default page content panels plus, we don't have subtitle in here, but we've got custom title and blog image and content. So we've already got content in there. Let's do custom title. And this one is going to be blog image. But because it's a foreign key to an image, we're going to put this as image chooser panel And let's copy that up. And let's put that in our listing page.

7:46

Add a colon there. Thank you, Syntax Highlighting, for showing me that. So we have a custom title, we don't have a blog image, and we don't have content, so all we're doing is we're adding that custom title to our blog listing page. So we're sort of skipping back and forth here between the listing page and the detail page. And this is very common when you're adding Something like a blog where you have a listing page and a detail page. Often you're going to build these together. So we have our stream fields, we've got our content panels. And our blog detail page does not need additional context. It's going to get everything it needs from the blog image and the stream fields But we need to go and import all of these, so I'm gonna copy all of that, move that up here, copy

8:32

and paste all of that, and change that to blog listing and blog detail pages. And all I did there was I made sure that field panel, stream field panel, stream field, and page are all imported. Now let's see if there's anything that's missing in here. We can take a visual look or in VS Code at the bottom here we can see Undefined name, image chooser panel. That's a good one. So let's import that from Wagtail. images. Not page. Image chooser panel. And Flake 8 says there's no complaints. Things are okay, so let's open up our terminal. And aha, our terminal caught something for us.

9:19

So it says here, field specifies on delete is equal to set null, but it cannot actually be null. So for simplicity, I'm going to say that the blog image in fact can be null, although it can't be blank. So when someone adds a page or edits a page, this has to this has to have some sort of image selected in there. Go back to our terminal, and everything's looking okay. Now if I open up Wagtail again, go into pages, let's view our homepage and add a child page. It says that we can add a blog detail page and we can add a blog listing page. Now let's add a blog listing page and have all the detail pages live under it. Oh no, we get an error. Now, if you have been following me throughout the entire Wagtail

10:04

course, this should be somewhat familiar to you. We've run into this a number of times, and basically the problem is. The problem is that Wagtail is saying, I'm gonna go look something up in the database. I'm gonna look up a certain table, bunch of columns, and the database is saying, mmm, actually there's nothing there, so I can't give you anything. And Wagtail just elegantly handles this with a 404. Now if we open up our terminal, we can actually see that we are getting a 404 in here. And really all we have to do at this point is we just need to run migrations. So Python3 manage. py make migrations. This will make our migrations for our WagTail blog. Python 3 manage. py migrate. This will migrate all of our changes into our database. Python 3 manage.

10:50

py run server will rerun our server for us. And when I open up Firefox again or Chrome or whatever browser you want, here we have our new blog listing page. So we're just going to call this blog. And we want to make sure that the slug is always blog because it's going to be your website. com slash blog. It's pretty standard. Custom title is going to be Startup life blog. Now when I view this, it's going to say template does not exist. This is expected behavior. We'll get to templates in just a moment. Now we want to add a child page to our blog. So we want to add a blog detail page. And this is going to be blog post number one.

11:35

Custom title is going to be the same thing. In fact, actually let's overwrite that with custom just for the sake of. showing that's custom. And uh let's use that image and some content. Oh look at that we have all of our stream fields ready to go. Isn't that nice? So let's add full rich text and let's put hello world make that an H2. This is a sample blog post from a full, whoops, from a full rich text Stream field, that's all it's gonna be. Pretty lame for rich text, but that's what we have. And let's go ahead and copy this page.

12:20

So copy, we're gonna change this to two. Just because I want at least two of these pages. And there's a couple things I want to edit in here. So The custom title didn't change, didn't give me an option to change, and that's okay. I'm gonna go and change that manually. Hello world 2. Put a 2 or something in there, maybe change this image. We'll make it that lady's face and then publish. So we have blog post one and blog post two, just like that. Now we have a bunch of pages, and again, if we view this, it's gonna say template does not exist. We're expecting that. That's okay. The first thing we want to do, however, is work on that listing page. So let's view live this listing page.

13:05

We have localhost 8000 slash blog. We need a blog listing page. And in fact, this is where it's looking for it. And because I like to be explicit In our blog listing page, I'll make this just a bit smaller here. I hope you can still see that. I'm going to add a template in here. Template. Template is equal to blog slash blog listing page. Now I'm going to close up a couple of these and where is my site? Templates and I'm going to add a new file in here called blog slash blog listing page and this is going to extend

13:51

Base. html. This is very, very similar to pretty much any other page we've done. For instance, homepage, it extends base. html. It's the exact same thing up here Now because we're working on our blog listing page, what do we want to see on this blog listing page? And this is something that we have to think about ahead of time. Well, you don't have to, but you probably should. So our blog listing page is going to list all of our blog detail pages. And those blog detail pages have a custom title and they have a banner. Or some sort of image. So we need to be able to show an image. So let's do load wagtail images tags. This allows us to create image renditions. Let's create a new block.

14:40

Block content. And let's go here and end block content And anything in here shows up. And let's just demonstrate that. Okay. And I actually made a mistake there where I named the file blog listing page. It should have been bloglistingpage. html. If you caught that, good eye. I hope you weren't screaming at me too loudly there. So now when we refresh our page, it says nothing in here shows up. And that's exactly what we told it to do. It says nothing in here shows up. And let's go ahead and change this to a Django template for some better syntax highlighting. And now at this point we're going to want to loop through all of our blog posts. But what do we loop through? It's at this point we're thinking, I don't know what we're supposed to be looping through.

15:28

So we need to figure out how to do that. Now, in our listing page, I kept talking about this context thing. We're going to be adding our blog detail pages to the context. And this starts to deal with Django's ORM a little bit. So we're going to add context. Posts is what we're going to call it, short for blog posts, and we're going to use blog detailpage. objects dot live dot public And what this is going to do is this allows us to grab all of these blog detail pages, grab them, make sure that they're live so that they're not in draft mode, and make sure that they're public. So lastly, make sure you always have that equal sign in there. And when we go, oh, I I'll show you what this public actually looks like.

16:13

So we've got. Different visibility settings. So is it public? Is it accessible to logged in users only? Is it accessible with the following password? So you can set a password, or is it accessible to users in particular groups such as editors or moderators? For us, we want all the public ones. But now when we refresh our page, again, still nothing happens, and that's again because we have to be very explicit. Now if any point in time you're thinking to yourself, wow, this is a lot of work. Technically it's not. It's just that we have been trained to be lazy with nice systems like WordPress where we can click, click, click, click, click, and everything just works. But developers still have to put in the effort. The nice thing about Wagtail is that we actually have to put in a little extra effort. And yes, that is a nice thing because that means we have full control over the code.

16:59

We have full control over the performance. how it looks, how it acts, it does exactly what we need. So there's no more accidental errors. There's no more accidental problems. Things work because we told it to work, and if it breaks, it's because we told it to break. That kind of power is very, very nice to have in web development. So now we have these posts. If you're not familiar with what this is, basically it's getting all your blog detail pages that are live and public, and it's going to put them into basically a list. And this list is called a query set. So we can actually loop through query sets. So let's do that now. Let's for post in posts And let's be just a little bit boring for the time being and put post. title and then let's put a line break underneath it

17:47

And we're gonna refresh our page. We have blog post 1 and blog post 2. Would you look at that? Now this doesn't look very nice, so let's add a little bit of styling to it, and maybe let's add our image in just a moment. Div class container. Let's actually move that up. We don't need that inside of our loop, although it doesn't really hurt. And I'm just cleaning this up so this is a little more legible. I'm going to add a new row here. So div class row. Close the div. And I'm going to add a column in here. So let's put the image on the left and let's put some title on the right So this row is going to have a column on the left, so

18:33

call SM4, which doesn't work with this kind of template highlighting, unfortunately. Not in VS Code anyways, and splime it does. Image here and then div class call S M eight I was gonna put post title there, but we can actually just do post dot title And let's add a link to our post as well. So let's make this a link. So A-H-R-E-F is equal to something or other. Slash A and this is really easy too. So because it's already in this loop, where did we get post.

19:20

url from? Post. url comes from the post object. So in posts It's gonna get two blog posts, and we've actually seen that here. It has blog post one and blog post two. In each one of those loop iterations, It's actually giving us the entire blog detail page. So we can use the custom title, which we should be using, and a blog image. So let's use that custom title now. We'll do custom title. And let's refresh and see what happens here. So the spacing is still not fantastic, but there is something there at least. So we have an image here and we have a blog post one and blog post two and it actually links. So if we were to click on this in our URL, just ignore that error for a moment.

20:08

In our URL it says blog slash blog post dash one. And if we click on number two, it says blog post two. So now we actually have a listing page, and we have learned how to use our context inside of our listing page. Lastly, let's go and make that image actually exist. I actually want to make these columns just a little different here. And Let's do an image in here. So image post dot what did we call it? Blog image post dot blog image Fill, let's make it a square by 250 by 250 as blog image. And I'm gonna put an image in here S R C and an alt tag because that's always good.

20:54

And I also want to wrap this in a link because a lot of people tend to try to click an image. So let's do this. H-R-E-F is equal to Whatever that's gonna be, and we'll fill these in just a moment. So our link is going to be the exact same. We want to go to our post URL. And our image source is going to be our blog image. url. I got blog image, by the way, from here. You could rename that to anything you like. And let's put an alt in here, blog image. alt. Save and refresh. Ta-da! So we have something in here now. We've got our custom title, we have our images, that's all clickable. So at this point, we are actually done with our blog listing page

21:40

Behind the scenes, I'm going to go and make this a little bit prettier. If you wanted to, you could also add like a little blog summary, which is not actually a bad idea. You can make that rich text as well, and you could limit the fields that you want on it But the last thing we need to do is we need to make this blog post actually work. The detail page does not work, so let's go ahead and create a blog detail page. So under templates, I'm going to create a new file called blog, blog detail page And I'm going to copy the extends because it always needs to extend base. html. The blog detail page, will it need wagtail images? Yeah, it probably should because it has that image in there. So let's also put that in there. So let's do blog.

22:27

Remember, we're using self, not post this time. The reason we're using self is because this is the blog detail page. The blog detail page is this. So self. blog image. That's object-oriented programming for you. Self. blog image And let's make this ginormous. This is a terrible, terrible practice because I'm using one image in two very differ different ways, but let's Let's go with it anyways. Let's just see how gross this can get, I guess. So fill 900 by 400, that's gonna be our blog image, and we're going to call this banner. Like Bruce Banner. And then we also have to add block

23:12

content and let's do an end block and block. And let's throw our entire image in here. So we've got banner. url, and we have banner. alt. So at least when we show the page now we're going to see something. Or at least you think we would see something, but we didn't. Now why is that? At this point in time we should be looking at our code going oh something right something wrong did our banner image work? Let's let's look at the source. So we go inspect element and we're looking for that image tag The source is empty, the alt is empty. Now why is that? Let's look at our blog post number one. Blog

23:58

post number one. We have a blog image. And in fact the problem is, is that I should probably put that inside of our content here so that all the content is together inside of a block. There's our image, our very gross image. And let's actually let's change this to have a style of a width of 100% Height is going to be auto. And let's change that from 900 to I don't know 1200 by 300. Okay, so we have some sort of blog image there. Now we need a title. So let's go ahead and add a container. Let's add text center and we need

24:43

the blog title and that was called custom title if I recall correctly Custom title, there it is, custom title. Now that should probably be larger. Let's make that an H1. Margin top 5, margin bottom 5, something like that. Alright, things are looking okay. Now let's look at what else we need to add in here. We have a custom title done. Blog image done, and now we need our stream fields. Now our stream fields, if you don't remember or if you weren't around for that lesson, we actually have stream fields on both of our homepage and our flex page. So let's just re-reference our flex page real quick Let's go flex page

25:29

models. We have our content, our stream field in there. Now our stream field panel exists. So now if we go into Flex, FlexPage, we can see that we have a loop in here, four block in page. content. Basically, that's looping through every stream field. All we want to do is copy this over. Two here. Now if you're wondering, Caleb, what's the difference between page and self? Technically nothing. Behind the scenes, they are the same thing. You can use self. content as well. Now this is not going to work because we need to grab a template tag. And we're going to get that from Wagtail Core Tags.

26:17

Now let's go and refresh our page. And look at that, we have some rich text in here. And in fact, we don't really know how that rich text is working, so let's take a look at a rich text block. We do in fact have a container in there. Now I feel like that's a little bit wide still. So let's go ahead and add another container. We're going to add a container with a row in it. Container. A row. Column large, let's make this eight and offset large two And just pop that in there.

27:04

Fix up my indenting because I am a stickler for nice indenting. Yeah, okay, that didn't work as well as I expected it to. And that could be because I there we go. Little debugging for you. LDLG That's all it was was a little typo. Cool, so now we have a blog listing page. We've got a blog detail page. Lastly, let's go and throw a link up to our header. So Let's open up header. html. That doesn't exist. That's probably because it's in base. html, and we have not split that out yet, but this could be a good time to do that. And I'm going to uncomment this.

27:52

I just want to show you what this is going to look like. Now I just grabbed this from a site. We have search home features pricing about. Now we don't want search in there yet, so let's get rid of that. Get out of here search. And we want to change home. Home will always be home features. Let's change that to blog. Now I'm hard coding these. There's a better way to do this. Just because if that blog slug ever changes to let's say slash news, that could be a problem. And let's also do about Because I think we have an about page. Again, that's the problem is I think we have an about page and I think it's slash about

28:39

Here we go. We have our homepage. We have a blog. So we have a blog listing page. It's ugly, yes, but we can fix that, and I will fix it behind the scenes. We have an about page. Oh hello, there is an about page. And going back to our blog page, we actually have blog detail pages. And all we have to do is click on one of those, the title, or the image, and it will show us our blog page. Now a long time ago I got you to install Django debug toolbar and I said it was really really useful. Why is that useful Well let's take a look at this. We have four static files. The time took 278 milliseconds to process on my computer. Pretty slow actually. It's using 17 queries, so that's an acceptable amount. But mostly let's take a look at these templates I now know that the blog detail page is using blog slash blog underscore

29:28

detail page. Can I make that larger? Yes I can. I know that it's using base. html. I know that it has Wagtail userbar in here. So we've got some Wagtail admin stuff, which only shows up if you're an admin and you're logged in Look at the stream fields that I I currently have enabled right now and and I'm I'm using on this page. I have streams slash rich text underscore block. html. I can see the context that's being thrown into it, and I can also see the actual stream field itself. So now I have the ability to have a larger blog page and I can see exactly what stream fields are being pulled in and what What it's doing behind the scenes. So for instance, this one has a container, margin bottom small five, margin top small five.

30:14

And all it is doing is calling self So there we have it. This was a very packed lesson. We learned a lot of stuff in here. We learned how to make a listing page. We learned how to make a detail page. We also learned how to uh what else did we do in here? We did some we added some extra stuff to our context. And we also worked with templates. So we learned quite a bit in this lesson Now I'm hoping that this lesson, because there are technically three subjects in here, the listing page, the detail page, and also the custom context.

31:00

I hope that that did not confuse you too much. If it did, you can always reference the code on github. com. It's always available for you there. There are also more tutorials available on learnwagtail. com. You can always access the docs at docs. wagtail. io. And as always, I am Caleb Tullian. I am the voice behind the video. If you like this video, don't forget you can subscribe, thumbs up, leave a comment, share it with your friends, share it on Slack, share it in Facebook groups, share it on Reddit. Sharing is caring, or even if you don't share, I just hope you learnt something useful in this video.

Questions this talk answers

How do I create blog listing and blog detail page models in Wagtail?

Create separate Wagtail `Page` subclasses for the listing and detail pages. The listing page can have a custom title and context logic, while the detail page holds fields such as a custom title, required blog image, and stream-field content.

Discussed at 1:34

How do I add blog posts to a Wagtail listing page’s context?

Override `get_context`, call the parent implementation, and add a `posts` value containing live, public `BlogDetailPage` objects from the ORM. The template can then loop over that queryset.

Discussed at 15:28

How do I create a Wagtail blog detail page template?

Create a detail-page template that extends the base template, renders the page’s blog image through an image rendition, displays its custom title, and loops through `self.content` using Wagtail’s stream-field template tag.

Discussed at 22:40

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 from Wagtail CMS