|
|
|
|
@ -0,0 +1,219 @@
|
|
|
|
|
import json
|
|
|
|
|
from django.utils.html import strip_tags
|
|
|
|
|
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):
|
|
|
|
|
"""
|
|
|
|
|
SEO 优化插件
|
|
|
|
|
功能:为网站提供全面的SEO优化支持,包括meta标签和JSON-LD结构化数据
|
|
|
|
|
提升网站在搜索引擎中的可见性和排名
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# 插件基本信息
|
|
|
|
|
PLUGIN_NAME = 'SEO 优化器'
|
|
|
|
|
PLUGIN_DESCRIPTION = '为文章、页面等提供 SEO 优化,动态生成 meta 标签和 JSON-LD 结构化数据。'
|
|
|
|
|
PLUGIN_VERSION = '0.2.0'
|
|
|
|
|
PLUGIN_AUTHOR = 'liuangliangyy' # 注意:这里可能有拼写错误,应该是 liangliangyy
|
|
|
|
|
|
|
|
|
|
def register_hooks(self):
|
|
|
|
|
"""
|
|
|
|
|
注册钩子函数
|
|
|
|
|
将SEO生成方法注册到head_meta钩子,在页面头部生成SEO相关标签
|
|
|
|
|
"""
|
|
|
|
|
hooks.register('head_meta', self.dispatch_seo_generation)
|
|
|
|
|
|
|
|
|
|
def _get_article_seo_data(self, context, request, blog_setting):
|
|
|
|
|
"""
|
|
|
|
|
生成文章页面的SEO数据
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
context: 模板上下文,包含文章等信息
|
|
|
|
|
request: HTTP请求对象
|
|
|
|
|
blog_setting: 博客设置
|
|
|
|
|
|
|
|
|
|
返回值:
|
|
|
|
|
dict - 包含文章页面SEO数据的字典
|
|
|
|
|
"""
|
|
|
|
|
# 从上下文中获取文章对象
|
|
|
|
|
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 Graph meta标签(用于社交媒体分享)
|
|
|
|
|
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()}"/>
|
|
|
|
|
<meta property="article:published_time" content="{article.pub_time.isoformat()}"/>
|
|
|
|
|
<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}"/>
|
|
|
|
|
'''
|
|
|
|
|
# 为每个标签添加article:tag meta标签
|
|
|
|
|
for tag in article.tags.all():
|
|
|
|
|
meta_tags += f'<meta property="article:tag" content="{tag.name}"/>'
|
|
|
|
|
# 添加网站名称
|
|
|
|
|
meta_tags += f'<meta property="og:site_name" content="{blog_setting.site_name}"/>'
|
|
|
|
|
|
|
|
|
|
# 生成JSON-LD结构化数据(用于搜索引擎理解内容结构)
|
|
|
|
|
structured_data = {
|
|
|
|
|
"@context": "https://schema.org",
|
|
|
|
|
"@type": "Article", # 文章类型
|
|
|
|
|
"mainEntityOfPage": {"@type": "WebPage", "@id": request.build_absolute_uri()},
|
|
|
|
|
"headline": article.title, # 文章标题
|
|
|
|
|
"description": description, # 文章描述
|
|
|
|
|
"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"]
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"title": f"{article.title} | {blog_setting.site_name}", # 页面标题
|
|
|
|
|
"description": description, # 页面描述
|
|
|
|
|
"keywords": keywords, # 页面关键词
|
|
|
|
|
"meta_tags": meta_tags, # meta标签
|
|
|
|
|
"json_ld": structured_data # JSON-LD数据
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _get_category_seo_data(self, context, request, blog_setting):
|
|
|
|
|
"""
|
|
|
|
|
生成分类页面的SEO数据
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
context: 模板上下文
|
|
|
|
|
request: HTTP请求对象
|
|
|
|
|
blog_setting: 博客设置
|
|
|
|
|
|
|
|
|
|
返回值:
|
|
|
|
|
dict - 包含分类页面SEO数据的字典
|
|
|
|
|
"""
|
|
|
|
|
# 从上下文中获取分类名称
|
|
|
|
|
category_name = context.get('tag_name') # 注意:这里变量名可能有误,应该是category_name
|
|
|
|
|
if not category_name:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# 获取分类对象
|
|
|
|
|
category = Category.objects.filter(name=category_name).first()
|
|
|
|
|
if not category:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# 生成页面基本信息
|
|
|
|
|
title = f"{category.name} | {blog_setting.site_name}"
|
|
|
|
|
description = strip_tags(category.name) or blog_setting.site_description
|
|
|
|
|
keywords = category.name
|
|
|
|
|
|
|
|
|
|
# 生成面包屑导航的结构化数据
|
|
|
|
|
breadcrumb_items = [
|
|
|
|
|
{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')}
|
|
|
|
|
]
|
|
|
|
|
breadcrumb_items.append(
|
|
|
|
|
{"@type": "ListItem", "position": 2, "name": category.name, "item": request.build_absolute_uri()})
|
|
|
|
|
|
|
|
|
|
# 面包屑导航的JSON-LD数据
|
|
|
|
|
structured_data = {
|
|
|
|
|
"@context": "https://schema.org",
|
|
|
|
|
"@type": "BreadcrumbList",
|
|
|
|
|
"itemListElement": breadcrumb_items
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"title": title,
|
|
|
|
|
"description": description,
|
|
|
|
|
"keywords": keywords,
|
|
|
|
|
"meta_tags": "",
|
|
|
|
|
"json_ld": structured_data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _get_default_seo_data(self, context, request, blog_setting):
|
|
|
|
|
"""
|
|
|
|
|
生成默认页面(首页等)的SEO数据
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
context: 模板上下文
|
|
|
|
|
request: HTTP请求对象
|
|
|
|
|
blog_setting: 博客设置
|
|
|
|
|
|
|
|
|
|
返回值:
|
|
|
|
|
dict - 包含默认页面SEO数据的字典
|
|
|
|
|
"""
|
|
|
|
|
# 网站类型的JSON-LD结构化数据
|
|
|
|
|
structured_data = {
|
|
|
|
|
"@context": "https://schema.org",
|
|
|
|
|
"@type": "WebSite",
|
|
|
|
|
"url": request.build_absolute_uri('/'),
|
|
|
|
|
"potentialAction": {
|
|
|
|
|
"@type": "SearchAction",
|
|
|
|
|
"target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}",
|
|
|
|
|
"query-input": "required name=search_term_string"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
"title": f"{blog_setting.site_name} | {blog_setting.site_description}",
|
|
|
|
|
"description": blog_setting.site_description,
|
|
|
|
|
"keywords": blog_setting.site_keywords,
|
|
|
|
|
"meta_tags": "",
|
|
|
|
|
"json_ld": structured_data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def dispatch_seo_generation(self, metas, context):
|
|
|
|
|
"""
|
|
|
|
|
SEO数据分发器 - 根据当前页面类型调用相应的SEO生成方法
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
metas: 原始的meta标签内容
|
|
|
|
|
context: 模板上下文
|
|
|
|
|
|
|
|
|
|
返回值:
|
|
|
|
|
str - 完整的SEO相关HTML代码
|
|
|
|
|
"""
|
|
|
|
|
request = context.get('request')
|
|
|
|
|
if not request:
|
|
|
|
|
return metas
|
|
|
|
|
|
|
|
|
|
# 获取当前视图名称,用于判断页面类型
|
|
|
|
|
view_name = request.resolver_match.view_name
|
|
|
|
|
blog_setting = get_blog_setting()
|
|
|
|
|
|
|
|
|
|
seo_data = None
|
|
|
|
|
# 根据视图名称调用不同的SEO生成方法
|
|
|
|
|
if view_name == 'blog:detailbyid':
|
|
|
|
|
seo_data = self._get_article_seo_data(context, request, blog_setting)
|
|
|
|
|
elif view_name == 'blog:category_detail':
|
|
|
|
|
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_ld_script = f'<script type="application/ld+json">{json.dumps(seo_data.get("json_ld", {}), ensure_ascii=False, indent=4)}</script>'
|
|
|
|
|
|
|
|
|
|
# 返回完整的SEO HTML代码
|
|
|
|
|
return f"""
|
|
|
|
|
<title>{seo_data.get("title", "")}</title>
|
|
|
|
|
<meta name="description" content="{seo_data.get("description", "")}">
|
|
|
|
|
<meta name="keywords" content="{seo_data.get("keywords", "")}">
|
|
|
|
|
{seo_data.get("meta_tags", "")}
|
|
|
|
|
{json_ld_script}
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 实例化插件,自动注册到系统
|
|
|
|
|
plugin = SeoOptimizerPlugin()
|