指南:列表页

列表视图(文章归档、分类页、搜索结果)需要能感知分页的 canonical 和标题。SeoListViewMixin 同时处理这两项。


基本列表视图

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

感知分页的标题

请求中带有 ?page=2 时,标题会自动变为 "All Articles – Page 2"。第 1 页渲染为 "All Articles"。

这一行为由 SeoListViewMixin.get_seo_title() 提供,不需要额外代码。


感知分页的 canonical

默认情况下,SeoListViewMixin 把 canonical URL 设置为:

  • 第 1 页:/articles/
  • 第 2 页:/articles/?page=2,第 3 页:/articles/?page=3,依此类推

这是推荐的做法:它告诉搜索引擎每个分页页面都有自己的 canonical URL,而不是全部指向第 1 页。

要在 canonical 中去掉页码(所有页面都指向 /articles/):

# settings.py
SEO_SUITE = {
    "LIST_CANONICAL_INCLUDES_PAGE": False,
}

同时作为列表页的分类页

Category 模型既可以是提供 SEO 元数据的对象,也可以是展示条目列表的页面。两个 mixin 一起用:

# 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

视图通过 get_seo_title 提供标题时,它以优先级 50(视图层)生效,会覆盖对象层(优先级 40)。分类对象的 description 等其他字段不会自动经由视图带入。要包含分类对象的完整元数据,可以把它作为 obj 传给 attach_seo,或者重写 get_context_data 并调用 resolve_seo(request, obj=self.category)。对于大多数列表页,直接在视图上设置 seo_title 和 seo_description 最简单。


纯列表视图(无模型)

对于首页或静态归档页这类页面,直接设置属性即可:

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

独立于 title 的 h1

<title> 标签和页面上可见的 <h1> 标题经常不一样。设置 seo_h1 可以单独控制页面标题:

class ArticleList(SeoListViewMixin, ListView):
    model = Article
    seo_title = "Articles | My Blog"  # browser tab / search result
    seo_h1 = "All Articles"           # visible page heading

在模板中这样使用:

<h1>{{ seo.h1 }}</h1>