Skip to content

xzy4260/blog

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

~/blog — 终端风格个人博客系统

零依赖、纯本地部署的终端风格博客。Python 后端 + 纯前端,无需 Node.js、数据库或任何第三方服务。

博客主页

管理后台


目录


特性一览

分类 功能
文章管理 终端命令式导航 (cd/ls/pwd)、路径分类、全文搜索、批量操作
编辑器 全屏 Markdown 编辑器、16 按钮工具栏、左右分屏实时预览、代码块子编辑器
渲染 完整 Markdown 支持、14 种语言语法高亮、终端风格排版、语言标签
同步 SSE 服务端推送,所有客户端实时同步文章变更
外观 终端/玻璃拟态风格、自定义背景(图片/视频)、毛玻璃效果
安全 PBKDF2 密码存储(60 万次迭代)、登录限流、Token 24h 过期、CORS 白名单
部署 纯 Python 标准库、systemd 服务模板、零外部网络依赖、非 root 运行

快速开始

1. 克隆项目

git clone https://github.com/xzy4260/blog.git
cd blog

2. 安装依赖

仅需 Python 3 标准库 + qrcode(用于生成二维码):

pip3 install qrcode[pil]

3. 启动服务

python3 server.py

默认监听 0.0.0.0:3000,访问:

  • 博客首页:http://localhost:3000
  • 管理后台:http://localhost:3000/admin

4. 首次使用

  1. 打开管理后台,设置管理员密码
  2. 在「系统设置」中配置博客标题、昵称、头像等
  3. 点击「新建文章」开始写作

自定义端口与部署

方式一:环境变量

PORT=8080 HOST=127.0.0.1 python3 server.py
变量 默认值 说明
PORT 3000 监听端口
HOST 0.0.0.0 监听地址
CORS_ORIGINS 空(允许所有) 逗号分隔的允许来源域名

方式二:修改 start.sh

编辑 start.sh,修改端口号:

#!/bin/bash
cd "$(dirname "$0")"
export PORT=8080  # 改为你想要的端口
exec python3 server.py

赋予执行权限后运行:

chmod +x start.sh
./start.sh

方式三:systemd 服务(推荐生产环境)

  1. 创建专用用户并部署:
sudo useradd -r -s /usr/sbin/nologin blog
sudo cp -r . /opt/blog
sudo chown -R blog:blog /opt/blog
  1. 编辑 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
  1. 安装并启动服务:
sudo cp blog.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now blog.service
  1. 查看状态:
sudo systemctl status blog.service
sudo journalctl -u blog.service -f

方式四:反向代理(Nginx)

如果需要 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 编辑器

管理后台内置全屏 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 图片 ![alt](url)
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 纯黑背景

博客和管理后台的背景可以分别设置。


API 接口文档

所有接口返回 JSON,POST/PUT/DELETE 接口需要认证(除登录/设置密码外)。

认证方式

在请求头中携带 Token:

Authorization: Bearer <token>

Token 通过登录接口获取,24 小时后自动过期。


GET 接口

GET /api/articles

获取所有文章列表。

响应:

{
  "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(默认 /

GET /api/articles/{id}

获取单篇文章详情。

响应: 文章 JSON 对象,字段同上。

错误: 404 {"error": "not found"}


GET /api/config

获取博客配置。

响应:

{
  "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": ""
}

GET /api/dirs

获取文件夹列表。

响应:

{
  "dirs": ["/技术/", "/技术/Python/", "/生活/"]
}

GET /api/auth/status

检查是否已设置密码(无需认证)。

响应:

{"hasPassword": true}

GET /api/events

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') {
    // 重新加载文章列表
  }
};

GET /api/qr?url={url}

生成二维码图片。

参数:

参数 类型 必填 说明
url string 要编码的 URL

响应: image/png 图片,灰色前景 + 深色背景,缓存 24 小时。

错误: 400 {"error": "missing url"}


POST 接口

POST /api/auth/setup

首次设置管理员密码(无需认证,仅可调用一次)。

请求体:

{"password": "你的密码"}

响应:

  • 200 {"ok": true}
  • 400 {"error": "密码已设置"}
  • 400 {"error": "密码至少4个字符"}

POST /api/auth/login

登录获取 Token(无需认证)。

请求体:

{"password": "你的密码"}

响应:

{"ok": true, "token": "64位hex字符串"}

