diff --git a/blog/static/blog/css/style.css b/blog/static/blog/css/style.css
index d43f7f38..bd317dba 100644
--- a/blog/static/blog/css/style.css
+++ b/blog/static/blog/css/style.css
@@ -2501,4 +2501,206 @@ li #reply-title {
height: 1px;
border: none;
/*border-top: 1px dashed #f5d6d6;*/
+}
+
+/* =============================================================================
+ 评论内容溢出修复样式
+ 解决代码块和长文本撑开页面布局的问题
+ ============================================================================= */
+
+/* 评论容器基础样式 */
+.comment-body {
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+ max-width: 100%;
+ box-sizing: border-box;
+}
+
+/* 修复评论中的代码块溢出 */
+.comment-content pre,
+.comment-body pre {
+ white-space: pre-wrap !important;
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ max-width: 100% !important;
+ overflow-x: auto;
+ padding: 10px;
+ background-color: #f8f8f8;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 12px;
+ line-height: 1.4;
+ margin: 10px 0;
+}
+
+/* 修复评论中的行内代码 */
+.comment-content code,
+.comment-body code {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ white-space: pre-wrap;
+ max-width: 100%;
+ display: inline-block;
+ vertical-align: top;
+}
+
+/* 修复评论中的长链接 */
+.comment-content a,
+.comment-body a {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ word-break: break-all;
+ max-width: 100%;
+}
+
+/* 修复评论段落 */
+.comment-content p,
+.comment-body p {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ max-width: 100%;
+ margin: 10px 0;
+}
+
+/* 特殊处理代码高亮块 - 关键修复! */
+.comment-content .codehilite,
+.comment-body .codehilite {
+ max-width: 100% !important;
+ overflow-x: auto;
+ margin: 10px 0;
+ background: #f8f8f8 !important;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 10px;
+ font-size: 12px;
+ line-height: 1.4;
+ /* 关键:防止内容撑开容器 */
+ width: 100%;
+ box-sizing: border-box;
+ display: block;
+}
+
+.comment-content .codehilite pre,
+.comment-body .codehilite pre {
+ white-space: pre-wrap !important;
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ background: transparent !important;
+ border: none !important;
+ font-size: inherit;
+ line-height: inherit;
+ /* 确保pre标签不会超出父容器 */
+ max-width: 100%;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+/* 修复代码高亮中的span标签 */
+.comment-content .codehilite span,
+.comment-body .codehilite span {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ /* 防止行内元素导致的溢出 */
+ display: inline;
+ max-width: 100%;
+}
+
+/* 针对特定的代码高亮类 */
+.comment-content .codehilite .kt,
+.comment-content .codehilite .nf,
+.comment-content .codehilite .n,
+.comment-content .codehilite .p,
+.comment-content .codehilite .w,
+.comment-content .codehilite .o,
+.comment-body .codehilite .kt,
+.comment-body .codehilite .nf,
+.comment-body .codehilite .n,
+.comment-body .codehilite .p,
+.comment-body .codehilite .w,
+.comment-body .codehilite .o {
+ word-break: break-all;
+ overflow-wrap: break-word;
+}
+
+/* 修复评论列表项 */
+.commentlist li {
+ max-width: 100%;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* 确保评论内容不超出容器 */
+.commentlist .comment-body {
+ max-width: calc(100% - 20px); /* 留出一些边距 */
+ margin-left: 10px;
+ margin-right: 10px;
+ overflow: hidden; /* 防止内容溢出 */
+ word-wrap: break-word;
+}
+
+/* 重要:限制评论列表项的最大宽度 */
+.commentlist li[style*="margin-left"] {
+ max-width: calc(100% - 2rem) !important;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* 特别处理深层嵌套的评论 */
+.commentlist li[style*="margin-left: 3rem"],
+.commentlist li[style*="margin-left: 6rem"],
+.commentlist li[style*="margin-left: 9rem"] {
+ max-width: calc(100% - 1rem) !important;
+}
+
+/* 移动端优化 */
+@media (max-width: 768px) {
+ .comment-content pre,
+ .comment-body pre {
+ font-size: 11px;
+ padding: 8px;
+ margin: 8px 0;
+ }
+
+ .commentlist .comment-body {
+ max-width: calc(100% - 10px);
+ margin-left: 5px;
+ margin-right: 5px;
+ }
+
+ /* 移动端评论缩进调整 */
+ .commentlist li[style*="margin-left"] {
+ margin-left: 1rem !important;
+ max-margin-left: 2rem !important;
+ }
+}
+
+/* 防止表格溢出 */
+.comment-content table,
+.comment-body table {
+ max-width: 100%;
+ overflow-x: auto;
+ display: block;
+ white-space: nowrap;
+}
+
+/* 修复图片溢出 */
+.comment-content img,
+.comment-body img {
+ max-width: 100% !important;
+ height: auto !important;
+}
+
+/* 修复引用块 */
+.comment-content blockquote,
+.comment-body blockquote {
+ max-width: 100%;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ padding: 10px 15px;
+ margin: 10px 0;
+ border-left: 4px solid #ddd;
+ background-color: #f9f9f9;
}
\ No newline at end of file
diff --git a/blog/static/mathjax/js/mathjax-config.js b/blog/static/mathjax/js/mathjax-config.js
deleted file mode 100644
index 158ba65a..00000000
--- a/blog/static/mathjax/js/mathjax-config.js
+++ /dev/null
@@ -1,21 +0,0 @@
-$(function () {
- MathJax.Hub.Config({
- showProcessingMessages: false, //关闭js加载过程信息
- messageStyle: "none", //不显示信息
- extensions: ["tex2jax.js"], jax: ["input/TeX", "output/HTML-CSS"], displayAlign: "left", tex2jax: {
- inlineMath: [["$", "$"]], //行内公式选择$
- displayMath: [["$$", "$$"]], //段内公式选择$$
- skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'a'], //避开某些标签
- }, "HTML-CSS": {
- availableFonts: ["STIX", "TeX"], //可选字体
- showMathMenu: false //关闭右击菜单显示
- }
- });
- // 识别范围 => 文章内容、评论内容标签
- const contentId = document.getElementById("content");
- const commentId = document.getElementById("comments");
- MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentId, commentId]);
-})
-
-
-
diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py
index d6cd5d5a..03c28324 100644
--- a/blog/templatetags/blog_tags.py
+++ b/blog/templatetags/blog_tags.py
@@ -51,7 +51,14 @@ def datetimeformat(data):
@register.filter()
@stringfilter
def custom_markdown(content):
- return mark_safe(CommonMarkdown.get_markdown(content))
+ html_content = CommonMarkdown.get_markdown(content)
+
+ # 然后应用插件过滤器优化HTML
+ from djangoblog.plugin_manage import hooks
+ from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
+ optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content)
+
+ return mark_safe(optimized_html)
@register.simple_tag
diff --git a/blog/views.py b/blog/views.py
index d5dc7ec0..ace9e636 100644
--- a/blog/views.py
+++ b/blog/views.py
@@ -154,10 +154,6 @@ class ArticleDetailView(DetailView):
article = self.object
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
- # # Filter Hook, 允许插件修改文章正文
- article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
- request=self.request)
-
return context
diff --git a/djangoblog/settings.py b/djangoblog/settings.py
index d076bb63..6f071cec 100644
--- a/djangoblog/settings.py
+++ b/djangoblog/settings.py
@@ -318,6 +318,21 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
+# 安全头部配置 - 防XSS和其他攻击
+SECURE_BROWSER_XSS_FILTER = True
+SECURE_CONTENT_TYPE_NOSNIFF = True
+SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
+
+# 内容安全策略 (CSP) - 防XSS攻击
+CSP_DEFAULT_SRC = ["'self'"]
+CSP_SCRIPT_SRC = ["'self'", "'unsafe-inline'", "cdn.mathjax.org", "*.googleapis.com"]
+CSP_STYLE_SRC = ["'self'", "'unsafe-inline'", "*.googleapis.com", "*.gstatic.com"]
+CSP_IMG_SRC = ["'self'", "data:", "*.lylinux.net", "*.gravatar.com", "*.githubusercontent.com"]
+CSP_FONT_SRC = ["'self'", "*.googleapis.com", "*.gstatic.com"]
+CSP_CONNECT_SRC = ["'self'"]
+CSP_FRAME_SRC = ["'none'"]
+CSP_OBJECT_SRC = ["'none'"]
+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
@@ -339,5 +354,7 @@ ACTIVE_PLUGINS = [
'reading_time',
'external_links',
'view_count',
- 'seo_optimizer'
-]
\ No newline at end of file
+ 'seo_optimizer',
+ 'image_lazy_loading',
+]
+
diff --git a/djangoblog/utils.py b/djangoblog/utils.py
index 57f63dca..91d2b913 100644
--- a/djangoblog/utils.py
+++ b/djangoblog/utils.py
@@ -224,9 +224,49 @@ def get_resource_url():
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
- 'h2', 'p']
-ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
-
+ 'h2', 'p', 'span', 'div']
+
+# 安全的class值白名单 - 只允许代码高亮相关的class
+ALLOWED_CLASSES = [
+ 'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs',
+ 'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt',
+ 'ld', 'm', 'mf', 'mh', 'mi', 'mo', 'na', 'nb', 'nc', 'no', 'nd', 'ni', 'ne', 'nf', 'nl', 'nn',
+ 'nt', 'nv', 'ow', 'w', 'mb', 'mh', 'mi', 'mo', 'sb', 'sc', 'sd', 'se', 'sh', 'si', 'sx', 's2',
+ 's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il'
+]
+
+def class_filter(tag, name, value):
+ """自定义class属性过滤器"""
+ if name == 'class':
+ # 只允许预定义的安全class值
+ allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES]
+ return ' '.join(allowed_classes) if allowed_classes else False
+ return value
+
+# 安全的属性白名单
+ALLOWED_ATTRIBUTES = {
+ 'a': ['href', 'title'],
+ 'abbr': ['title'],
+ 'acronym': ['title'],
+ 'span': class_filter,
+ 'div': class_filter,
+ 'pre': class_filter,
+ 'code': class_filter
+}
+
+# 安全的协议白名单 - 防止javascript:等危险协议
+ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
def sanitize_html(html):
- return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
+ """
+ 安全的HTML清理函数
+ 使用bleach库进行白名单过滤,防止XSS攻击
+ """
+ return bleach.clean(
+ html,
+ tags=ALLOWED_TAGS,
+ attributes=ALLOWED_ATTRIBUTES,
+ protocols=ALLOWED_PROTOCOLS, # 限制允许的协议
+ strip=True, # 移除不允许的标签而不是转义
+ strip_comments=True # 移除HTML注释
+ )
diff --git a/plugins/image_lazy_loading/__init__.py b/plugins/image_lazy_loading/__init__.py
new file mode 100644
index 00000000..2d27de09
--- /dev/null
+++ b/plugins/image_lazy_loading/__init__.py
@@ -0,0 +1 @@
+# Image Lazy Loading Plugin
diff --git a/plugins/image_lazy_loading/plugin.py b/plugins/image_lazy_loading/plugin.py
new file mode 100644
index 00000000..b4b9e0a8
--- /dev/null
+++ b/plugins/image_lazy_loading/plugin.py
@@ -0,0 +1,182 @@
+import re
+import hashlib
+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 ImageOptimizationPlugin(BasePlugin):
+ PLUGIN_NAME = '图片性能优化插件'
+ PLUGIN_DESCRIPTION = '自动为文章中的图片添加懒加载、异步解码等性能优化属性,显著提升页面加载速度。'
+ PLUGIN_VERSION = '1.0.0'
+ PLUGIN_AUTHOR = 'liangliangyy'
+
+ def __init__(self):
+ # 插件配置
+ self.config = {
+ 'enable_lazy_loading': True, # 启用懒加载
+ 'enable_async_decoding': True, # 启用异步解码
+ 'add_loading_placeholder': True, # 添加加载占位符
+ 'optimize_external_images': True, # 优化外部图片
+ 'add_responsive_attributes': True, # 添加响应式属性
+ 'skip_first_image': True, # 跳过第一张图片(LCP优化)
+ }
+ super().__init__()
+
+ def register_hooks(self):
+ hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.optimize_images)
+
+ def optimize_images(self, content, *args, **kwargs):
+ """
+ 优化文章中的图片标签
+ """
+ if not content:
+ return content
+
+ # 正则表达式匹配 img 标签
+ img_pattern = re.compile(
+ r']*?)(?:\s*/)?>',
+ re.IGNORECASE | re.DOTALL
+ )
+
+ image_count = 0
+
+ def replace_img_tag(match):
+ nonlocal image_count
+ image_count += 1
+
+ # 获取原始属性
+ original_attrs = match.group(1)
+
+ # 解析现有属性
+ attrs = self._parse_img_attributes(original_attrs)
+
+ # 应用优化
+ optimized_attrs = self._apply_optimizations(attrs, image_count)
+
+ # 重构 img 标签
+ return self._build_img_tag(optimized_attrs)
+
+ # 替换所有 img 标签
+ optimized_content = img_pattern.sub(replace_img_tag, content)
+
+ return optimized_content
+
+ def _parse_img_attributes(self, attr_string):
+ """
+ 解析 img 标签的属性
+ """
+ attrs = {}
+
+ # 正则表达式匹配属性
+ attr_pattern = re.compile(r'(\w+)=(["\'])(.*?)\2')
+
+ for match in attr_pattern.finditer(attr_string):
+ attr_name = match.group(1).lower()
+ attr_value = match.group(3)
+ attrs[attr_name] = attr_value
+
+ return attrs
+
+ def _apply_optimizations(self, attrs, image_index):
+ """
+ 应用各种图片优化
+ """
+ # 1. 懒加载优化(跳过第一张图片以优化LCP)
+ if self.config['enable_lazy_loading']:
+ if not (self.config['skip_first_image'] and image_index == 1):
+ if 'loading' not in attrs:
+ attrs['loading'] = 'lazy'
+
+ # 2. 异步解码
+ if self.config['enable_async_decoding']:
+ if 'decoding' not in attrs:
+ attrs['decoding'] = 'async'
+
+ # 3. 添加样式优化
+ current_style = attrs.get('style', '')
+
+ # 确保图片不会超出容器
+ if 'max-width' not in current_style:
+ if current_style and not current_style.endswith(';'):
+ current_style += ';'
+ current_style += 'max-width:100%;height:auto;'
+ attrs['style'] = current_style
+
+ # 4. 添加 alt 属性(SEO和可访问性)
+ if 'alt' not in attrs:
+ # 尝试从图片URL生成有意义的alt文本
+ src = attrs.get('src', '')
+ if src:
+ # 从文件名生成alt文本
+ filename = src.split('/')[-1].split('.')[0]
+ # 移除常见的无意义字符
+ clean_name = re.sub(r'[0-9a-f]{8,}', '', filename) # 移除长hash
+ clean_name = re.sub(r'[_-]+', ' ', clean_name).strip()
+ attrs['alt'] = clean_name if clean_name else '文章图片'
+ else:
+ attrs['alt'] = '文章图片'
+
+ # 5. 外部图片优化
+ if self.config['optimize_external_images'] and 'src' in attrs:
+ src = attrs['src']
+ parsed_url = urlparse(src)
+
+ # 如果是外部图片,添加 referrerpolicy
+ if parsed_url.netloc and parsed_url.netloc != self._get_current_domain():
+ attrs['referrerpolicy'] = 'no-referrer-when-downgrade'
+ # 为外部图片添加crossorigin属性以支持性能监控
+ if 'crossorigin' not in attrs:
+ attrs['crossorigin'] = 'anonymous'
+
+ # 6. 响应式图片属性(如果配置启用)
+ if self.config['add_responsive_attributes']:
+ # 添加 sizes 属性(如果没有的话)
+ if 'sizes' not in attrs and 'srcset' not in attrs:
+ attrs['sizes'] = '(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
+
+ # 7. 添加图片唯一标识符用于性能追踪
+ if 'data-img-id' not in attrs and 'src' in attrs:
+ img_hash = hashlib.md5(attrs['src'].encode()).hexdigest()[:8]
+ attrs['data-img-id'] = f'img-{img_hash}'
+
+ # 8. 为第一张图片添加高优先级提示(LCP优化)
+ if image_index == 1 and self.config['skip_first_image']:
+ attrs['fetchpriority'] = 'high'
+ # 移除懒加载以确保快速加载
+ if 'loading' in attrs:
+ del attrs['loading']
+
+ return attrs
+
+ def _build_img_tag(self, attrs):
+ """
+ 重新构建 img 标签
+ """
+ attr_strings = []
+
+ # 确保 src 属性在最前面
+ if 'src' in attrs:
+ attr_strings.append(f'src="{attrs["src"]}"')
+
+ # 添加其他属性
+ for key, value in attrs.items():
+ if key != 'src': # src 已经添加过了
+ attr_strings.append(f'{key}="{value}"')
+
+ return f'
'
+
+ def _get_current_domain(self):
+ """
+ 获取当前网站域名
+ """
+ try:
+ from djangoblog.utils import get_current_site
+ return get_current_site().domain
+ except:
+ return ''
+
+
+# 实例化插件
+plugin = ImageOptimizationPlugin()
diff --git a/plugins/seo_optimizer/plugin.py b/plugins/seo_optimizer/plugin.py
index b5b19a33..de12c152 100644
--- a/plugins/seo_optimizer/plugin.py
+++ b/plugins/seo_optimizer/plugin.py
@@ -97,6 +97,8 @@ class SeoOptimizerPlugin(BasePlugin):
structured_data = {
"@context": "https://schema.org",
"@type": "WebSite",
+ "name": blog_setting.site_name,
+ "description": blog_setting.site_description,
"url": request.build_absolute_uri('/'),
"potentialAction": {
"@type": "SearchAction",
@@ -131,12 +133,15 @@ class SeoOptimizerPlugin(BasePlugin):
json_ld_script = f''
- return f"""
+ seo_html = f"""