Guide: Listing pages¶
List views (article archives, category pages, search results) need
pagination-aware canonicals and titles. SeoListViewMixin handles both.
Basic list view¶
# blog/views.py
from django.views.generic import ListView
from seo_suite.mixins import SeoListViewMixin
from .models import Article
class ArticleList(SeoListViewMixin, ListView):
model = Article
queryset = Article.objects.filter(published=True).order_by("-published_at")
paginate_by = 20
seo_title = "All Articles"
seo_description = "Browse the full archive."
Pagination-aware title¶
When ?page=2 is in the request, the title automatically becomes
"All Articles – Page 2". Page 1 renders as "All Articles".
This behaviour comes from SeoListViewMixin.get_seo_title() and requires no
extra code.
Pagination-aware canonical¶
By default, SeoListViewMixin sets the canonical URL to:
/articles/on page 1/articles/?page=2on page 2,/articles/?page=3on page 3, and so on
This is the recommended behaviour: it tells search engines that each paginated page has its own canonical URL rather than all pointing to page 1.
To disable page numbers in canonicals (all pages point to /articles/):
Category pages that are also listing pages¶
A Category model can be both the object providing SEO metadata and the
page presenting a list of items. Use both mixins:
# blog/models.py
from django.db import models
from seo_suite.mixins import SeoModelMixin
class Category(SeoModelMixin, models.Model):
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
slug = models.SlugField(unique=True)
def get_absolute_url(self):
return f"/categories/{self.slug}/"
# blog/views.py
from django.views.generic import ListView
from seo_suite.mixins import SeoListViewMixin
from .models import Article, Category
class CategoryDetail(SeoListViewMixin, ListView):
model = Article
template_name = "blog/category_detail.html"
paginate_by = 20
def get_queryset(self):
self.category = Category.objects.get(slug=self.kwargs["slug"])
return Article.objects.filter(category=self.category, published=True)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["category"] = self.category
return ctx
def get_seo_title(self, context=None):
# Delegate to the category object for the base title; the parent
# class appends " – Page N" automatically.
if hasattr(self, "category"):
return self.category.name
return None
def get_seo_canonical(self, context=None):
# Let the parent build the paginated canonical from the request path.
return super().get_seo_canonical(context)
Note
When the view provides a title via get_seo_title, it runs at priority 50
(view layer), which overrides the object layer (priority 40). The category's
description and other fields are not automatically pulled in through the
view. To include the category object's full metadata, pass it as obj to
attach_seo or override get_context_data to call resolve_seo(request,
obj=self.category). For most listing pages, setting seo_title and
seo_description directly on the view is simplest.
Pure listing views (no model)¶
For pages like a homepage or a static archive page, set attributes directly:
class HomepageView(SeoListViewMixin, ListView):
model = Article
template_name = "home.html"
queryset = Article.objects.filter(published=True)[:5]
seo_title = "My Blog"
seo_description = "Thoughts on Python, Django, and the web."
h1 independent from title¶
The <title> tag and the visible <h1> heading on the page are often
different. Set seo_h1 to control the heading independently:
class ArticleList(SeoListViewMixin, ListView):
model = Article
seo_title = "Articles | My Blog" # browser tab / search result
seo_h1 = "All Articles" # visible page heading
Access it in the template: