背景

Hexo 是一个优秀的静态博客框架,但它有一个天然短板:发文必须用电脑。写文章、生成、部署,整个流程离不开终端和编辑器。

对于我来说,很多时候灵感是碎片化的——路上看到一件事、吃饭时想到一个点子,想随手记下来,但掏手机打开 Hexo 后台根本不现实。

于是就有了这个项目:HexoAdmin

核心目标很简单:

在阿里云服务器上跑一个轻量 API 服务,绑定独立域名,通过手机浏览器就能发帖、上传图片,博客前端以微博式时间线展示。


功能概览

后台管理(手机可用)

  • 密码登录,Token 认证,30 天有效期
  • 富文本输入,支持换行、空格、Emoji 表情
  • 拍照/选图上传,支持 JPG、PNG、GIF、WebP,单张最大 20MB
  • 图片九宫格预览,可删除
  • 历史记录管理,支持编辑删除
  • 公开/私密切换:默认公开,点击锁图标切换为私密,私密文章前台不可见

前台展示(微博式时间线)

  • 卡片式布局,头像 + 时间 + 内容 + 图片
  • 图片自适应九宫格(1张单图、2-3张横排、4张2x2、5-9张3x3)
  • 单张图片限制最大 200px,避免过大
  • 超过 300 字自动折叠,点击展开全文
  • 图片点击放大查看(Lightbox)
  • 无限滚动加载,每页 10 条
  • 仅展示公开文章,私密文章对访客不可见

技术架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
手机浏览器 ──HTTPS──→ api.weixing.me(阿里云 ECS + 宝塔面板)

Nginx(反向代理)

Flask + Gunicorn(Python 3.11)

SQLite(本地数据库)

/uploads/(图片存储)

浏览器 ──HTTPS──→ www.weixing.me(GitHub Pages + EdgeOne CDN)

Hexo 静态页面(timeline/index.html)

fetch → api.weixing.me/posts(跨域请求)

渲染卡片展示

技术选型理由

组件 选择 理由
后端框架 Flask 轻量、简单,API 就几个接口
WSGI 服务器 Gunicorn 生产级,多进程,宝塔 Python 项目管理器原生支持
数据库 SQLite 零配置,单文件,博客数据量小完全够用
反向代理 Nginx 宝塔自带,SSL + 反代一站式搞定
前端 原生 JS + Pug 不引入框架,Hexo 主题直接集成

核心代码

1. 后端 API(app.py)

核心接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 健康检查
GET /health → {"status": "ok"}

# 公开:获取文章列表(仅公开文章,分页)
GET /posts?page=1&size=10 → {"posts": [...], "has_more": true, "total": 42}

# 公开:管理员登录
POST /admin/login → {"token": "xxxxx"}

# 需认证:上传图片
POST /admin/upload → {"url": "https://api.weixing.me/uploads/xxx.jpg"}

# 需认证:发布文章
POST /admin/posts → {"id": 1, "created_at": "..."}

# 需认证:编辑文章
PUT /admin/posts/<id> → {"ok": true}

# 需认证:删除文章
DELETE /admin/posts/<id> → {"ok": true}

# 需认证:切换公开/私密
POST /admin/posts/<id>/toggle → {"is_public": false}

# 需认证:获取所有文章(含私密,后台用)
GET /admin/posts → {"posts": [...]}

数据库 posts 表包含 is_public 字段(1=公开,0=私密),公开接口 GET /posts 只查询 is_public = 1 的记录:

1
2
3
4
rows = db.execute(
"SELECT ... FROM posts WHERE is_public = 1 ORDER BY id DESC LIMIT ? OFFSET ?",
(size, offset),
).fetchall()

数据库初始化用了 before_request 钩子,确保 Gunicorn 多进程下也能正确建表:

1
2
3
4
5
@app.before_request
def _ensure_db():
"""首次请求时确保数据库表已创建"""
init_db()
os.makedirs(UPLOAD_DIR, exist_ok=True)

三张表:posts(文章)、tokens(认证令牌)、images(图片关联)。

2. 前台时间线(timeline.pug)

Hexo 的 Butterfly 主题使用 Pug 模板引擎。在 themes/butterfly/layout/ 下新建 timeline.pug,继承 layout.pug

1
2
3
4
5
6
7
8
9
10
11
extends layout

block content
#timeline-page
#article-container
#timeline-container
// JS 动态渲染卡片

