前言

这个博客的诞生源于一个需求:有一个地方能存放平时的学习笔记,免费、国内能访问、不用折腾服务器。整个过程从 Hexo 初始化开始,到 Obsidian 笔记自动同步,再到评论系统搭建和优化,踩了不少坑,记录如下。

技术选型

组件 选择 原因
博客框架 Hexo 静态站点,免服务器,GitHub 友好
主题 Fluid 美观简洁,功能齐全,中文本地化好
托管 Cloudflare Pages 国内访问快,免费额度足,支持 Pages Functions
评论 Twikoo + Vercel + MongoDB Atlas 免费,中国人开发,支持点赞,国内生态
笔记源 Obsidian 本地 Markdown 编辑器,双向链接
代码仓库 GitHub 免费,CI/CD 集成

一、Hexo + Fluid 初始化

1. 安装 Hexo

1
2
3
4
npm install -g hexo-cli
hexo init MingZaiBlog
cd MingZaiBlog
npm install

2. 安装 Fluid 主题

1
npm install --save hexo-theme-fluid

_config.yml 中启用:

1
theme: fluid

Fluid 的配置采用双层结构:_config.yml 是 Hexo 站点配置,_config.fluid.yml 是主题配置覆盖。主题自带的默认配置在 node_modules/hexo-theme-fluid/_config.yml 中,用户只需在 _config.fluid.yml 中写出需要覆盖的项即可。

3. 导航栏配置

1
2
3
4
5
6
7
8
navbar:
blog_title: "MingZai Blog"
menu:
- { key: "home", link: "/", icon: "iconfont icon-home-fill" }
- { key: "archive", link: "/archives/", icon: "iconfont icon-archive-fill" }
- { key: "category", link: "/categories/", icon: "iconfont icon-category-fill" }
- { key: "tag", link: "/tags/", icon: "iconfont icon-tags-fill" }
- { key: "about", link: "/about/", icon: "iconfont icon-user-fill" }

二、Cloudflare Pages 部署

1. 连接 GitHub 仓库

Cloudflare Pages 支持从 GitHub 仓库自动部署。配置如下:

  • Framework: None(Hexo 不是预设框架之一)
  • Build command: npx hexo generate
  • Output directory: public

2. 部署脚本

为了快速部署,写了两个脚本:

deploy.sh — 只重新生成并推送(不导入新笔记):

1
2
3
4
npx hexo generate
git add -A
git commit -m "$1"
git push

sync.sh — 从 Obsidian 导入笔记后部署:

1
2
3
4
5
python3 import_notes.py
npx hexo generate
git add -A
git commit -m "$1"
git push

三、Obsidian 笔记自动导入

背景

Obsidian 笔记存放在 Windows 分区(NTFS 挂载到 Linux),包含大量中文文件名和图片引用。Hexo 的 source/_posts 目录是博客的源文件目录,每次导入时清空重建。

导入脚本 import_notes.py

脚本的核心逻辑:

  1. 遍历 Obsidian 仓库中的所有 Markdown 文件
  2. 解析文件中的图片引用(支持 ![[image.png]]![]() 两种格式)
  3. 复制图片到 source/images/,用 MD5 hash 重命名
  4. 生成 Hexo 兼容的 Markdown 文件到 source/_posts/

关键技术点

图片引用解析

Obsidian 的 ![[图片.png]] 语法是双向链接格式,Hexo 不原生支持。脚本通过正则匹配将其转换为 <img> 标签:

1
2
3
4
# 匹配 ![[xxx.png]]
img_ref = re.findall(r'!\[\[([^\]]+\.(png|jpg|jpeg|gif|bmp|webp|svg))\]\]', content)
# 匹配 ![alt](xxx.png)
img_ref2 = re.findall(r'!\[.*?\]\(([^)]+\.(png|jpg|jpeg|gif|bmp|webp|svg))\)', content)

中文文件名问题

