OctoBlog 以 GitHub 为蓝本的个人博客系统
全部文章

用 300 行 Node.js 写一个静态博客生成器

核心只有一个数据流

静态博客生成器的本质非常朴素:

构建流程示意

  1. 读 content/posts/*.md,用 front matter 解析出元数据;
  2. 把正文渲染成 HTML,同时抽出目录和摘要;
  3. 套进统一模板,写出一个个 index.html;
  4. 顺带生成 feed.xml、sitemap.xml 和搜索索引。

第一步:解析 front matter

不要为了几十行 YAML 去装一个依赖。博客需要的子集很小:字符串、数字、布尔、数组。

JS
export function parseFrontMatter(raw) {
  const text = raw.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
  const match = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/.exec(text);
  if (!match) return { data: {}, body: text.trim() };
  return { data: parseYamlLite(match[1]), body: text.slice(match[0].length) };
}

支持这两种写法就够了:

YAML
tags: [工程, Node.js]
tags:
  - 工程
  - Node.js

第二步:渲染 Markdown

marked 的 ESM 单文件版本只有 40KB,直接放进仓库,离线也能构建:

JS
import { marked } from '../../vendor/marked.esm.mjs';

const html = marked.parse(md);

难点从来不是 Markdown 本身,而是周边:

需求 做法
标题要有 id 和锚点 渲染后用正则补 <a class="heading-anchor">
代码要高亮 自己写个 200 行的分词器,避免引入 highlight.js
外链新窗口打开 给 <a href="http..."> 加 target="_blank"
图片懒加载 给 <img> 补 loading="lazy"

第三步:写一个能用的本地服务器

http 模块 40 行就能得到一个带目录索引的静态服务器:

JS
const server = http.createServer((req, res) => {
  const urlPath = decodeURIComponent((req.url || '/').split('?')[0]);
  let file = path.join(base, urlPath);
  if (!path.extname(file) || urlPath.endsWith('/')) file = path.join(file, 'index.html');
  if (!fs.existsSync(file)) return show404(res);
  res.writeHead(200, { 'content-type': MIME[path.extname(file)] || 'application/octet-stream' });
  fs.createReadStream(file).pipe(res);
});

再配一层 fs.watch 做热重载,写作体验就不输任何框架了。

为什么不直接用现成的框架

不是因为框架不好,而是因为这个博客的复杂度配不上它的依赖树:

  • 没有客户端路由,每个页面都是真实的 HTML;
  • 没有组件状态,交互仅限主题切换和搜索;
  • 没有构建性能问题,50 篇文章的构建时间是 0.3 秒。

当你不需要一个工具时,不用它才是最省心的决定。

收尾

顺便给项目加了一个 --watch:

BASH
node scripts/build.mjs --watch

保存 Markdown 的瞬间,浏览器里就是最新的内容。写博客这件事,反馈越快,越容易坚持下去。