diff --git a/src/__init__.py b/src/__init__.py index e69de29..e88afca 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..a2da290 Binary files /dev/null and b/src/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/article_copyright/__init__.py b/src/article_copyright/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/src/article_copyright/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/src/article_copyright/__pycache__/__init__.cpython-312.pyc b/src/article_copyright/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..01c80d2 Binary files /dev/null and b/src/article_copyright/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/article_copyright/__pycache__/plugin.cpython-312.pyc b/src/article_copyright/__pycache__/plugin.cpython-312.pyc new file mode 100644 index 0000000..5cb84e8 Binary files /dev/null and b/src/article_copyright/__pycache__/plugin.cpython-312.pyc differ diff --git a/src/article_copyright/plugin.py b/src/article_copyright/plugin.py new file mode 100644 index 0000000..ab14d18 --- /dev/null +++ b/src/article_copyright/plugin.py @@ -0,0 +1,55 @@ +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME + + +class ArticleCopyrightPlugin(BasePlugin): + """ + 文章版权声明插件 + 功能:在文章正文末尾自动添加版权声明信息 + """ + + # 插件基本信息配置 + PLUGIN_NAME = '文章结尾版权声明' # 插件显示名称 + PLUGIN_DESCRIPTION = '一个在文章正文末尾添加版权声明的插件。' # 插件功能描述 + PLUGIN_VERSION = '0.2.0' # 插件版本号 + PLUGIN_AUTHOR = 'liangliangyy' # 插件作者 + + # 2. 实现 register_hooks 方法,专门用于注册钩子 + def register_hooks(self): + """ + 注册钩子函数 + 这个方法在插件初始化时被自动调用,用于将插件的功能方法注册到系统的钩子上 + """ + # 将 add_copyright_to_content 方法注册到文章内容钩子 + # 当系统处理文章内容时,会自动调用这个方法来添加版权信息 + hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_copyright_to_content) + + def add_copyright_to_content(self, content, *args, **kwargs): + """ + 文章内容处理函数 - 在文章末尾添加版权声明 + + 参数: + content: str - 原始的文章内容 + *args, **kwargs: 其他参数,其中包含文章对象 + + 返回值: + str - 添加了版权声明后的完整内容 + """ + # 从参数中获取文章对象 + article = kwargs.get('article') + + # 如果没有文章对象,直接返回原内容(避免错误) + if not article: + return content + + # 构建版权声明HTML代码 + copyright_info = f"\n

本文由 {article.author.username} 原创,转载请注明出处。

