From 806f636eea2153d5fcd73cf1c9b14c67a1e00ecc Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sun, 28 Sep 2025 16:30:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=8F=92=E4=BB=B6&&=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog/static/blog/css/style.css | 202 +++++++++++++++++++++++ blog/static/mathjax/js/mathjax-config.js | 21 --- blog/templatetags/blog_tags.py | 9 +- blog/views.py | 4 - djangoblog/settings.py | 21 ++- djangoblog/utils.py | 48 +++++- plugins/image_lazy_loading/__init__.py | 1 + plugins/image_lazy_loading/plugin.py | 182 ++++++++++++++++++++ plugins/seo_optimizer/plugin.py | 7 +- requirements.txt | Bin 2554 -> 1656 bytes templates/share_layout/base.html | 72 ++++---- 11 files changed, 493 insertions(+), 74 deletions(-) delete mode 100644 blog/static/mathjax/js/mathjax-config.js create mode 100644 plugins/image_lazy_loading/__init__.py create mode 100644 plugins/image_lazy_loading/plugin.py diff --git a/blog/static/blog/css/style.css b/blog/static/blog/css/style.css index d43f7f3..bd317db 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 158ba65..0000000 --- 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 d6cd5d5..03c2832 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 d5dc7ec..ace9e63 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 d076bb6..6f071ce 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 57f63dc..91d2b91 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 0000000..2d27de0 --- /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 0000000..b4b9e0a --- /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 b5b19a3..de12c15 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""" {seo_data.get("title", "")} {seo_data.get("meta_tags", "")} {json_ld_script} """ + + # 将SEO内容追加到现有的metas内容上 + return metas + seo_html plugin = SeoOptimizerPlugin() diff --git a/requirements.txt b/requirements.txt index 9dc5c935191f166db408850beb747475a262f65f..4c0ad1508f70b43e9e02349807b675edb924fcf3 100644 GIT binary patch delta 53 zcmV-50LuUR6Zi~}Ad&bw0XCCi0TZ)E0iFSq3IlMHk^?-G`U6IjWCTdF&IE=6lMDzV Lla>fdlPn295%>~A delta 904 zcmY*Y%TB^j6g-sR!f0HuBqZ)wL0VqAF~o&&;lh}>b74vWMR~SG9vi;EMAI)Je1xCk zCum$bb8cHg$Sq0F+?hFZPJfU7@*k(`wv5FWR~B-CUtKEV;MC+;4u!Q4zkzhMYHPKI zXI1MS-utNcrH8}&Rk3jzDT@>1dMSeJj-2Te8u#MKGvs{Bcl0PXl3e~hfSu~1Myf%MoJezf>63>L|JQ@?@I@ghJX@|*wNg& zEWN+|>8LaPP%Gj(KSWRjPv$=83 z%y1^qN*jPeW`&D(tfzvbjFTWxI2MX1?NpQ&CJbi>?7!d1M=|&a0|L8O34qvd_@1TrIlF# diff --git a/templates/share_layout/base.html b/templates/share_layout/base.html index 75d0df5..32d5952 100644 --- a/templates/share_layout/base.html +++ b/templates/share_layout/base.html @@ -16,29 +16,37 @@ - - {% block header %} - {% block title %}{{ SITE_NAME }}{% endblock %} - - - {% endblock %} + + + {% load blog_tags %} {% head_meta %} + {% block header %} + + {% endblock %} + + + + + + + + - - - + + + + + + {% compress css %} {{ SITE_DESCRIPTION }} {% load i18n %} - - - {#
#} - {#
{% csrf_token %}#} - {# #} - {# #} - {# #} - {#
#} - {#
#} - - {% include 'share_layout/nav.html' %} @@ -105,19 +94,20 @@ {% include 'share_layout/footer.html' %} - - +