这个博客采用了一条直接的内容路径:文章写在 Markdown 文件里,构建脚本把内容转成 HTML,运行中的 Node.js 服务负责把生成文件发给浏览器。
本文记录的是本站当前实现,对应文件是 scripts/build.mjs、server.mjs 和 scripts/personal-blog.service。它不涉及登录、数据库或在线编辑器;内容更新通过本地文件完成。
一篇文章的输入是什么
构建脚本扫描 content/posts/,只处理文件名由小写字母、数字和连字符组成的 Markdown 文件。文件名去掉扩展名后,成为文章 URL 的最后一段。
例如,本篇的源文件名是 blog-markdown-to-http.md,生成后对应路径是 /posts/blog-markdown-to-http/。
文章头部包含标题、摘要、日期、分类和标签。这个博客的解析器把每行字段值交给 JSON.parse,因此字符串必须加双引号,标签使用 JSON 数组。它是一个明确受限的格式,不能把任意 YAML 配置直接放进来。
内容怎样变成页面
scripts/build.mjs 读取文章后,先检查必填字段,再解析正文,最后按日期排序。首页、分类入口和归档都使用这一份文章集合,RSS 也从相同数据生成。
正文解析支持段落、二至四级标题、列表、引用、代码块、行内代码、粗体和 HTTP/HTTPS 链接。普通文本会先转义,原始 HTML 不会直接作为页面元素执行。
这一选择让代码保持简短,也带来了实际限制:图片、表格和复杂 Markdown 语法暂时不支持。需要这些能力时,应该先明确格式需求,再决定是否更换成熟的 Markdown 解析器。
生成结果集中写入 public/:
public/
index.html
archive/index.html
about/index.html
posts/blog-markdown-to-http/index.html
assets/
rss.xml
404.html
请求到达服务端之后
server.mjs 的根目录固定为 public/。正式服务通过环境变量监听 127.0.0.1:8080,由 Nginx 接收域名的 80/443 端口请求并转发。Node.js 只接受 GET 和 HEAD;其他请求方法返回 405。
对于文章目录,请求 /posts/blog-markdown-to-http 会跳转到带尾斜杠的地址,然后读取该目录下的 index.html。不存在的路径返回自定义 404 页面,同时保持 404 状态码。
这里有一个容易忽略的路径边界:根目录本身也必须是有效目标。本站先把根目录规范化,再分别判断“目标等于根目录”和“目标位于根目录内部”。不能只判断目标是否以一个带尾分隔符的根目录字符串开头,否则首页路径可能被错误拒绝。
请求中的隐藏路径段、反斜杠和空字符也会被拒绝。配置与文章源文件位于公开目录之外,访问 /config.json 不会返回项目配置。
静态文件更新为何无需重启
运行中的服务在收到请求时读取文件,没有把整站 HTML 固定缓存在进程内。因此执行构建后,新的请求就可以读到更新后的文件。内容和样式更新不必重启 Node.js;修改服务端逻辑后则需要重启进程。
目前构建直接覆盖输出文件,并非整站原子发布。对于访问量较大的场景,可以考虑先生成完整的新目录,检查通过后再切换服务目录,减少读到构建中间状态的机会。
systemd 负责进程生命周期
本站的服务单元指定工作目录、Node.js 可执行文件和服务脚本,使用 Restart=on-failure 处理进程异常退出,并在启用服务后随系统启动。
日常维护用到的命令如下。它们需要在部署这份服务的主机上执行:
cd /root/personal-blog
npm run build
systemctl status personal-blog
journalctl -u personal-blog -n 50
域名 melody.is-a.dev 的访问入口由 Nginx 管理。80 端口提供 HTTP,443 端口提供 HTTPS,证书由 Let's Encrypt 签发并通过 Certbot 续期。域名解析和防火墙属于访问入口的另一层配置,不能仅根据内部服务返回 200 就判断公网 HTTPS 已经可用。
这套实现的优点是路径清楚:从源文件到输出文件,再到 HTTP 响应,都可以逐步检查。需要增加能力时,也能先定位到具体环节,再决定怎样扩展。