符合 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 窗口内完成,很契合我的工作习惯 编辑器
  • 可以自己控制全部样式和布局,因此可以做出现在的这种超级复 yayi 效果
  • 可以自由扩展我喜欢的阅读器功能,比如 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 是这个:

favicon

原图是 Code平底锅 两个 SVG 图,自行拼接并改为黑色背景和白色线条。

我认为软件工程犹如烹饪,是纯粹的创造。因此这个博客除了技术内容之外应该也会整理一些原创食谱。


  1. vike 是基于 vite 的元框架,有极高的自由度,不限制前端框架、渲染策略、后端框架和部署方式等

  2. Static Site Generator,静态站点生成器

  3. Don't Repeat Yourself,即不要重复自己,对本例来说就是标题写一次就够了

  4. Dependence Inversion Principle,即依赖倒置原则,对本例来说就是高层文档内容与 vue 风格的模板语法强耦合

  5. Abstract Syntax Tree,即抽象语法树,博客使用的解析器是 mdast