Obsidian 中的笔记和图片经常包含中文,Hexo 在处理中文文件名时会出现各种问题。解决方案是:将所有图片用 MD5 hash 重命名

1
2
3
hash_name = hashlib.md5(img_path.encode()).hexdigest()[:10]
ext = os.path.splitext(img_ref)[1].lower()
new_name = f"img_{hash_name}{ext}"

这样 实验数据_结果.png 变成 img_a1b2c3d4e5.png,彻底规避了中文字符带来的渲染问题。

黑名单机制

某些笔记不想发布(如个人作业互评),通过 SKIP_FILES 列表过滤:

1
SKIP_FILES = {"未命名.md", "专业写作基础 小组互评.md", "专业写作基础作业.md"}

四、Twikoo 评论系统搭建

Twikoo 是一个国人开发的评论系统,免费、无广告、支持 Markdown 和表情。它由三部分组成:

1
博客页面 → Twikoo 前端 JS → Vercel 云函数 → MongoDB Atlas

1. MongoDB Atlas 数据库

在 MongoDB Atlas 创建免费集群(M0 沙盒),创建一个数据库用户(用户名 twikoo),获取连接字符串:

1
mongodb+srv://twikoo:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority

注意:连接字符串中只需替换 <password> 部分,用户名保持 twikoo 不变。

2. Vercel 云函数

从 GitHub fork twikoojs/twikoo 仓库,在 Vercel 中导入。关键配置:

  • ROOT 配置:需要添加 vercel.jsonapi/ 目录,让 Vercel 识别为 Serverless Function 而非静态站点
  • 环境变量MONGODB_URI 设为 Atlas 连接字符串

遇到的问题:Vercel 默认会执行 yarn run build(Webpack 构建前端),但我们需要的是服务端函数,不需要构建。解决方案是在 vercel.json 中设置 "buildCommand": "",并在 api/ 目录下创建函数入口。

1
2
3
4
{
"rewrites": [{ "source": "/(.*)", "destination": "api/index" }],
"buildCommand": ""
}

3. Fluid 主题集成

_config.fluid.yml 中启用 Twikoo:

1
2
3
4
5
6
7
post:
comments:
enable: true
type: twikoo

twikoo:
envId: https://你的域名.vercel.app

4. 国内访问优化

问题vercel.app 域名在国内移动数据网络下被屏蔽,导致手机用户无法评论。

方案一:Cloudflare Worker 反向代理

创建一个 Cloudflare Worker,将请求转发到 Vercel:

1
2
3
4
5
6
7
8
9
10
11
export default {
async fetch(request) {
const url = new URL(request.url);
const target = 'https://project-5luql.vercel.app' + url.pathname + url.search;
return fetch(new Request(target, {
method: request.method,
headers: request.headers,
body: request.body
}));
}
}

.workers.dev 域名在某些网络下同样被屏蔽。

方案二(最终方案):Cloudflare Pages Function 代理

由于博客本身托管在 Cloudflare Pages 上(国内可访问),直接在博客项目中创建一个 Pages Function,将 /twikoo 路径的请求代理到 Vercel:

1
2
3
4
5
6
7
8
9
10
// functions/twikoo/[[path]].js
export async function onRequest(context) {
const url = new URL(context.request.url);
const target = 'https://project-5luql.vercel.app' + url.pathname.replace('/twikoo', '') + url.search;
return fetch(new Request(target, {
method: context.request.method,
headers: context.request.headers,
body: context.request.method === 'GET' ? null : await context.request.text(),
}));
}

然后将 envId 改为:

1
2
twikoo:
envId: https://你的博客域名.pages.dev/twikoo

这样评论 API 请求走的是博客同域名,彻底解决了网络屏蔽问题。

工作原理图解

1
2
3
4
5
6
7
用户评论 → mingzaiblog.pages.dev/twikoo (POST)

Cloudflare Pages Function (functions/twikoo/[[path]].js)

project-5luql.vercel.app (Vercel Serverless Function)

