增加插件&&部分优化

pull/5/head
liangliangyy 5 months ago
parent 76918f2c7f
commit 806f636eea

@ -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;
}

@ -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]);
})

@ -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

@ -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

@ -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'
]
'seo_optimizer',
'image_lazy_loading',
]

@ -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注释
)

@ -0,0 +1 @@
# Image Lazy Loading Plugin

@ -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'<img\s+([^>]*?)(?:\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'<img {" ".join(attr_strings)}>'
def _get_current_domain(self):
"""
获取当前网站域名
"""
try:
from djangoblog.utils import get_current_site
return get_current_site().domain
except:
return ''
# 实例化插件
plugin = ImageOptimizationPlugin()

@ -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'<script type="application/ld+json">{json.dumps(seo_data.get("json_ld", {}), ensure_ascii=False, indent=4)}</script>'
return f"""
seo_html = 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}
"""
# 将SEO内容追加到现有的metas内容上
return metas + seo_html
plugin = SeoOptimizerPlugin()

Binary file not shown.

@ -16,29 +16,37 @@
<!--<![endif]-->
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width"/>
{% block header %}
<title>{% block title %}{{ SITE_NAME }}{% endblock %}</title>
<meta name="description" content="{{ SITE_DESCRIPTION }}">
<meta name="keywords" content="{{ SITE_KEYWORDS }}">
{% endblock %}
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="format-detection" content="telephone=no"/>
<meta name="theme-color" content="#21759b"/>
{% load blog_tags %}
{% head_meta %}
{% block header %}
<!-- SEO插件会自动生成title、description、keywords等标签 -->
{% endblock %}
<link rel="profile" href="http://gmpg.org/xfn/11"/>
<!-- 预加载关键资源 -->
<link rel="preload" href="{% static 'blog/css/style.css' %}" as="style"/>
<link rel="preload" href="{% static 'blog/js/jquery-3.6.0.min.js' %}" as="script"/>
<!-- DNS预解析 -->
<link rel="dns-prefetch" href="//cdn.mathjax.org"/>
<link rel="dns-prefetch" href="//cdn.bootcss.com"/>
<!--[if lt IE 9]>
<script src="{% static 'blog/js/html5.js' %}" type="text/javascript"></script>
<![endif]-->
<link rel="alternate" type="application/rss+xml" title="{{ SITE_NAME }} &raquo; Feed"
href="/feed"/>
<link rel='stylesheet preload' as="font" id='twentytwelve-fonts-css'
href='{% static 'blog/fonts/fonts.css' %}'
type='text/css' media='all'/>
<link rel="dns-prefetch" href="//cdn.mathjax.org"/>
<!-- RSS和图标 -->
<link rel="alternate" type="application/rss+xml" title="{{ SITE_NAME }} &raquo; Feed" href="/feed"/>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"/>
<link rel="icon" href="/favicon.ico" type="image/x-icon"/>
<link rel="apple-touch-icon" href="/favicon.ico"/>
<!-- 字体预加载 -->
<link rel="preload" href="{% static 'blog/fonts/fonts.css' %}" as="style" onload="this.onload=null;this.rel='stylesheet'"/>
<noscript><link rel="stylesheet" href="{% static 'blog/fonts/fonts.css' %}"/></noscript>
{% compress css %}
<link rel='stylesheet' id='twentytwelve-style-css' href='{% static 'blog/css/style.css' %}' type='text/css'
@ -53,6 +61,7 @@
{% block compress_css %}
{% endblock %}
{% endcompress %}
{% if GLOBAL_HEADER %}
{{ GLOBAL_HEADER|safe }}
{% endif %}
@ -67,26 +76,6 @@
<h2 class="site-description">{{ SITE_DESCRIPTION }}</h2>
</hgroup>
{% load i18n %}
{# <div class="i18n-selector">#}
{# <form action="{% url 'set_language' %}" method="post" id="i18n-form">{% csrf_token %}#}
{# <input name="next" type="hidden" value="{{ redirect_to }}">#}
{# <select name="language" class="i18n-select" >#}
{# {% get_current_language as LANGUAGE_CODE %}#}
{# {% get_available_languages as LANGUAGES %}#}
{# {% get_language_info_list for LANGUAGES as languages %}#}
{# {% for language in languages %}#}
{# <option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>#}
{# {{ language.name_local }} ({{ language.code }})#}
{# </option>#}
{# {% endfor %}#}
{# </select>#}
{# <input type="submit" value="Go">#}
{# </form>#}
{# </div>#}
{% include 'share_layout/nav.html' %}
@ -105,19 +94,20 @@
{% include 'share_layout/footer.html' %}
</div><!-- #page -->
</body>
<footer>
<script src="//cdn.bootcss.com/mathjax/2.7.0/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<!-- JavaScript资源 -->
{% compress js %}
<script type="text/javascript" src="{% static 'blog/js/jquery-3.6.0.min.js' %}"></script>
<script type="text/javascript" src="{% static 'blog/js/nprogress.js' %}"></script>
<script src="{% static 'blog/js/blog.js' %}" type="application/javascript"></script>
<script src="{% static 'blog/js/navigation.js' %}" type="application/javascript"></script>
<script src="{% static 'mathjax/js/mathjax-config.js' %}" type="application/javascript"></script>
<script src="{% static 'blog/js/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'blog/js/nprogress.js' %}"></script>
<script src="{% static 'blog/js/blog.js' %}"></script>
<script src="{% static 'blog/js/navigation.js' %}"></script>
{% block compress_js %}
{% endblock %}
{% endcompress %}
<!-- MathJax智能加载器 -->
<script src="{% static 'blog/js/mathjax-loader.js' %}" async defer></script>
{% block footer %}
{% endblock %}
</footer>
</body>
</html>

Loading…
Cancel
Save