本系统是一个轻量级的个人博客内容管理方案,支持通过 Web 界面上传 Markdown 博文及关联图片。后端基于 Flask 完成 Markdown 到 HTML 的渲染(含 LaTeX 支持),生成静态页面并保存。不需要数据库,所有数据以文件系统方式组织。前端采用瀑布流布局展示文章列表,风格简洁美观,仿照朋友圈的卡片式设计。
markdown + 扩展(codehilite, fenced_code, tables 等)$...$ / $$...$$ 标记)posts_index.json 文件存储所有文章索引(无数据库).
├── app.py # Flask 主程序,路由与核心逻辑
├── config.py # 配置文件(上传限制、路径等)
├── requirements.txt # Python 依赖列表
├── templates/ # Jinja2 模板
│ ├── base.html # 全局基础模板(含 meta、MathJax、导航栏)
│ ├── index.html # 博客首页(瀑布流文章列表)
│ ├── upload.html # 上传表单页面
│ └── admin.html # 管理页面(展示所有文章,提供删除操作)
├── static/ # 静态资源(Flask 自动提供)
│ ├── css/
│ │ └── style.css # 主样式表(瀑布流、卡片、暗色/亮色风格)
│ ├── js/
│ │ └── main.js # 可选前端交互(如瀑布流优化、懒加载)
│ └── uploads/ # 用户上传的图片存储目录
│ └── posts/ # 每篇文章的图片子目录(详见下文)
├── generated/ # 生成的静态 HTML 文件
│ └── <post_id>.html # 每篇文章对应的独立页面
├── posts_data/ # 原始数据存储
│ └── <post_id>/ # 以文章 ID 命名的目录
│ ├── content.md # 原始 Markdown 文件
│ └── images/ # 该文章用到的图片(软链接或实际存储)
│ └── 图片文件...
└── posts_index.json # 全局索引文件,存储所有文章元数据
说明:
- 图片实际保存到
static/uploads/posts/<post_id>/下,以便通过/static/uploads/posts/<post_id>/image.png直接访问。posts_data/<post_id>/images/可以是一个目录软链接,也可以在主程序中统一按 ID 查找图片路径。为简化,可以在上传时直接将图片保存到static/uploads/posts/<post_id>/,而posts_data中只保留 Markdown 副本。两种方案均可,本设计采用统一保存到static/uploads/posts/<post_id>/,在posts_data中不再重复存储图片。
| 文件路径 | 作用 |
|---|---|
app.py |
Flask 应用入口,定义所有路由(首页、上传、文章详情、管理、删除) |
config.py |
配置项:UPLOAD_FOLDER, GENERATED_FOLDER, INDEX_FILE, ALLOWED_EXTENSIONS, MAX_CONTENT_LENGTH 等 |
requirements.txt |
依赖列表(Flask, markdown, Pygments 等) |
templates/base.html |
全局模板,包含 HTML 骨架、导航栏、MathJax 脚本、基础 CSS/JS 引用 |
templates/index.html |
首页,继承 base.html,使用瀑布流卡片渲染文章列表 |
templates/upload.html |
上传表单,支持 Markdown 文件 + 多图片选择 |
templates/admin.html |
后台管理,以表格形式列出所有文章,提供删除按钮 |
static/css/style.css |
完整样式定义(瀑布流栅格、卡片样式、按钮、代码高亮等) |
static/js/main.js |
可选前端增强(如卡片悬停效果、动态瀑布流重排) |
posts_index.json |
运行时自动生成/更新,存储结构见下文 |
/upload,接收 title(字符串),markdown_file(文件),images(多文件列表)。int(time.time()*1000))或 uuid4 生成唯一 post_id。static/uploads/posts/<post_id>/posts_data/<post_id>/posts_data/<post_id>/content.md。static/uploads/posts/<post_id>/ 下,保留原文件名(需过滤危险字符)。content.md 内容,提取 Front Matter(若存在,使用 frontmatter 库或正则),获得 title, date, summary 等。若 Front Matter 缺失,则使用表单中的 title,当前时间作为发布日期,摘要从正文前 200 字生成。 式的本地图片引用,将 filename 替换为 /static/uploads/posts/<post_id>/filename。若图片未上传,记录警告但继续渲染。markdown.markdown() 并将扩展启用:extra, codehilite, tables, fenced_code。$...$ 和 $$...$$(不进行转义)。post_template.html,可从 base.html 继承)渲染完整 HTML,内容区为上述生成的 HTML,标题为文章标题。generated/<post_id>.html。posts_index.json,新增一条记录:{ "id": post_id, "title": title, "date": iso_date, "summary": summary, "thumbnail": 首张图片路径或空 }。/ 读取 posts_index.json,将文章列表按日期倒序传入模板。前端使用 CSS Grid 布局实现瀑布流:
.waterfall {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.card {
background: white;
border-radius: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
overflow: hidden;
transition: transform 0.2s;
}
.card:hover { transform: translateY(-4px); }
每个卡片包含:文章标题、日期、摘要、若存在缩略图则显示在卡片顶部。缩略图可从索引的 thumbnail 字段获取。
/post/<post_id> 使用 send_from_directory 返回 generated 目录下的 <post_id>.html。GET /admin,读取索引展示所有文章,提供删除按钮(需注意 CSRF,可简单用 POST 表单)。POST /admin/delete/<post_id>,执行以下操作:generated/<post_id>.htmlposts_data/<post_id>/ 整个目录static/uploads/posts/<post_id>/ 整个目录posts_index.json 中移除该条目并保存/adminposts_index.json 结构[
{
"id": "1703123456789",
"title": "Python 中的装饰器",
"date": "2023-12-21T10:30:00",
"summary": "装饰器是 Python 一个强大的功能...",
"thumbnail": "/static/uploads/posts/1703123456789/code.png"
},
...
]
若用户希望在 Markdown 文件头部定义元数据,支持以下格式:
---
title: 文章标题
date: 2024-01-01
summary: 自定义摘要
---
可使用 python-frontmatter 库解析,若未安装则回退到正则或忽略。在本设计中推荐使用,但非强制。
在渲染 Markdown 之前,预处理文本:
def fix_image_paths(md_content: str, post_id: str) -> str:
pattern = r'!\[([^\]]*)\]\(([^)]+)\)'
def repl(match):
alt, src = match.groups()
# 仅当 src 不含 '/' 或 'http' 且扩展名为图片时视为本地文件
if not src.startswith(('http://', 'https://', '/', '#')) and '.' in src:
new_src = f"/static/uploads/posts/{post_id}/{src}"
return f''
return match.group(0)
return re.sub(pattern, repl, md_content)
import markdown
md = markdown.Markdown(extensions=[
'extra',
'codehilite',
'tables',
'fenced_code'
])
html_body = md.convert(fixed_md)
from bs4 import BeautifulSoup
soup = BeautifulSoup(html_body, 'html.parser')
text = soup.get_text()
summary = text[:200] + ('...' if len(text) > 200 else '')
优先使用 Front Matter 中指定的 thumbnail,否则取正文中第一张图片的 URL。
#f8fafc(浅灰色),卡片纯白 #ffffff,圆角大半径(20px)。minmax(250px,1fr)。<img> 宽度 100%,高度 180px,object-fit: cover。/post/<id>。使用 Pygments 生成 CSS 类,在 style.css 中引入一个默认高亮主题(如 monokai 或 friendly)。需在 Markdown 渲染时启用 codehilite 扩展并生成对应 HTML。
在 base.html 中添加:
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" id="MathJax-script" async></script>
并设置 tex: { inlineMath: [['$', '$'], ['\\(', '\\)']] }。
| 路径 | 方法 | 说明 |
|---|---|---|
/ |
GET | 首页,瀑布流文章列表 |
/upload |
GET | 显示上传表单 |
/upload |
POST | 处理上传,保存文章并生成静态页 |
/post/<post_id> |
GET | 查看静态文章详情 |
/admin |
GET | 管理页面,列出所有文章 |
/admin/delete/<post_id> |
POST | 删除文章及相关文件 |
/static/<path:filename> |
GET | Flask 默认静态文件路由(用于 CSS、JS、图片) |
pip install -r requirements.txtpython app.py(默认监听 0.0.0.0:5000)generated/ 目录通过 Nginx 直接托管提升性能,但 Flask 本身也可完成。tags 列表,前端做筛选。本设计文档已覆盖完整架构、目录结构、核心代码逻辑及样式取向。请后续根据本设计实现具体代码。