" + + # 将版权信息追加到文章内容末尾 + return content + copyright_info + + +# 3. 实例化插件。 +# 这会自动调用 BasePlugin.__init__,然后 BasePlugin.__init__ 会调用我们上面定义的 register_hooks 方法。 +plugin = ArticleCopyrightPlugin() diff --git a/src/external_links/__init__.py b/src/external_links/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/src/external_links/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/src/external_links/__pycache__/__init__.cpython-312.pyc b/src/external_links/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..a8670a6 Binary files /dev/null and b/src/external_links/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/external_links/__pycache__/plugin.cpython-312.pyc b/src/external_links/__pycache__/plugin.cpython-312.pyc new file mode 100644 index 0000000..717991b Binary files /dev/null and b/src/external_links/__pycache__/plugin.cpython-312.pyc differ diff --git a/src/external_links/plugin.py b/src/external_links/plugin.py new file mode 100644 index 0000000..6dc71eb --- /dev/null +++ b/src/external_links/plugin.py @@ -0,0 +1,86 @@ +import re +from urllib.parse import urlparse +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME + + +class ExternalLinksPlugin(BasePlugin): + """ + 外部链接处理插件 + 功能:自动为文章中的外部链接添加安全属性,防止钓鱼攻击和提升用户体验 + """ + + # 插件基本信息 + PLUGIN_NAME = '外部链接处理器' + PLUGIN_DESCRIPTION = '自动为文章中的外部链接添加 target="_blank" 和 rel="noopener noreferrer" 属性。' + PLUGIN_VERSION = '0.1.0' + PLUGIN_AUTHOR = 'liangliangyy' + + def register_hooks(self): + """ + 注册钩子函数 + 将外部链接处理方法注册到文章内容钩子 + """ + hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.process_external_links) + + def process_external_links(self, content, *args, **kwargs): + """ + 处理文章内容中的外部链接 + + 参数: + content: str - 原始的文章HTML内容 + *args, **kwargs: 其他参数 + + 返回值: + str - 处理后的文章内容,外部链接已添加安全属性 + """ + # 导入获取当前网站域名的工具函数 + from djangoblog.utils import get_current_site + # 获取当前网站的域名,用于判断链接是否为外部链接 + site_domain = get_current_site().domain + + # 正则表达式模式:匹配所有的 标签 + # 分组1: ... (href属性后的内容和标签闭合部分) + link_pattern = re.compile(r'(]*?\s+)?href=")([^"]*)(".*?/a>)', re.IGNORECASE) + + def replacer(match): + """ + 正则替换函数 - 处理单个链接匹配 + + 参数: + match: 正则匹配对象 + + 返回值: + str - 处理后的链接HTML代码 + """ + # 提取链接URL + href = match.group(2) + + # 如果链接已经有 target 属性,则不处理(避免重复添加) + if 'target=' in match.group(0).lower(): + return match.group(0) + + # 解析链接URL,提取域名信息 + parsed_url = urlparse(href) + + # 判断是否为外部链接: + # 1. 有域名 (parsed_url.netloc 不为空) + # 2. 且域名不等于当前网站域名 + if parsed_url.netloc and parsed_url.netloc != site_domain: + # 为外部链接添加安全属性: + # - target="_blank": 在新标签页打开 + # - rel="noopener noreferrer": 安全防护,防止钓鱼攻击 + return f'{match.group(1)}{href}" target="_blank" rel="noopener noreferrer"{match.group(3)}' + + # 内部链接或无效链接,返回原样 + return match.group(0) + + # 使用正则表达式替换所有匹配的链接 + return link_pattern.sub(replacer, content) + + +# 实例化插件,自动注册到系统 +plugin = ExternalLinksPlugin() \ No newline at end of file diff --git a/src/reading_time/__init__.py b/src/reading_time/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/src/reading_time/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/src/reading_time/__pycache__/__init__.cpython-312.pyc b/src/reading_time/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..030d2a5 Binary files /dev/null and b/src/reading_time/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/reading_time/__pycache__/plugin.cpython-312.pyc b/src/reading_time/__pycache__/plugin.cpython-312.pyc new file mode 100644 index 0000000..83460ed Binary files /dev/null and b/src/reading_time/__pycache__/plugin.cpython-312.pyc differ diff --git a/src/reading_time/plugin.py b/src/reading_time/plugin.py new file mode 100644 index 0000000..d7fe4ae --- /dev/null +++ b/src/reading_time/plugin.py @@ -0,0 +1,73 @@ +import math +import re +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME + + +class ReadingTimePlugin(BasePlugin): + """ + 阅读时间预测插件 + 功能:自动估算文章的阅读时间并显示在文章开头,帮助读者了解阅读时长 + """ + + # 插件基本信息 + PLUGIN_NAME = '阅读时间预测' + PLUGIN_DESCRIPTION = '估算文章阅读时间并显示在文章开头。' + PLUGIN_VERSION = '0.1.0' + PLUGIN_AUTHOR = 'liangliangyy' + + def register_hooks(self): + """ + 注册钩子函数 + 将阅读时间计算方法注册到文章内容钩子 + """ + hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_reading_time) + + def add_reading_time(self, content, *args, **kwargs): + """ + 计算文章阅读时间并添加到内容开头 + + 参数: + content: str - 原始的文章HTML内容 + *args, **kwargs: 其他参数 + + 返回值: + str - 添加了阅读时间提示的文章内容 + """ + # 1. 清理HTML内容,提取纯文本 + # 使用正则表达式移除所有HTML标签,只保留文本内容 + clean_content = re.sub(r'<[^>]*>', '', content) + # 移除首尾空白字符 + clean_content = clean_content.strip() + + # 2. 计算字数(支持中英文混合) + # 正则表达式说明: + # [\u4e00-\u9fa5] - 匹配中文字符(Unicode范围) + # | - 或者 + # \w+ - 匹配连续的英文单词字符(字母、数字、下划线) + words = re.findall(r'[\u4e00-\u9fa5]|\w+', clean_content) + word_count = len(words) + + # 3. 计算阅读时间 + # 按照平均每分钟200字的速度计算(适合中文阅读速度) + reading_speed = 200 + # 使用math.ceil向上取整,确保不会出现0分钟的情况 + reading_minutes = math.ceil(word_count / reading_speed) + + # 4. 处理边界情况 + # 如果计算出的阅读时间少于1分钟,统一显示为1分钟 + # 避免出现"预计阅读时间:0分钟"的不合理显示 + if reading_minutes < 1: + reading_minutes = 1 + + # 5. 生成阅读时间提示的HTML代码 + # 使用灰色文字和斜体样式,视觉上不突兀 + reading_time_html = f'

预计阅读时间:{reading_minutes} 分钟

' + + # 6. 将阅读时间提示添加到文章内容开头 + return reading_time_html + content + + +# 实例化插件,自动注册到系统 +plugin = ReadingTimePlugin() \ No newline at end of file diff --git a/src/seo_optimizer/__init__.py b/src/seo_optimizer/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/src/seo_optimizer/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/src/seo_optimizer/__pycache__/__init__.cpython-312.pyc b/src/seo_optimizer/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..61a9e3b Binary files /dev/null and b/src/seo_optimizer/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/seo_optimizer/__pycache__/plugin.cpython-312.pyc b/src/seo_optimizer/__pycache__/plugin.cpython-312.pyc new file mode 100644 index 0000000..c2b73af Binary files /dev/null and b/src/seo_optimizer/__pycache__/plugin.cpython-312.pyc differ diff --git a/src/seo_optimizer/plugin.py b/src/seo_optimizer/plugin.py new file mode 100644 index 0000000..8e0d465 --- /dev/null +++ b/src/seo_optimizer/plugin.py @@ -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''' + + + + + + + + + ''' + # 为每个标签添加article:tag meta标签 + for tag in article.tags.all(): + meta_tags += f'' + # 添加网站名称 + meta_tags += f'' + + # 生成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'' + + # 返回完整的SEO HTML代码 + return f""" + {seo_data.get("title", "")} + + + {seo_data.get("meta_tags", "")} + {json_ld_script} + """ + + +# 实例化插件,自动注册到系统 +plugin = SeoOptimizerPlugin() diff --git a/src/view_count/__init__.py b/src/view_count/__init__.py new file mode 100644 index 0000000..8804fdf --- /dev/null +++ b/src/view_count/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package \ No newline at end of file diff --git a/src/view_count/__pycache__/__init__.cpython-312.pyc b/src/view_count/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..4729442 Binary files /dev/null and b/src/view_count/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/view_count/__pycache__/plugin.cpython-312.pyc b/src/view_count/__pycache__/plugin.cpython-312.pyc new file mode 100644 index 0000000..62d3e0a Binary files /dev/null and b/src/view_count/__pycache__/plugin.cpython-312.pyc differ diff --git a/src/view_count/plugin.py b/src/view_count/plugin.py new file mode 100644 index 0000000..717a2bb --- /dev/null +++ b/src/view_count/plugin.py @@ -0,0 +1,37 @@ +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks + + +class ViewCountPlugin(BasePlugin): + """ + 文章浏览次数统计插件 + 功能:自动记录和统计文章的浏览次数 + """ + + # 插件基本信息 + PLUGIN_NAME = '文章浏览次数统计' + PLUGIN_DESCRIPTION = '统计文章的浏览次数' + PLUGIN_VERSION = '0.1.0' + PLUGIN_AUTHOR = 'liangliangyy' + + def register_hooks(self): + """ + 注册钩子函数 + 将浏览统计方法注册到文章内容获取后的钩子 + """ + hooks.register('after_article_body_get', self.record_view) + + def record_view(self, article, *args, **kwargs): + """ + 记录文章浏览次数 + + 参数: + article: Article对象 - 需要记录浏览次数的文章 + *args, **kwargs: 其他参数(在此插件中未使用) + """ + # 调用文章的viewed()方法来增加浏览次数 + article.viewed() + + +# 实例化插件,自动注册到系统 +plugin = ViewCountPlugin() \ No newline at end of file