指南:JSON-LD 结构化数据

django-seo-suite 内置了一套现成的 schema.org 配置档(profile)。只需在模型上加一个类属性即可启用,无需改动模板。


在模型上启用配置档

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


class Article(SeoModelMixin, models.Model):
    headline = models.CharField(max_length=200)
    blurb = models.TextField(blank=True)
    cover = models.ImageField(upload_to="covers/", blank=True)
    published_at = models.DateTimeField()
    updated_at = models.DateTimeField(auto_now=True)
    slug = models.SlugField(unique=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()),
        ]

{% seo_head %} 会把每个配置档渲染成一个独立的 <script type="application/ld+json"> 块。


免费配置档库

下面所有配置档默认都已注册。在模型的 SEO_SCHEMA_PROFILES 或视图的 seo_schema_profiles 中按键名引用即可。

WebPage

键名: "WebPage"

读取 title/name/headline → name,description/summary/excerpt → description,get_absolute_url() → url。始终输出(即使没有匹配到任何字段)。

WebSite

键名: "WebSite"

读取 SEO_SUITE["DEFAULTS"]["og"]["site_name"] → name,请求根地址 → url。一般加在首页视图上。

Organization

键名: "Organization"

读取 SEO_SUITE["DEFAULTS"]["og"]["site_name"] → name,请求根地址 → url。在首页上与 WebSite 搭配使用。

Person

键名: "Person"

读取 full_name/name/get_full_name() → name,get_absolute_url() → url。用于作者个人主页。

键名: "BreadcrumbList"

读取 get_breadcrumbs()(或 breadcrumbs 属性)。该方法必须返回一个由 (name, url) 元组组成的列表,或一个由带 "name" 和 "url" 键的字典组成的列表:

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

    # Dict form (equivalent):
    return [
        {"name": "Home", "url": "/"},
        {"name": "Articles", "url": "/articles/"},
        {"name": self.headline, "url": self.get_absolute_url()},
    ]

只有至少存在一个有效项时才会输出。

Article

键名: "Article"

JSON-LD 字段 依次尝试的来源属性
headline headline、title、name
description meta_description、description、summary、excerpt
image og_image、image、cover_image、thumbnail
datePublished date_published、published_at、published、pub_date、created
dateModified date_modified、modified、updated_at、updated
author author_name、author(包装为 {"@type":"Person","name":...})
mainEntityOfPage get_absolute_url()

FAQPage

键名: "FAQPage"

读取 get_faqs()(或 faqs 属性)。该方法必须返回一个列表,元素为 (question, answer) 元组,或带 "question"/"answer" 键的字典(也接受 "q"/"a" 简写):

def get_faqs(self):
    return self.faq_items.values_list("question", "answer")
    # or:
    return [
        {"question": "What is …?", "answer": "It is …"},
        {"q": "How do I …?", "a": "You can …"},
    ]

只有至少存在一组完整的问答时才会输出。

Product

键名: "Product"

JSON-LD 字段 依次尝试的来源属性
name name、title
description meta_description、description、summary
image og_image、image、cover_image
sku sku
brand brand(包装为 {"@type":"Brand","name":...})
offers 由 price、currency/price_currency、availability 组装而成

用 get_schema_data 覆盖配置档字段

任何模型都可以为某个配置档补充或覆盖特定字段,无需继承该配置档:

class Article(SeoModelMixin, models.Model):
    headline = models.CharField(max_length=200)
    SEO_SCHEMA_PROFILES = ["Article"]

    def get_schema_data(self, key, context=None):
        if key == "Article":
            return {
                "inLanguage": "en-US",
                "publisher": {
                    "@type": "Organization",
                    "name": "My Blog",
                    "logo": {"@type": "ImageObject", "url": "https://example.com/logo.png"},
                },
            }
        return {}

get_schema_data 返回的字典会合并到配置档的自动输出之上(你的键优先于配置档的键)。不想修改的配置档,返回 {} 或 None 即可。


用 get_schema_image 输出更丰富的图片信息

默认情况下,配置档的 image 字段只是一个纯 URL 字符串(从 og_image、image、cover_image、thumbnail 解析而来)。如果你还有原始大图及其尺寸,可以改为输出 schema.org 的 ImageObject:Google 的富媒体结果更青睐带明确尺寸的图片。

实现可选的 get_schema_image(key, context) 钩子即可。它返回什么,就原样用作配置档的 image 值:可以是字符串、ImageObject 字典,或由二者组成的列表。django-seo-suite 不会处理你的图片;你可以自由对接自己的缩略图库(easy-thumbnails、sorl、imagekit 等):

class Article(SeoModelMixin, models.Model):
    cover = models.ImageField(upload_to="covers/")
    SEO_SCHEMA_PROFILES = ["Article", "BreadcrumbList"]

    def get_schema_image(self, key, context=None):
        # The `key` argument is a per-profile switch: return a value only
        # for the profiles you want, and None to fall back / skip.
        if key != "Article":
            return None
        thumb = get_thumbnailer(self.cover)["card"]  # your thumbnail backend
        return {
            "@type": "ImageObject",
            "contentUrl": self.cover.url,
            "thumbnailUrl": thumb.url,
            "width": self.cover.width,
            "height": self.cover.height,
        }

image 字段的解析顺序:先按约定设置纯字符串,再由 get_schema_image 覆盖;如果 get_schema_data(见上文)也返回了 image 键,则以它为最终结果。

这个钩子完全可选,不实现时行为不变。借助 key 参数,一个方法就能服务所有配置档,由你决定哪些配置档带图片(返回 None 即跳过)。

高流量站点的缓存

get_schema_image 在解析时运行,所以即使钩子很重(例如调用缩略图后端或探测图片尺寸),每次请求也只会调用一次。启用对象解析缓存(SEO_SUITE["CACHE_TTL"] > 0,需要已配置 Django 缓存后端)后,钩子的输出会作为缓存元数据的一部分保存下来,在多次请求间复用,并在对象保存或删除时自动失效。缓存关闭(默认)时,钩子在每次解析时都会运行,因此请保持它足够轻量,或在钩子内部自行缓存。


在视图上启用配置档

在视图上使用 seo_schema_profiles(注意:没有 SEO_ 前缀,且为小写):

class HomepageView(SeoViewMixin, TemplateView):
    template_name = "home.html"
    seo_title = "My Site"
    seo_schema_profiles = ["WebSite", "Organization"]

全站默认配置档

要给站点的每个页面都加上某个配置档,使用 DEFAULT_SCHEMA_PROFILES:

SEO_SUITE = {
    "DEFAULT_SCHEMA_PROFILES": ["WebSite"],
}

DEFAULT_SCHEMA_PROFILES 中的配置档会与模型级和视图级配置档一并合并;所有来源的 JSON-LD 块都会累加输出。


JSON-LD 的累加规则

标量字段遵循“高优先级覆盖低优先级”,而 JSON-LD 块会在所有层级间累加。模型声明了 ["Article"],视图声明了 ["WebPage"],同一页面就会输出这两个块。这是有意设计的:一个页面包含多种结构化数据类型是合法的,而且往往正是你想要的。


自定义序列化器

默认的序列化器会用 Unicode 转义处理 <、> 和 & 字符,防止通过 JSON-LD 注入代码。如果需要不同的序列化策略(例如调试时美化输出、自定义转义),可以替换序列化器:

# myapp/seo_utils.py
import json

def my_serializer(data: dict) -> str:
    return json.dumps(data, indent=2, ensure_ascii=False)
# settings.py
SEO_SUITE = {
    "JSONLD_SERIALIZER": "myapp.seo_utils.my_serializer",
}