← 返回博客

用 Node.js 手搓一个静态博客,比装 Hexo 更省心

2026-09-01·Node.js / 折腾·约 2 分钟

给自己搭个人网站,第一反应通常是 Hexo 或 Hugo。我最后选了自己写 —— 不是为了炫技,而是算了下账,发现手搓更划算。

账是这么算的

框架的价值在于「别人已经解决了你的问题」。但个人博客的问题实在太简单了:

而框架带来的成本是实打实的:主题定制要学它的模板语法,改个样式要翻文档,插件版本冲突要查 issue,一年不动之后升级可能直接跑不起来。

我写的这个生成器一共 200 多行,所有逻辑我都清楚。要改什么直接改,不用先去理解别人的抽象。

整个结构

site/
├── build.js              # 生成器本体
├── content/
│   ├── site.json         # 个人信息、项目列表
│   └── posts/*.md        # 文章
├── static/style.css      # 样式
└── dist/                 # 产物,丢给 Nginx

写文章就是往 posts/ 里丢 .md,跑一下 node build.jsdist/ 里就是完整站点。

两个值得说的实现细节

Front matter 解析不用引 yaml 库。 个人博客的 front matter 就那么几种字段,一个正则加逐行 split 够了:

const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
// 然后逐行匹配 key: value,数组用 [a, b] 的形式

少一个依赖,少一份升级负担。

中英文混排的阅读时长要分开算。 直接按字符数估会很不准,中文阅读速度约 400 字/分钟,英文约 220 词/分钟:

const cn = (md.match(/[\u4e00-\u9fa5]/g) || []).length;
const en = (md.replace(/[\u4e00-\u9fa5]/g, ' ').match(/[A-Za-z0-9]+/g) || []).length;
const minutes = Math.max(1, Math.round(cn / 400 + en / 220));

部署也简单

产物是纯静态文件,Nginx 配一个 root 就完事。更新用 rsync

rsync -avz --delete dist/ user@server:/var/www/site/

--delete 保证服务器上不会残留已删除的旧文章。整个部署过程两秒,比很多框架的构建都快。

什么情况下不该这么做

说句公道话,如果你需要全文搜索、评论系统、多语言、几百篇文章的增量构建 —— 老老实实用成熟框架。手搓的优势只在「需求足够简单」这个前提下成立。

但对于一个个人主页 + 偶尔更新的博客,这个前提是成立的。