栈隐札记的技术栈很薄:Markdown 写文章,一个 Node.js 脚本做构建,模板字符串拼 HTML,Cloudflare Pages 托管静态文件。没有框架,没有 bundler,没有 CMS。

选这套方案的理由不是"极简主义"——技术选择不需要道德化。真实原因是这条链路里每个环节的行为我都能预测,出问题能直接定位到具体文件。

从 Markdown 到 HTML

每篇文章就是一个 .md 文件,frontmatter 写元信息,正文就是 Markdown。

构建脚本的核心逻辑不到三百行。用 fast-glob 找到所有文章和页面,gray-matter 拆出 frontmatter 和正文,markdown-it 渲染成 HTML。标题锚点用 markdown-it-anchor,脚注用 markdown-it-footnote,表格外面包一层 table-scroll 容器做移动端横向滚动——这些都是构建时完成的,浏览器收到的就是最终 HTML。

content/posts/*.md  →  loadPost()  →  { title, date, slug, html, tags, ... }
content/pages/*.md  →  loadPage()  →  { title, slug, html }

文章 URL 从文件名自动生成:2026-05-21-slug.md 的前缀只用于本地排序,发布路径是 /posts/slug/draft: true 的文章默认跳过构建,除非显式设置 INCLUDE_DRAFTS=true——这样半成品可以留在仓库里,不用担心被意外发布。

首页内容也全由构建脚本生成:featured 文章取前 6 篇放在精选区,其余按年份归入归档列表。不需要手写 HTML,不需要 CMS 的拖拽排序。

模板为什么是字符串

模板系统没有用 JSX、Pug、Handlebars。就是五个纯函数,返回模板字面量拼出来的 HTML 字符串:

  • layout() — HTML shell:<head>、nav、footer
  • homePage() — 首页:intro 区 + featured grid + 年份归档
  • postPage() — 文章页:标题、日期、标签、正文
  • simplePage() — 通用页,用于 About 等
  • escapeHtml() / attrs() — HTML 转义辅助

函数组合的方式很直接:layout({ body: postPage({ post }) })。没有模板继承、没有 partial、没有 slot——每个函数独立产生一段完整的 HTML,调用关系只有一层。

代价很明显:改了 nav 结构需要手动检查所有用到它的地方。好处是没有任何"模板引擎的魔法"——打开文件就能看到完整的数据流,一个 escapeHtml() 调用能追到定义。对一个一年改不了几次模板的个人站来说,这点可预测性比模板引擎的便利更重要。

为什么是纯 CSS

样式是一个单文件 styles.css,约 700 行,CSS 自定义属性做主题变量,@media 做响应式,没有构建步骤。

不用 Tailwind 的原因不是反对 Tailwind。Tailwind 在团队协作和组件化项目里有明确优势——约束 class 命名、缩小样式作用域、降低随意写 CSS 的风险。但这些优势在这个站点上不存在:我一个人维护,组件不超过五个,没有样式冲突的问题。引入 Tailwind 只会多一个构建步骤和一个需要跟踪的配置文件。

反过来,纯 CSS 让我保留了直接修改样式的能力。调整一个颜色不需要过 utility class 层,不需要重新编译,不需要理解框架生成的 CSS——打开 styles.css,搜索,改值,完成。

字体栈、排版比例、颜色变量、滚动容器、表格处理——这些都在同一个文件里,按视觉层级排列。维护成本就是一个文件的 grep。

部署

Cloudflare Pages 监听仓库推送,执行 npm run build,输出 dist/。构建产物是完整的静态文件——HTML、CSS、RSS、sitemap、robots、favicon。

部署层面没有任何服务端运行时。Cloudflare Pages 不需要理解 Markdown、frontmatter 或模板,它只是托管了一组静态文件。这意味着如果以后想换托管平台,把 dist/ 搬到任何支持静态托管的服务上都能用。搬家成本等于一次 npm run build

RSS 用 feed 库生成,取最近 20 篇文章,每次构建自动更新。sitemap 和 robots 也是构建时生成,手工拼 XML 字符串——因为内容就那几行,不值得为这点 XML 引入额外的依赖。

这套方案的边界

这套方案不是"通用的轻量博客方案"——它只是在这个站点的规模下成立。

没有后台,写文章就是编辑 Markdown 文件然后 git push。没有站内搜索,没有评论系统,没有图片自动压缩,没有 i18n,没有主题切换。每加一个功能都需要手写代码,不存在装个插件就搞定的事。

这些限制对我目前是可接受的。博客的第一目标是让我能持续写,第二目标是读者能正常读。只要 Markdown 渲染不出错、页面加载够快、RSS 能正常更新,其余功能在这个阶段都是多余决策。

结尾

如果说这套方案有一个我特别看重的点,不是"轻量",也不是"简单"——是可预测。

构建脚本 300 行,模板 200 行,样式一个文件。改了什么东西,产出的影响范围不需要猜。出 bug 不需要排查框架内部行为,不需要怀疑是某个插件的副作用。维护一个个人博客,最消耗意志力的不是写代码,是遇到问题时不知道从哪里开始排查。

可预测性在技术讨论里不太被提起,可能是因为它不够性感。但对于一个你打算长期维护的东西,它可能是最重要的指标。