You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DjangoBlog/plugins/seo_optimizer/plugin.py

225 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 导入JSON模块用于将结构化数据转换为JSON格式
import json
# 导入Django工具函数用于移除HTML标签提取纯文本
from django.utils.html import strip_tags
# 导入Django模板过滤器当前未使用预留用于文本截断
from django.template.defaultfilters import truncatewords
# 导入插件基类,所有插件需继承此类实现标准化接口
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入插件钩子管理模块,用于注册插件功能到指定钩子
from djangoblog.plugin_manage import hooks
# 导入博客数据模型,用于获取文章、分类、标签等数据
from blog.models import Article, Category, Tag
# 导入工具函数,用于获取博客站点的基础配置(如站点名称、关键词等)
from djangoblog.utils import get_blog_setting
class SeoOptimizerPlugin(BasePlugin):
# 插件名称,用于在插件管理界面展示
PLUGIN_NAME = 'SEO 优化器'
# 插件功能描述,说明插件的核心作用
PLUGIN_DESCRIPTION = '为文章、页面等提供 SEO 优化,动态生成 meta 标签和 JSON-LD 结构化数据。'
# 插件版本号,用于版本管理
PLUGIN_VERSION = '0.2.0'
# 插件作者信息
PLUGIN_AUTHOR = 'liuangliangyy'
def register_hooks(self):
"""注册插件钩子将SEO处理逻辑绑定到页面<head>区域的meta标签生成环节"""
# 当系统渲染<head>中的meta标签时触发dispatch_seo_generation方法
hooks.register('head_meta', self.dispatch_seo_generation)
def _get_article_seo_data(self, context, request, blog_setting):
"""
生成文章详情页的SEO数据私有方法仅内部调用
参数:
context模板上下文包含当前页面的文章对象等数据
requestHTTP请求对象用于构建绝对URL
blog_setting博客站点配置信息如站点名称、关键词
返回:
包含SEO相关数据的字典若上下文无有效文章对象则返回None
"""
# 从上下文获取文章对象
article = context.get('article')
# 校验是否为有效的Article实例避免非文章页误处理
if not isinstance(article, Article):
return None
# 生成描述信息移除文章内容中的HTML标签截取前150字符符合多数搜索引擎的描述长度建议
description = strip_tags(article.body)[:150]
# 生成关键词:优先使用文章标签,若无标签则使用站点默认关键词
keywords = ",".join([tag.name for tag in article.tags.all()]) or blog_setting.site_keywords
# 生成Open GraphOG协议标签用于优化社交平台分享效果
meta_tags = f'''
<meta property="og:type" content="article"/> <!-- 内容类型为文章 -->
<meta property="og:title" content="{article.title}"/> <!-- 社交分享标题 -->
<meta property="og:description" content="{description}"/> <!-- 社交分享描述 -->
<meta property="og:url" content="{request.build_absolute_uri()}"/> <!-- 文章完整URL -->
<meta property="article:published_time" content="{article.pub_time.isoformat()}"/> <!-- 发布时间ISO标准格式 -->
<meta property="article:modified_time" content="{article.last_modify_time.isoformat()}"/> <!-- 最后修改时间 -->
<meta property="article:author" content="{article.author.username}"/> <!-- 作者信息 -->
<meta property="article:section" content="{article.category.name}"/> <!-- 所属分类 -->
'''
# 为文章的每个标签添加OG标签
for tag in article.tags.all():
meta_tags += f'<meta property="article:tag" content="{tag.name}"/>'
# 添加站点名称OG标签关联文章所属站点
meta_tags += f'<meta property="og:site_name" content="{blog_setting.site_name}"/>'
# 生成JSON-LD结构化数据帮助搜索引擎理解页面内容结构
structured_data = {
"@context": "https://schema.org", # 遵循schema.org的结构化数据标准
"@type": "Article", # 内容类型为文章
"mainEntityOfPage": { # 声明页面的主体内容
"@type": "WebPage",
"@id": request.build_absolute_uri() # 页面唯一标识完整URL
},
"headline": article.title, # 文章标题
"description": description, # 文章描述
# 文章首图通过绝对URL访问增强内容丰富度
"image": request.build_absolute_uri(article.get_first_image_url()),
"datePublished": article.pub_time.isoformat(), # 发布时间
"dateModified": article.last_modify_time.isoformat(), # 修改时间
"author": { # 作者信息
"@type": "Person",
"name": article.author.username
},
"publisher": { # 发布机构信息
"@type": "Organization",
"name": blog_setting.site_name
}
}
# 若文章无图片则移除image字段避免空值影响结构化数据有效性
if not structured_data.get("image"):
del structured_data["image"]
# 返回整合后的文章页SEO数据
return {
"title": f"{article.title} | {blog_setting.site_name}", # 页面标题(文章标题+站点名称,增强品牌关联)
"description": description,
"keywords": keywords,
"meta_tags": meta_tags, # 包含OG标签的HTML片段
"json_ld": structured_data # JSON-LD结构化数据
}
def _get_category_seo_data(self, context, request, blog_setting):
"""生成分类页面的SEO数据私有方法"""
# 从上下文获取分类名称变量名tag_name可能为笔误实际应为分类名称
category_name = context.get('tag_name')
if not category_name:
return None
# 根据名称查询分类对象
category = Category.objects.filter(name=category_name).first()
if not category: # 若分类不存在返回None
return None
# 页面标题:分类名称+站点名称
title = f"{category.name} | {blog_setting.site_name}"
# 描述:使用分类名称或站点默认描述
description = strip_tags(category.name) or blog_setting.site_description
# 关键词:使用分类名称
keywords = category.name
# 生成面包屑导航的JSON-LD数据帮助搜索引擎理解页面在站点中的层级位置
breadcrumb_items = [
# 首页面包屑项位置1
{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')}
]
# 当前分类面包屑项位置2
breadcrumb_items.append({
"@type": "ListItem",
"position": 2,
"name": category.name,
"item": request.build_absolute_uri()
})
structured_data = {
"@context": "https://schema.org",
"@type": "BreadcrumbList", # 类型为面包屑列表
"itemListElement": breadcrumb_items # 面包屑项列表
}
# 返回分类页SEO数据
return {
"title": title,
"description": description,
"keywords": keywords,
"meta_tags": "", # 分类页暂无需额外meta标签
"json_ld": structured_data
}
def _get_default_seo_data(self, context, request, blog_setting):
"""生成默认页面如首页、未匹配的页面的SEO数据私有方法"""
# 生成网站级别的JSON-LD数据包含站点基本信息和搜索功能描述
structured_data = {
"@context": "https://schema.org",
"@type": "WebSite", # 类型为网站
"url": request.build_absolute_uri('/'), # 网站首页URL
# 描述站点的搜索功能(帮助搜索引擎识别并支持站内搜索)
"potentialAction": {
"@type": "SearchAction",
# 搜索结果页URL模板{search_term_string}为搜索关键词占位符)
"target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}",
"query-input": "required name=search_term_string" # 声明搜索参数为必填项
}
}
# 返回默认页SEO数据
return {
"title": f"{blog_setting.site_name} | {blog_setting.site_description}", # 首页标题(站点名称+描述)
"description": blog_setting.site_description, # 站点描述
"keywords": blog_setting.site_keywords, # 站点默认关键词
"meta_tags": "", # 默认页无需额外meta标签
"json_ld": structured_data
}
def dispatch_seo_generation(self, metas, context):
"""
分发SEO数据生成逻辑核心方法根据当前页面类型调用对应的数据生成方法
参数:
metas原始的meta标签内容未使用预留用于扩展
context模板上下文包含请求对象和页面数据
返回:
生成的完整SEO标签包含title、meta标签、JSON-LD脚本等
"""
# 从上下文获取请求对象用于判断页面类型和构建URL
request = context.get('request')
if not request: # 若无请求对象,返回原始内容
return metas
# 获取当前视图的名称通过Django的URL解析器用于区分页面类型
view_name = request.resolver_match.view_name
# 获取博客站点配置
blog_setting = get_blog_setting()
# 根据页面类型视图名称生成对应的SEO数据
seo_data = None
if view_name == 'blog:detailbyid':
# 文章详情页调用文章SEO数据生成方法
seo_data = self._get_article_seo_data(context, request, blog_setting)
elif view_name == 'blog:category_detail':
# 分类详情页调用分类SEO数据生成方法
seo_data = self._get_category_seo_data(context, request, blog_setting)
# 若未匹配到特定页面类型使用默认SEO数据
if not seo_data:
seo_data = self._get_default_seo_data(context, request, blog_setting)
# 生成JSON-LD脚本标签将结构化数据转换为JSON字符串确保非ASCII字符正常显示
json_ld_script = f'<script type="application/ld+json">{json.dumps(seo_data.get("json_ld", {}), ensure_ascii=False, indent=4)}</script>'
# 拼接所有SEO相关标签并返回最终会被插入到页面的<head>区域
return f"""
<title>{seo_data.get("title", "")}</title> <!-- 页面标题SEO核心要素 -->
<meta name="description" content="{seo_data.get("description", "")}"> <!-- 描述标签(影响搜索结果展示) -->
<meta name="keywords" content="{seo_data.get("keywords", "")}"> <!-- 关键词标签 -->
{seo_data.get("meta_tags", "")} <!-- 额外meta标签如Open Graph -->
{json_ld_script} <!-- JSON-LD结构化数据脚本提升搜索结果丰富度 -->
"""
# 实例化插件,使插件系统能够识别并加载该插件
plugin = SeoOptimizerPlugin()