指南:博客文章与文章页面

本指南介绍如何为你自己的模型附加 SEO 元数据,从零配置的命名约定,一直到可在后台逐条编辑的字段。


第 1 层:按约定命名字段(无需配置)

如果模型已经有名为 title、description(或 summary、excerpt)、image(或 cover_image、photo、thumbnail)的字段,并且实现了 get_absolute_url() 方法,SeoModelMixin 会自动识别它们,无需任何额外配置:

from django.db import models
from seo_suite.mixins import SeoModelMixin


class Article(SeoModelMixin, models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    image = models.ImageField(upload_to="articles/", blank=True)
    slug = models.SlugField(unique=True)

    def get_absolute_url(self):
        return f"/articles/{self.slug}/"

完整的约定查找顺序如下:

SEO 字段 依次尝试
title title、name、headline
meta_description meta_description、description、summary、excerpt
meta_keywords meta_keywords、keywords
og_image og_image、image、cover_image、photo、thumbnail
canonical_url get_absolute_url() 的返回值

对于 ImageField 和 FileField 类型的值,只要已上传文件,mixin 会自动调用 .url;字段为空则不提供任何值。

空文本字段("")同样不提供任何值。它与不存在的字段等同处理,因此优先级更低的默认值仍可补上这个空缺。


第 2 层:用 SEO_FIELD_MAP 映射自定义字段名

如果模型的字段名与约定不一致,在类属性中声明 SEO_FIELD_MAP:

class Article(SeoModelMixin, models.Model):
    headline = models.CharField(max_length=200)
    blurb = models.TextField(blank=True)
    cover = models.ImageField(upload_to="covers/", blank=True)
    slug = models.SlugField(unique=True)

    SEO_FIELD_MAP = {
        "title": "headline",
        "meta_description": "blurb",
        "og_image": "cover",
    }

    def get_absolute_url(self):
        return f"/articles/{self.slug}/"

SEO_FIELD_MAP 的值是模型上的属性名。mixin 也接受方法名(无参数的可调用对象会由它代为调用)。

SeoMetadata 的任何字段都可以映射:title、h1、meta_description、meta_keywords、og_image、robots、canonical_url。


第 3 层:逐条可编辑的 SEO 列

如果编辑团队需要在不改代码的前提下为单篇文章定制 SEO,使用 SeoModelFieldsMixin。它是一个抽象模型,会直接在你的表中添加 seo_* 列:

from seo_suite.mixins import SeoModelFieldsMixin


class Article(SeoModelFieldsMixin, models.Model):
    headline = models.CharField(max_length=200)
    blurb = models.TextField(blank=True)
    cover = models.ImageField(upload_to="covers/", blank=True)
    slug = models.SlugField(unique=True)

    SEO_FIELD_MAP = {
        "title": "headline",
        "meta_description": "blurb",
        "og_image": "cover",
    }

    def get_absolute_url(self):
        return f"/articles/{self.slug}/"

SeoModelFieldsMixin 继承自 SeoModelMixin,所以所有命名约定和 SEO_FIELD_MAP 依然有效。在此基础上,它再把自己的列作为更高优先级的一层合并进来:

列 覆盖
seo_title title
seo_description meta_description
seo_keywords meta_keywords
seo_robots robots
seo_canonical canonical_url
seo_og_image og_image

seo_title 列为空表示“不作主张”,此时改用约定回退值(通过 SEO_FIELD_MAP 映射到 headline)。也就是说,编辑只需在想偏离自动值时才填写这些列。

由于 SeoModelFieldsMixin 是抽象模型,迁移需要由你来创建:

python manage.py makemigrations blog
python manage.py migrate

在 Django Admin 中注册这些字段

# blog/admin.py

from django.contrib import admin
from .models import Article


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    fieldsets = (
        (None, {
            "fields": ("headline", "blurb", "cover", "slug"),
        }),
        ("SEO", {
            "fields": (
                "seo_title",
                "seo_description",
                "seo_keywords",
                "seo_robots",
                "seo_canonical",
                "seo_og_image",
            ),
            "classes": ("collapse",),
            "description": (
                "Leave blank to use the article's headline and blurb automatically."
            ),
        }),
    )

接入详情视图

# blog/views.py

from django.views.generic import DetailView
from seo_suite.mixins import SeoViewMixin

from .models import Article


class ArticleDetail(SeoViewMixin, DetailView):
    model = Article
    slug_field = "slug"
    slug_url_kwarg = "slug"

视图会自动把 self.object 传给解析器,你不需要自己调用 get_seo_metadata()。


添加 JSON-LD Article 结构化数据

添加 SEO_SCHEMA_PROFILES,即可在 meta 标签之外同时输出一段 Article 结构化数据:

class Article(SeoModelMixin, models.Model):
    headline = models.CharField(max_length=200)
    blurb = models.TextField(blank=True)
    cover = models.ImageField(upload_to="covers/", blank=True)
    slug = models.SlugField(unique=True)
    published_at = models.DateTimeField()
    updated_at = models.DateTimeField(auto_now=True)
    author = models.ForeignKey("auth.User", on_delete=models.SET_NULL, null=True)

    SEO_FIELD_MAP = {
        "title": "headline",
        "meta_description": "blurb",
        "og_image": "cover",
    }
    SEO_SCHEMA_PROFILES = ["Article", "BreadcrumbList"]

    def get_absolute_url(self):
        return f"/articles/{self.slug}/"

    def get_breadcrumbs(self):
        return [
            ("Home", "/"),
            ("Articles", "/articles/"),
            (self.headline, self.get_absolute_url()),
        ]

Article 配置会自动读取 headline、blurb、cover.url、published_at、updated_at 和 author,与上面的字段名一一对应。完整的配置参考见 JSON-LD 结构化数据。


在视图层覆盖特定字段

如果要忽略对象本身的值、强制使用某个 SEO 值(例如给草稿内容固定设置一条 robots 指令),使用视图级属性:

class DraftArticleDetail(SeoViewMixin, DetailView):
    model = Article
    seo_robots = "noindex,nofollow"

视图层(优先级 50)总是优先于对象层(优先级 40)。更多用法见 详情视图。