限流: 同一 IP 每分钟最多 5 次失败尝试,超出返回 429 {"error": "尝试次数过多,请稍后再试"}

自动迁移: 如果旧密码使用 SHA256 存储,登录成功后自动升级为 PBKDF2。


POST /api/auth/change-password

修改密码(需要认证)。

请求体:

{"oldPassword": "旧密码", "newPassword": "新密码"}

响应:

  • 200 {"ok": true}
  • 400 {"error": "新密码至少4个字符"}
  • 401 {"error": "原密码错误"}

POST /api/articles

创建文章(需要认证)。

请求体:

{
  "title": "文章标题",
  "tags": "标签1,标签2",
  "content": "Markdown 内容",
  "status": "published",
  "date": "2024-06-13",
  "path": "/技术/"
}

响应: 201 + 完整文章对象(含自动生成的 id)。

说明:

  • title 必填,其余可选
  • status 默认 draft
  • date 默认当天
  • path 默认 /
  • id 格式:art_{时间戳}{4位随机数}

POST /api/config

更新博客配置(需要认证)。

请求体: 任意配置字段的 JSON 对象,会与现有配置合并。

{"title": "新标题", "nickname": "新昵称"}

响应:

{"ok": true, "config": { ... }}

POST /api/dirs

管理文件夹(需要认证)。

请求体:

action 说明 额外参数
create 创建文件夹 path: 文件夹路径
delete 删除文件夹 path: 文件夹路径
set 覆盖整个文件夹列表 dirs: 路径数组

示例:

{"action": "create", "path": "/技术/Go/"}

响应:

{"ok": true, "dirs": ["/技术/", "/技术/Go/"]}

POST /api/upload/avatar

上传头像(需要认证)。

请求体:

{
  "data": "base64编码的图片数据",
  "ext": "png"
}

限制: 仅支持 jpg/jpeg/png/gif/webp,文件 ≤ 5MB。

响应:

{"ok": true, "url": "/assets/avatar.png"}

POST /api/upload/bg

上传背景图片/视频(需要认证)。

请求体:

{
  "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"}

PUT 接口

PUT /api/articles/{id}

更新文章(需要认证)。

请求体: 要更新的字段(id 字段会被忽略)。

{
  "title": "新标题",
  "status": "published",
  "content": "更新后的内容"
}

响应: 更新后的完整文章对象。

说明: 采用合并更新,只传需要修改的字段即可。


DELETE 接口

DELETE /api/articles/{id}

删除文章(需要认证)。文章会被移动到 data/trash/ 目录,而非永久删除。

响应:

  • 200 {"ok": true}
  • 404 {"error": "not found"}

OPTIONS 接口

OPTIONS *

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 的失败记录

Token 管理

  • 登录成功后生成 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"
}

更新日志

v2.1.0 — 安全加固与架构重构

安全

  • CORS 从 * 改为可配置白名单(CORS_ORIGINS 环境变量)
  • 头像上传接口加认证,防止被滥用为免费图床
  • systemd 模板改为非 root 用户运行(User=blog
  • systemd 加固:NoNewPrivilegesProtectSystem=strictPrivateTmp

架构

  • server.py 拆分为模块化结构:app/config.pyapp/auth.pyapp/sse.pyapp/handler.py
  • server.py 仅作为入口,负责启动 HTTP 服务
  • 所有路径使用 BASE_DIR 相对计算,不再硬编码

部署

  • start.sh 使用 cd "$(dirname "$0")" 相对路径
  • systemd WorkingDirectory=/opt/blog,不再硬编码 /root/blog

v2.0.0 — 编辑器重构与视觉升级

编辑器

  • 全屏编辑器,覆盖几乎整个屏幕
  • 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 许可证。

你可以:

  • 共享 — 在任何媒介以任何形式复制、发行本作品
  • 演绎 — 修改、转换或以本作品为基础进行创作

惟须遵守下列条件:

  • 署名 — 必须给出适当的署名
  • 非商业性使用 — 不得将本作品用于商业目的
  • 相同方式共享 — 修改后的作品必须以相同的许可证发布

禁止:

  • 出售本软件或其修改版本
  • 作为付费服务提供
  • 整合到商业产品中进行分发

致谢


如果这个项目对你有帮助,欢迎 Star!

About

~/blog — 终端风格个人博客系统 | Python 后端 + 纯前端,零依赖,SSE 实时同步,Markdown 语法高亮,全屏编辑器

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors