#文章发布与 Markdown 支持:富文本编辑器与分页
📂 所属阶段:第四阶段 — 实战演练(道满博客)
🔗 相关章节:项目架构重构(Blueprints) · 数据库关系设计
#1. Markdown 支持
#1.1 安装 markdown 库
pip install markdown bleach#1.2 渲染 Markdown
# app/utils.py
import markdown
import bleach
def render_markdown(content):
"""将 Markdown 渲染为安全的 HTML"""
# 支持常用扩展
html = markdown.markdown(
content,
extensions=[
"extra", # 表格、定义列表等
"codehilite", # 代码高亮
"toc", # 目录
]
)
# 清理危险标签
allowed_tags = [
"h1", "h2", "h3", "h4", "h5", "h6",
"p", "br", "strong", "em", "u", "s",
"blockquote", "pre", "code",
"ul", "ol", "li",
"a", "img",
"table", "thead", "tbody", "tr", "th", "td",
"div", "span",
]
return bleach.clean(html, tags=allowed_tags, attributes={"a": ["href", "title"], "img": ["src", "alt", "title"], "code": ["class"], "span": ["class"]})#1.3 在文章详情页渲染
# app/articles/routes.py
from app.utils import render_markdown
@articles_bp.route("/<int:post_id>")
def detail(post_id):
post = Post.query.get_or_404(post_id)
post.views += 1
db.session.commit()
# 渲染 Markdown
rendered_content = render_markdown(post.content)
return render_template("articles/detail.html", post=post, rendered_content=rendered_content)<!-- templates/articles/detail.html -->
<article class="article">
<h1>{{ post.title }}</h1>
<div class="article-meta">
<span>作者:{{ post.author.username }}</span>
<span>发布时间:{{ post.created_at.strftime('%Y-%m-%d') }}</span>
<span>阅读:{{ post.views }}</span>
</div>
<!-- 渲染后的 HTML -->
<div class="article-content">
{{ rendered_content|safe }}
</div>
</article>#2. 文章表单
# app/forms/article.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SelectField, BooleanField
from wtforms.validators import DataRequired, Length, Optional
class ArticleForm(FlaskForm):
title = StringField("标题", validators=[
DataRequired(message="标题不能为空"),
Length(min=5, max=200, message="标题长度 5-200 个字符")
])
summary = StringField("摘要", validators=[
Optional(),
Length(max=300)
])
content = TextAreaField("正文(支持 Markdown)", validators=[
DataRequired(message="正文不能为空"),
Length(min=10)
])
category_id = SelectField("分类", coerce=int)
tags = StringField("标签", description="多个标签用逗号分隔")
is_published = BooleanField("立即发布")
submit = SubmitField("发布")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from app.models import Category
self.category_id.choices = [(0, "无")] + [
(c.id, c.name) for c in Category.query.all()
]#3. 文章列表分页
@articles_bp.route("/")
def index():
page = request.args.get("page", 1, type=int)
per_page = 10
pagination = Post.query\
.filter_by(is_published=True)\
.order_by(Post.created_at.desc())\
.paginate(page=page, per_page=per_page, error_out=False)
return render_template("articles/index.html", pagination=pagination)模板分页组件:
<!-- templates/macros/pagination.html -->
{% macro render_pagination(pagination, endpoint) %}
{% if pagination.pages > 1 %}
<nav aria-label="分页导航">
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for(endpoint, page=pagination.prev_num) }}">上一页</a>
</li>
{% endif %}
{% for p in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if p %}
<li class="page-item {% if p == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for(endpoint, page=p) }}">{{ p }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for(endpoint, page=pagination.next_num) }}">下一页</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endmacro %}#4. 文章标签处理
# 创建/更新文章时处理标签
def process_tags(post, tags_string):
"""解析标签字符串,更新文章标签"""
if not tags_string:
return
tag_names = [t.strip() for t in tags_string.split(",") if t.strip()]
from app.models import Tag
for name in tag_names:
tag = Tag.query.filter_by(name=name).first()
if not tag:
tag = Tag(name=name)
db.session.add(tag)
if tag not in post.tags:
post.tags.append(tag)
# 移除不在新列表中的标签
current_names = set(tag_names)
post.tags = [t for t in post.tags if t.name in current_names]#5. 小结
Markdown 文章发布流程:
1. 安装:pip install markdown bleach
2. 渲染:render_markdown(content)
3. 清理:bleach.clean(html, tags=[...], attributes={...})
4. 显示:{{ rendered_content|safe }}💡 安全提示:永远不要对用户输入的 HTML/Markdown 不做清理直接渲染!XSS 攻击可以通过 Markdown 评论植入恶意脚本。
🔗 扩展阅读

