符合 DRY 和 DIP 的博客渲染器 <:_>
此文旨在测试博客的 md 渲染能力和内容页功能,也顺便介绍一下我的小博客。
本站点由 vike1 驱动,以 SSG2 模式在构建阶段完成渲染并部署在 GitHub Pages 上。
背景
博客最终还是以内容为中心的,因此我觉得码字体验是高于一切的。我曾尝试过 WordPress、Halo、VitePress、Ghost、Mix Space 等各种形态的博客应用。但这些通用产品都有令我不满的地方,例如:
- VitePress 虽然是 SSG,但是毕竟还是为产品文档一类需求设计的,写文章还要考虑 Sidebar 位置,不能做到在一个
.md中就完成全部工作。另外文档标题、正文标题、Sidebar Item标题又都是分开定义的,很烦人 - Halo、WordPress、Ghost、Mix Space 等首先都是依赖 Web Server 的全栈应用。其次它也不是专门的博客应用,还兼顾知识库、官网等用例,且为了追赶潮流而大都有各种 AI 功能,对我就显得臃肿。另外写文章还得用 Web 编辑器,不够顺手
因此我就做了这么一个简单的小应用来满足自己的需求。用它,我可以:
- 直接编辑 markdown 文件,不用为了写个博客去花时间适应那些所见即所得编辑器,且不用引入任何 MCP 就可以适配任何 Agent CLI
- 产出静态页面,白嫖 GitHub Pages 的服务器,不用为了写博客多花钱
- 文章直接以文件形式储存在 GitHub 上,安全可靠
- 文章和博客代码维护在一起,写文章的过程中发现的代码问题可以立即修改并同步发布
- 从写文章到预览到发布的全流程都可以在一个 VSCode 窗口内完成,很契合我的工作习惯

- 可以自己控制全部样式和布局,因此可以做出现在的这种超级复
ya古yi效果 - 可以自由扩展我喜欢的阅读器功能,比如 hover 预览
设计理念
DRY 和 DIP
在一个系统中,每一处知识都必须单一、明确、权威地表达。
《The Pragmatic Programmer》
A. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
B. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
《Agile Software Development: Principles, Patterns, and Practices》
博客系统天然需要 Blog(title, description, content) 这样的实体,但传统的分开定义的方式难免有以下问题:
content里的# 文章标题算正文还是标题?渲染和导出时要去重吗?- 我习惯在正文的第一段概括全文内容,我为什么非得把它单独写成文档描述,与文档分离?
- 导出为 md 时标题要拼接到文档头部还是 frontmatter,描述呢?
我认为这是一种违反 DRY3 原则的、讨厌的重复。
VitePress 等很多博客系统引入了 frontmatter 用于定义文档实体的元数据,并让用户可以引用这些数据来解决重复问题:
---
title: 文章标题
---
# {{ $frontmatter.title }}
正文内容
但这就让正文只有在被 VitePress 编译后才能正确渲染,也就是违反了 DIP4。而且我觉得 frontmatter 好丑。
为了避免上述这些问题,我的博客标题取原始 MD AST5 中第一个 heading,而描述则取标题后的第一个 paragraph。得益于此,我不用反复书写相同的标题和描述(DRY),也不用为了编译和渲染器需要的实体结构来改变我的 md 文档结构,而是让 文档 -> 解析器 反转为 文档 -> MD AST <- 解析器 —— 典型的 DIP。
易用性
写文章本来就很费心了,我不想再给自己加任务。目前我的工作流是:
# 创建文档模板
pnpm write second
/workspaces/blog/public/blogs/1781614950277-second/index.md
# 启动开发服务器
pnpm dev
这样就可以在侧边栏实时预览渲染效果,并在 VSCode 中编辑文档。
写完后就直接通过 VSCode 的源代码管理 UI 进行提交和推送即可,GitHub Action 会自动触发并完成测试、构建和发布到 Pages 的工作。
这种两条命令就可以开始写文章的复杂度对我来说是可以接受的,多几个步骤换来的是与编码心智模型相同的码字体验。
站点还仅在 SSG 期间自动生成 sitemap.xml、robots.txt、llms.txt、atom.xml 等辅助检索、阅读的静态文件,同时将部分图片转换为 webp 以降低网络压力。所有这些功能对码字过程都是透明的。
文档优先
我用 vite 原生支持的 Static Assets 机制在 public/blogs/ 目录下管理所有文档。pnpm write <slug> 命令会帮我创建以下格式的博客模板:
<timestamp-slug>
├── assets/
└── index.md
在写文章时,我不用考虑图片等静态文件的放置位置,可以自由地用 public/a/b/c.png、./assets/a.png、../xxx/assets/b.png、../xxx/index.md#title 等各种方式引用 public 目录中的静态资源和其他文章等。在渲染时,一个 remark 插件会完成目录的映射工作,将对文档的引用解析为 /blog/slug/#hash 的形式,而将其他静态资源解析为相对于 / 的 url 形式,以便直接利用 Static Assets 机制引用打包好的静态资源。
同时,文章的创建时间也自然地被通过目录名中的 unix timestamp 维护起来,且可以实现文章自然地按时间顺序在目录下排序。
最后,index.md 和存储它的目录基本上完整描述了一篇博客的的所有数据,无需额外的配置文就可以定义一个 Blog 实体,这种文档优先的做法也进一步降低了码字难度。
当然我还没有引入文档 tags,也主要是 tags 很难像 title 和 description 一样从 AST 中被提取。这里就只能依赖后人的智慧了。
Icon
本站的 favicon 是这个:
原图是 Code 和 平底锅 两个 SVG 图,自行拼接并改为黑色背景和白色线条。
我认为软件工程犹如烹饪,是纯粹的创造。因此这个博客除了技术内容之外应该也会整理一些原创食谱。
Footnotes
-
Static Site Generator,静态站点生成器 ↩
-
Don't Repeat Yourself,即不要重复自己,对本例来说就是标题写一次就够了 ↩
-
Dependence Inversion Principle,即依赖倒置原则,对本例来说就是高层文档内容与 vue 风格的模板语法强耦合 ↩
-
Abstract Syntax Tree,即抽象语法树,博客使用的解析器是 mdast ↩