block bottom_js
script.
// fetch API + 渲染逻辑 + 滚动加载

关键设计:

  • 滚动加载:监听 window.scroll,距底部 800px 时触发下一页
  • 图片九宫格:根据图片数量动态计算 grid 列数
  • 文字折叠:超过 300 字截断,显示”展开全文”按钮
  • Lightbox:点击图片弹出全屏遮罩查看大图

3. 后台管理页(admin.html)

纯 HTML + CSS + JS 单文件,不依赖任何框架:

  • 密码输入框 + 登录按钮
  • 文本域(支持 Emoji 表情面板)
  • 文件选择器(accept="image/*",支持多选)
  • 图片预览 + 删除
  • 历史记录列表,每条记录支持三个操作:
    • 🔒 锁按钮:点击切换公开/私密状态,私密文章前台不可见
    • 编辑按钮:将内容回填到输入框,修改后保存
    • 删除按钮:确认后删除文章及关联图片
  • 编辑模式下顶部显示黄色提示条,发布按钮变为”保存”
  • 私密文章显示”私密”标签 + 锁图标

服务器配置

宝塔面板部署

  1. 安装软件:Nginx + Python 项目管理器 + Python 环境管理器
  2. 安装 Python 3.11:Python 项目管理器 → 版本管理
  3. 创建网站api.weixing.me,纯静态,申请 Let’s Encrypt SSL,强制 HTTPS
  4. 添加 Python 项目
    • 入口文件:app:app
    • 启动方式:Gunicorn
    • 端口:5000
    • 环境变量:TIMELINE_ADMIN_PASSWORDTIMELINE_SECRET_KEY
  5. Nginx 反向代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
client_max_body_size 60m;

location /uploads/ {
alias /www/wwwroot/timeline-api/uploads/;
expires 30d;
add_header Cache-Control "public";
access_log off;
}

location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

关键配置项

配置 说明
MAX_IMAGE_SIZE 20MB 单张图片大小限制
ALLOWED_EXTENSIONS jpg/jpeg/png/gif/webp 允许的图片格式
POSTS_PER_PAGE 10 每页文章数
client_max_body_size 60m Nginx 上传限制
CORS origins www.weixing.me + weixing.me 跨域允许域名

数据备份

宝塔计划任务,每天凌晨 3 点自动备份 SQLite 数据库:

1
2
3
mkdir -p /www/backup/timeline
cp /www/wwwroot/timeline-api/data.db /www/backup/timeline/data-$(date +%Y%m%d).db
find /www/backup/timeline -name "data-*.db" -mtime +30 -delete

踩过的坑

1. CORS 反引号问题

在 shell 里用 sed 修改 Python 代码时,反引号被当作命令替换符号,导致 CORS origin 变成了 `https://www.weixing.me`(带反引号),浏览器不认。最终用 cat > /tmp/fix.py << 'EOF' 的 heredoc 方式解决。

2. Gunicorn 多进程下数据库表不存在

init_db() 放在模块顶层执行时,Gunicorn 多进程启动可能导致并发写入 SQLite,表创建不完整。改用 @app.before_request 钩子,在首次请求时确保表已创建。

3. Nginx 全局上传限制

站点配置里设了 client_max_body_size 60m,但 Nginx 主配置文件 nginx.conf 里还有全局的 50m 限制,大文件上传仍然被拦截。需要同时修改两处。

4. CORS 域名匹配

博客同时有 www.weixing.meweixing.me 两个域名,CORS 必须同时允许两者,否则其中一个会被拦截。

5. IntersectionObserver 不触发

visibility: hidden 隐藏加载指示器后,IntersectionObserver 不再触发滚动加载。改用 window.scroll 事件监听,距底部 800px 时触发下一页,更可靠。


总结

整个项目从零到上线,核心代码不到 500 行(后端 ~200 行 + 前台 ~150 行 + 后台 ~150 行),但实现了一个完整的内容管理系统:

  • 手机发帖:随时随地记录灵感
  • 微博式展示:轻量、直观、适合碎片化内容
  • 零维护成本:SQLite 单文件数据库 + 宝塔自动管理进程
  • 静态博客动态化:Hexo 负责主站内容,API 负责动态时间线,各司其职

对于有类似需求的朋友,这套方案可以直接复用。代码已开源在 HexoAdmin 目录下,欢迎参考。

timeline微博样式