零依赖、纯本地部署的终端风格博客。Python 后端 + 纯前端,无需 Node.js、数据库或任何第三方服务。
| 分类 | 功能 |
|---|---|
| 文章管理 | 终端命令式导航 (cd/ls/pwd)、路径分类、全文搜索、批量操作 |
| 编辑器 | 全屏 Markdown 编辑器、16 按钮工具栏、左右分屏实时预览、代码块子编辑器 |
| 渲染 | 完整 Markdown 支持、14 种语言语法高亮、终端风格排版、语言标签 |
| 同步 | SSE 服务端推送,所有客户端实时同步文章变更 |
| 外观 | 终端/玻璃拟态风格、自定义背景(图片/视频)、毛玻璃效果 |
| 安全 | PBKDF2 密码存储(60 万次迭代)、登录限流、Token 24h 过期、CORS 白名单 |
| 部署 | 纯 Python 标准库、systemd 服务模板、零外部网络依赖、非 root 运行 |
git clone https://github.com/xzy4260/blog.git
cd blog仅需 Python 3 标准库 + qrcode(用于生成二维码):
pip3 install qrcode[pil]python3 server.py默认监听 0.0.0.0:3000,访问:
- 博客首页:
http://localhost:3000 - 管理后台:
http://localhost:3000/admin
- 打开管理后台,设置管理员密码
- 在「系统设置」中配置博客标题、昵称、头像等
- 点击「新建文章」开始写作
PORT=8080 HOST=127.0.0.1 python3 server.py| 变量 | 默认值 | 说明 |
|---|---|---|
PORT |
3000 |
监听端口 |
HOST |
0.0.0.0 |
监听地址 |
CORS_ORIGINS |
空(允许所有) | 逗号分隔的允许来源域名 |
编辑 start.sh,修改端口号:
#!/bin/bash
cd "$(dirname "$0")"
export PORT=8080 # 改为你想要的端口
exec python3 server.py赋予执行权限后运行:
chmod +x start.sh
./start.sh- 创建专用用户并部署:
sudo useradd -r -s /usr/sbin/nologin blog
sudo cp -r . /opt/blog
sudo chown -R blog:blog /opt/blog- 编辑
blog.service,修改端口:
[Unit]
Description=Blog Server
After=network.target
[Service]
Type=simple
User=blog
Group=blog
WorkingDirectory=/opt/blog
Environment=PORT=3000
ExecStart=/usr/bin/python3 server.py
Restart=always
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/opt/blog/data /opt/blog/assets
PrivateTmp=true
[Install]
WantedBy=multi-user.target- 安装并启动服务:
sudo cp blog.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now blog.service- 查看状态:
sudo systemctl status blog.service
sudo journalctl -u blog.service -f如果需要 HTTPS 或域名绑定,建议在前面加 Nginx:
server {
listen 443 ssl;
server_name blog.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# SSE 长连接需要额外配置
location /api/events {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
}
}首页顶部有一个终端式路径栏,支持以下命令:
| 命令 | 功能 | 示例 |
|---|---|---|
ls |
列出当前路径的子目录和文章 | ls |
cd <路径> |
进入指定路径 | cd /技术/Python/ |
cd .. |
返回上级目录 | cd .. |
cd / |
返回根目录 | cd / |
pwd |
显示当前完整路径 | pwd |
help |
显示帮助信息 | help |
点击路径栏右侧的 ? 按钮进入搜索模式,搜索范围包括文章标题、内容、标签。
文章通过 path 字段进行分类,类似文件系统目录结构。管理后台支持:
- 创建文件夹
- 移动/复制/重命名文章和文件夹
- 多选批量操作(删除、移动、复制、下载)
管理后台内置全屏 Markdown 编辑器,覆盖几乎整个屏幕。
┌─────────────────────────────────────────────────────┐
│ [标题输入] [标签输入] [状态选择] [取消] [保存] │
├─────────────────────────────────────────────────────┤
│ H1 H2 H3 | B I S U | </> " | UL OL [ ] | Link Img │
├────────────────────┬────────────────────────────────┤
│ │ │
│ Markdown 编辑 │ 实时预览 │
│ │ │
└────────────────────┴────────────────────────────────┘
| 按钮 | 功能 | 插入语法 |
|---|---|---|
H1 H2 H3 |
标题 | # ## ### (行首插入) |
B |
粗体 | **文字**(选中包裹) |
I |
斜体 | *文字*(选中包裹) |
S |
删除线 | ~~文字~~(选中包裹) |
U |
下划线 | ++文字++(选中包裹) |
` |
行内代码 | `代码`(选中包裹) |
</> |
代码块 | 弹出子编辑器,选择语言后插入 |
" |
引用 | > 文字(行首插入) |
UL |
无序列表 | - 项目(行首插入) |
OL |
有序列表 | 1. 项目(行首插入) |
[ ] |
任务列表 | - [ ] 任务(行首插入) |
Link |
链接 | [文字](url) |
Img |
图片 |  |
Table |
表格 | 自动生成 3×3 表格 |
--- |
分割线 | ---(独占一行) |
点击 </> 按钮弹出代码块编辑器:
- 选择编程语言(支持 20+ 种)
- 输入代码内容
- 右侧实时预览语法高亮效果
- 点击「插入」将
```lang\n...\n```放到编辑器光标位置
代码块支持 14 种语言的语法高亮,采用 VS Code 暗色配色方案:
| 语言 | 关键字示例 |
|---|---|
| Python | def, class, import, if, for, return, True, None |
| JavaScript | function, const, let, var, if, for, return, true, null |
| TypeScript | 同 JS + interface, type, enum, as, readonly |
| HTML | 标签名、属性名、属性值 |
| CSS | 属性名、选择器、颜色值、单位 |
| JSON | 键名、字符串、数字、布尔值 |
| Shell | 注释、变量、字符串 |
| Go | func, package, import, if, for, return, nil, true |
| Rust | fn, let, mut, if, for, return, true, None |
| Java | public, class, void, if, for, return, true, null |
| C | int, void, if, for, return, NULL |
| C++ | 同 C + class, template, namespace, nullptr |
| YAML | 注释、键名、字符串 |
| Lua | function, local, if, for, return, nil, true |
| 元素 | 颜色 |
|---|---|
| 注释 | 绿色 #6a9955 |
| 字符串 | 橙色 #ce9178 |
| 关键字 | 蓝色 #569cd6 |
| 函数名 | 黄色 #dcdcaa |
| 数字/布尔 | 浅绿 #b5cea8 |
| HTML 标签 | 青色 #4ec9b0 |
| CSS 属性 | 浅蓝 #9cdcfe |
| 装饰器 | 黄色 #dcdcaa |
访问 /admin 进入管理后台。
- 文章总数统计
- 已发布/草稿数量
- 快捷操作入口
- 文章列表,支持终端路径导航
- 多选模式:勾选多个文章/文件夹
- 批量操作:删除、移动、复制、下载
- 文件夹管理:创建、重命名、移动、复制、删除
6 个配置卡片:
| 卡片 | 配置项 |
|---|---|
| 主站信息 | 博客标题、标签页名称、标签页图标 |
| 个人资料 | 昵称、身份、头像、个签(含颜色选择) |
| 联系方式 | GitHub / Email / QQ / 微信 / 微博,支持排序 |
| 背景设置 | 博客/管理后台背景(图片/视频/关闭) |
| 安全 | 修改密码 |
| 数据 | 数据导入/导出 |
- 全局毛玻璃效果:
backdrop-filter: blur(20px) saturate(1.6) - 组件半透明背景,层次分明
- JetBrains Mono 英文等宽字体
- GoogleSans 中文字体
- 像素风格标题动画
支持三种模式:
| 模式 | 说明 | 限制 |
|---|---|---|
video |
本地视频背景 | MP4/WebM/MOV,≤ 200MB,分辨率 ≥ 640×360 |
image |
本地图片背景 | JPG/PNG/WebP,≤ 5MB,分辨率 ≥ 800×600 |
off |
纯黑背景 | — |
博客和管理后台的背景可以分别设置。
所有接口返回 JSON,POST/PUT/DELETE 接口需要认证(除登录/设置密码外)。
在请求头中携带 Token:
Authorization: Bearer <token>
Token 通过登录接口获取,24 小时后自动过期。
获取所有文章列表。
响应:
{
"articles": [
{
"id": "art_17180000001234",
"title": "文章标题",
"tags": "标签1,标签2",
"content": "Markdown 内容",
"status": "published",
"date": "2024-06-13",
"path": "/技术/Python/",
"type": "article"
}
]
}说明:
- 按日期倒序排列
- 包含所有状态的文章(published / draft)
- 自动补充缺失的
type(默认article)和path(默认/)
获取单篇文章详情。
响应: 文章 JSON 对象,字段同上。
错误: 404 {"error": "not found"}
获取博客配置。
响应:
{
"author": "站长",
"nickname": "独立开发者",
"role": "Full-Stack Engineer",
"avatar": "/assets/avatar.png",
"title": "~/blog",
"desc": "终端风格个人博客",
"favicon": "",
"faviconUseAvatar": false,
"pixelColor": "#7c8aff",
"links": [
{"type": "github", "url": "https://github.com/...", "label": "GitHub"},
{"type": "email", "url": "mailto:...", "label": "Email"},
{"type": "qq", "url": "...", "label": "QQ"},
{"type": "wechat", "url": "...", "label": "微信"},
{"type": "weibo", "url": "...", "label": "微博"}
],
"motto": "座右铭内容",
"mottoColor": "#ffffff",
"blogBgType": "image",
"blogBg": "/assets/bg-blog.jpg",
"adminBgType": "off",
"adminBg": ""
}获取文件夹列表。
响应:
{
"dirs": ["/技术/", "/技术/Python/", "/生活/"]
}检查是否已设置密码(无需认证)。
响应:
{"hasPassword": true}SSE(Server-Sent Events)实时推送端点。
响应: text/event-stream 流,当文章发生变更时推送 data: updated\n\n,空闲时每秒发送心跳 : keepalive\n\n。
前端用法:
const es = new EventSource('/api/events');
es.onmessage = (e) => {
if (e.data === 'updated') {
// 重新加载文章列表
}
};生成二维码图片。
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url |
string | 是 | 要编码的 URL |
响应: image/png 图片,灰色前景 + 深色背景,缓存 24 小时。
错误: 400 {"error": "missing url"}
首次设置管理员密码(无需认证,仅可调用一次)。
请求体:
{"password": "你的密码"}响应:
200 {"ok": true}400 {"error": "密码已设置"}400 {"error": "密码至少4个字符"}
登录获取 Token(无需认证)。
请求体:
{"password": "你的密码"}响应:
{"ok": true, "token": "64位hex字符串"}限流: 同一 IP 每分钟最多 5 次失败尝试,超出返回 429 {"error": "尝试次数过多,请稍后再试"}。
自动迁移: 如果旧密码使用 SHA256 存储,登录成功后自动升级为 PBKDF2。
修改密码(需要认证)。
请求体:
{"oldPassword": "旧密码", "newPassword": "新密码"}响应:
200 {"ok": true}400 {"error": "新密码至少4个字符"}401 {"error": "原密码错误"}
创建文章(需要认证)。
请求体:
{
"title": "文章标题",
"tags": "标签1,标签2",
"content": "Markdown 内容",
"status": "published",
"date": "2024-06-13",
"path": "/技术/"
}响应: 201 + 完整文章对象(含自动生成的 id)。
说明:
title必填,其余可选status默认draftdate默认当天path默认/id格式:art_{时间戳}{4位随机数}
更新博客配置(需要认证)。
请求体: 任意配置字段的 JSON 对象,会与现有配置合并。
{"title": "新标题", "nickname": "新昵称"}响应:
{"ok": true, "config": { ... }}管理文件夹(需要认证)。
请求体:
| action | 说明 | 额外参数 |
|---|---|---|
create |
创建文件夹 | path: 文件夹路径 |
delete |
删除文件夹 | path: 文件夹路径 |
set |
覆盖整个文件夹列表 | dirs: 路径数组 |
示例:
{"action": "create", "path": "/技术/Go/"}响应:
{"ok": true, "dirs": ["/技术/", "/技术/Go/"]}上传头像(需要认证)。
请求体:
{
"data": "base64编码的图片数据",
"ext": "png"
}限制: 仅支持 jpg/jpeg/png/gif/webp,文件 ≤ 5MB。
响应:
{"ok": true, "url": "/assets/avatar.png"}上传背景图片/视频(需要认证)。
请求体:
{
"target": "blog",
"data": "base64编码的文件数据",
"ext": "jpg"
}| 参数 | 类型 | 说明 |
|---|---|---|
target |
string | blog(博客背景)或 admin(管理后台背景) |
data |
string | base64 编码的文件内容 |
ext |
string | 文件扩展名(jpg/png/webp/mp4/webm/mov) |
验证规则:
- 图片:分辨率 ≥ 800×600,文件 ≤ 5MB
- 视频:分辨率 ≥ 640×360,文件 ≤ 200MB
响应:
{"ok": true, "url": "/assets/bg-blog.jpg"}更新文章(需要认证)。
请求体: 要更新的字段(id 字段会被忽略)。
{
"title": "新标题",
"status": "published",
"content": "更新后的内容"
}响应: 更新后的完整文章对象。
说明: 采用合并更新,只传需要修改的字段即可。
删除文章(需要认证)。文章会被移动到 data/trash/ 目录,而非永久删除。
响应:
200 {"ok": true}404 {"error": "not found"}
CORS 预检请求,返回允许的跨域方法和头。
响应头:
Access-Control-Allow-Origin: <匹配的来源或 *>
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
CORS 配置:
- 默认允许所有来源(
*) - 通过
CORS_ORIGINS环境变量限制为逗号分隔的白名单 - 例:
CORS_ORIGINS=https://blog.example.com,https://admin.example.com
- 算法:PBKDF2-HMAC-SHA256
- 迭代次数:600,000
- 盐值:32 字节随机 hex
- 密钥长度:32 字节
- 格式:
{salt}:{iterations}:{derived_key_hex} - 自动迁移:旧版 SHA256 格式(
{salt}:{hash})在首次成功登录后自动升级为 PBKDF2
- 同一 IP 每分钟最多 5 次失败尝试
- 超出后返回 HTTP 429
- 成功登录后清除该 IP 的失败记录
- 登录成功后生成 64 位 hex Token
- 24 小时自动过期
- 每次请求清理过期 Token
- 每个账户最多保留 10 个 Token
| 接口 | 需要认证 |
|---|---|
GET /api/articles |
否 |
GET /api/articles/{id} |
否 |
GET /api/config |
否 |
GET /api/dirs |
否 |
GET /api/auth/status |
否 |
GET /api/events |
否 |
GET /api/qr |
否 |
POST /api/auth/setup |
否 |
POST /api/auth/login |
否 |
POST /api/auth/change-password |
是 |
POST /api/articles |
是 |
POST /api/config |
是 |
POST /api/dirs |
是 |
POST /api/upload/avatar |
是 |
POST /api/upload/bg |
是 |
PUT /api/articles/{id} |
是 |
DELETE /api/articles/{id} |
是 |
blog/
├── server.py # 入口:启动 ThreadingHTTPServer
├── app/
│ ├── __init__.py
│ ├── config.py # 常量、路径、JSON 读写、CORS 配置
│ ├── auth.py # 密码哈希、Token 管理、登录限流
│ ├── sse.py # 文章版本追踪、SSE 推送
│ └── handler.py # HTTP 请求处理(所有路由)
├── index.html # 博客首页(HTML + CSS + JS 一体化)
├── admin/
│ └── index.html # 管理后台(HTML + CSS + JS 一体化)
├── data/
│ ├── config.json # 博客配置
│ ├── auth.json # 认证数据(首次使用时自动生成)
│ ├── dirs.json # 文件夹列表
│ ├── articles/ # 文章 JSON 文件
│ │ ├── art_001.json
│ │ └── ...
│ └── trash/ # 已删除文章(软删除)
├── assets/
│ ├── GoogleSans.ttf # 中文字体
│ ├── avatar.png # 头像(上传后生成)
│ ├── bg-blog.jpg # 博客背景(上传后生成)
│ └── bg-admin.jpg # 管理后台背景(上传后生成)
├── screenshots/ # README 截图
├── start.sh # 启动脚本
├── blog.service # systemd 服务模板
├── .gitignore
├── LICENSE
└── README.md
每篇文章是一个独立的 JSON 文件:
{
"id": "art_17180000001234",
"title": "文章标题",
"tags": "Python,博客",
"content": "# 标题\n\n正文内容...",
"status": "published",
"date": "2024-06-13",
"path": "/技术/Python/",
"type": "article"
}安全
- CORS 从
*改为可配置白名单(CORS_ORIGINS环境变量) - 头像上传接口加认证,防止被滥用为免费图床
- systemd 模板改为非 root 用户运行(
User=blog) - systemd 加固:
NoNewPrivileges、ProtectSystem=strict、PrivateTmp
架构
server.py拆分为模块化结构:app/config.py、app/auth.py、app/sse.py、app/handler.pyserver.py仅作为入口,负责启动 HTTP 服务- 所有路径使用
BASE_DIR相对计算,不再硬编码
部署
start.sh使用cd "$(dirname "$0")"相对路径- systemd
WorkingDirectory=/opt/blog,不再硬编码/root/blog
编辑器
- 全屏编辑器,覆盖几乎整个屏幕
- 16 个 Markdown 工具栏按钮,光标位置插入
- 左右分栏:左侧编辑,右侧实时预览
- 代码块子编辑器:20+ 语言选择,独立预览
- 自定义滚动条样式
Markdown 渲染
- 完整重写
renderMd()引擎 - 14 种语言语法高亮(VS Code 暗色配色)
- 代码块语言标签(终端风格)
- 下划线语法
++text++ - 修复引用块转义、分割线空格格式
管理后台
- 多选批量操作(删除、移动、复制、下载)
- 设置面板重构为 6 个分类卡片
- 联系方式模板(GitHub/Email/QQ/微信/微博)
- 系统字体替代 GoogleSans
安全
- PBKDF2 密码存储(600k 迭代)
- 登录限流(5 次/分钟/IP)
- Token 24 小时过期
本项目采用 CC BY-NC-SA 4.0 许可证。
你可以:
- 共享 — 在任何媒介以任何形式复制、发行本作品
- 演绎 — 修改、转换或以本作品为基础进行创作
惟须遵守下列条件:
- 署名 — 必须给出适当的署名
- 非商业性使用 — 不得将本作品用于商业目的
- 相同方式共享 — 修改后的作品必须以相同的许可证发布
禁止:
- 出售本软件或其修改版本
- 作为付费服务提供
- 整合到商业产品中进行分发
- JetBrains Mono — 等宽英文字体
- GoogleSans — 中文字体
- 一言 API — 随机句子接口
如果这个项目对你有帮助,欢迎 Star!

