# 导入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处理逻辑绑定到页面区域的meta标签生成环节""" # 当系统渲染中的meta标签时,触发dispatch_seo_generation方法 hooks.register('head_meta', self.dispatch_seo_generation) def _get_article_seo_data(self, context, request, blog_setting): """ 生成文章详情页的SEO数据(私有方法,仅内部调用) 参数: context:模板上下文,包含当前页面的文章对象等数据 request:HTTP请求对象,用于构建绝对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 Graph(OG)协议标签,用于优化社交平台分享效果 meta_tags = f''' ''' # 为文章的每个标签添加OG标签 for tag in article.tags.all(): meta_tags += f'' # 添加站点名称OG标签,关联文章所属站点 meta_tags += f'' # 生成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'' # 拼接所有SEO相关标签并返回,最终会被插入到页面的区域 return f""" {seo_data.get("title", "")} {seo_data.get("meta_tags", "")} {json_ld_script} """ # 实例化插件,使插件系统能够识别并加载该插件 plugin = SeoOptimizerPlugin()