MongoDB Atlas (数据存储)

五、留言板功能

Fluid 主题的 page.ejs 布局通过 inject_point('pageComments') 渲染评论。但默认情况下页面评论需要通过前端注入方式渲染,简单地在 frontmatter 设置 comments: true 可能不生效。

解决方案:在留言板页面直接嵌入 Twikoo 脚本,绕过主题的评论注入系统:

1
2
3
4
5
6
7
8
---
layout: page
title: 留言板
comments: true
comment: twikoo
---

欢迎在这里留言!

另一种方案是使用 about 布局,它自带评论渲染代码,但会显示头像和自我介绍等 about 页面特有的内容,不适合留言板。

六、主页点赞按钮

由于不需要后端存储跨用户点赞数据,使用 localStorage 实现了一个简单的点赞按钮:

  • 点赞状态存储在浏览器的 localStorage
  • 每个浏览器只能点赞一次
  • 点赞计数存储在本地
1
2
3
4
5
6
7
8
9
10
11
12
var key = 'blog_liked_home';
var liked = localStorage.getItem(key);
var count = parseInt(localStorage.getItem('blog_like_count') || '0');

container.onclick = function() {
if (!liked) {
liked = '1';
count++;
localStorage.setItem(key, liked);
localStorage.setItem('blog_like_count', count.toString());
}
};

七、遇到的问题汇总

1. 中文图片文件名

问题:Obsidian 中的图片多为中文名,Hexo 渲染时报错或生成错误路径。

解决:导入脚本自动用 MD5 hash 重命名所有图片。

2. Vercel 找不到 public 目录

问题:Vercel 把 Twikoo 仓库当成静态站点部署,要求有 public 输出目录。

解决:添加 vercel.json 配置路由重写到 api/index,并禁用 build 命令。

3. 手机无法加载评论

问题vercel.appworkers.dev 域名在国内移动数据下被屏蔽。

解决:利用 Cloudflare Pages Function 做反向代理,评论走博客同域名。

4. 留言板评论不显示

问题:Fluid 主题的 pageComments inject 条件判断未通过。

解决:直接嵌入 Twikoo 脚本并加上 comment: twikoo 前页元数据。

5. Twikoo CDN 版本不存在

问题lib.baomitu.com 上没有 Twikoo 1.7.9 版本,导致 404 错误。

解决:回退到 1.6.8 版本,前后端版本可跨小版本兼容。

6. 个人隐私信息暴露

问题:作业笔记中包含姓名、学号等个人信息。

解决:在导入脚本中通过正则替换对特定文件做脱敏处理。

八、项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MingZaiBlog/
├── _config.yml # Hexo 站点配置
├── _config.fluid.yml # Fluid 主题配置
├── source/
│ ├── _posts/ # 博客文章(由导入脚本生成)
│ ├── images/ # 图片资源(hash 命名)
│ ├── about/ # 关于页面
│ ├── guestbook/ # 留言板
│ ├── img/ # 主题图片(头像、横幅)
│ └── js/ # 自定义 JS
├── functions/ # Cloudflare Pages Functions
│ └── twikoo/
│ └── [[path]].js # Twikoo 代理
├── import_notes.py # Obsidian 导入脚本
├── sync.sh # 同步 + 部署
└── deploy.sh # 仅部署

九、总结

这个博客从零开始搭建,涉及了静态博客、对象存储、无服务器函数、反向代理等多个技术领域。整个过程有几点体会:

  1. 静态博客 + Serverless 是个人博客的最优解:零成本、免运维、高性能
  2. 国内外的云服务组合需要特别注意网络互通问题,反向代理是常用的解决方案
  3. Obsidian + Hexo 的工作流可以很顺畅,关键是要处理好图片引用和文件名兼容性
  4. Twikoo 作为国内开发的评论系统,虽然部署步骤稍多,但功能完善、免费、自主可控

整个博客的所有代码开源在 GitHub,欢迎参考。