老实说,我一开始也没想到会为“rea”这个看起来没啥信息的名字折腾这么久。事情起因很简单:我长期维护一个个人技术笔记库,里面攒了几百篇Markdown文档,包含大量临时记录、代码片段、项目复盘和日常踩坑备忘,初期一直依赖现成的静态站点生成方案,但用着用着总觉得极度别扭——配置越堆越厚,构建越来越慢,明明只想写点东西,最后反而被工具链拖住了。后来我花了不少时间,把整套发布流程压缩到几条命令,核心思路就是围绕一个我私下叫“rea”的精简渲染方案做文章。这篇文章就把从需求判断、目录设计、构建链路到部署维护的完整过程摊开讲,适合那些跟我一样手里有大批Markdown笔记、又不想被重型框架绑架的开发者参考。它不是一篇工具评测,也不是文档翻译,而是一个真实项目在反复取舍后留下的实操切片。
1. “它不是框架”:我是如何确定rea的定位的
1.1 需求是从上百篇零散文档里逼出来的
每个人的笔记库都会有一个失控期。我早期有相当长一段时间是随手乱放文档,今天新建一个Note_20250115.md,明天又建一个最终版V3.md,偶尔还会把图片、PDF一并塞进同一个文件夹。等到某天真正想找一篇关于某次调试过程的记录时,才发现内容堆积得毫无规则,搜索还得靠文件管理器逐个点开。这时候我意识到,我需要的不是一个“花哨的博客系统”,而是一个能让我快速把Markdown文档变成可浏览网页结构的最小化发布方案。我希望它做到三件事:输入只要求Markdown纯文本,输出只要求静态HTML文件,中间过程不引入数据库、不依赖在线服务。rea这个代号也是这时候定的——“简单、直接、不需要多余东西”,它后来成了一种设计准则,而不是某个具体产品的名字。
很多人会问,既然已经有那么多成熟工具,为什么还要自己动手?我的理由很现实:绝大多数现成方案都默认你要配置主题、分类、标签体系、社交链接、评论系统等一堆东西,而我的核心需求只是“把Markdown变成一组可以通过浏览器阅读的HTML页面”。工具链越复杂,日常维护成本越高,一旦某个依赖升级导致构建失败,我还得花时间排查。这对我来说是完全不必要的负担。某次我遇到过一次上游依赖接口变更,整整一个晚上都在修构建问题,而不是写文档。也就是那一次之后,我开始思考一个极简的自建方案,把控制权完全拿回自己手里。我给自己定的硬约束是:所有核心依赖尽量只保留Markdown解析和HTML生成两个环节,其余功能能砍则砍,保证即使三年后回来,依然能在一分钟内跑通发布流程。
1.2 rea与传统建站方案的关键取舍
如果纯粹为了写作,直接用Git托管Markdown也能活,但可读性和检索性会很差。如果用重型静态站点生成器,功能固然丰富,但学习成本和配置成本双双上升。我最终选择的方案,用一个公式可以说明白:
rea = Markdown解析器 + 模板渲染 + 本地目录结构(不依赖数据库)与常见方案相比,这套路线的核心差异有几处:
- 内容组织形式完全不同:传统方案通常要求你把日期、分类、slug等信息塞进文件头部FrontMatter,而rea直接使用文件目录和文件名的结构来推导这些信息,等于用文件系统本身承担了元数据管理职责。
- 没有“标签系统”的概念,而是通过自动扫描每个文档中出现的关键词来生成关联索引。这个设计会在后面详细讲,好处是写文档时完全不用维护元数据标签,坏处是算法的准确性需要调优。
- 发布形态是纯静态的HTML文件组,任意HTTP服务器、任意托管环境都能跑,不需要服务端解析,不需要PHP/Python运行时。这也意味着迁移成本极低——换一台机器,只需把整个文件夹拷贝过去,部署即完成。
我在取舍时还有一个很重要的判断:这个方案只面对“我自己的个人文档库”,不需要考虑多人协作。因此很多为了协作而设计的机制(权限、审核、多用户)都不是必需品。这也让rea可以走一条极致精简的技术路线。后续所有步骤、配置、命令,都是在“个人单用户、离线优先、长期可维护”这三个前提条件下展开的。如果你需要的是一个团队知识库或多作者平台,那rea的设计思路可能并不适合你,这一点我希望读者能提前判断清楚。
2. 目录设计:用文件系统做元数据库
2.1 源文件目录划分的“最小但可扩展”原则
我用了相当长时间琢磨目录结构,最终确定的方案其实非常朴素:把整个内容库分成content/、assets/、output/和theme/四个目录。content/存放全部Markdown源文件,它内部再按主题分子目录,比如backend/、frontend/、devops/、notes/等,这个分类完全跟随我自己的工作领域,不做抽象总结,保持直观。assets/统一存放图片、附件、参考文件,便于构建时批量复制。output/是构建产物目录,每次发布都会整体重刷,保证每个HTML文件都是最新版本。theme/则存放模板、样式和脚本资源。
之所以坚持“目录即元数据”,是因为我发现给每篇文档维护FrontMatter里的title、date、categories字段,本质上是把信息写两遍:一遍在文件本身,一遍在文件头部,这是维护成本的重复。文件系统已经能告诉我文件名、创建时间、所在目录、相对路径,如果我在构建阶段读取这些信息并自动换算成页面的标题、分类和URL路径,就不需要额外去维护一套元数据。唯一需要额外记录的是“展示用标题”和“发布日期”,因为我经常给文件起英文名,而展示时想要中文标题。所以rea规定每个Markdown文件头部可以有一段极简的YAML块,可选字段只有title和date,其余信息一律靠路径推导。这个设计在用了几个月后,确实极大降低了新增页面的摩擦成本——我只需要把.md文件丢进对应目录,构建脚本就会自动认出它。
2.2 文件命名规范与排序策略
rea的命名规则简单到近乎苛刻:要求Markdown文件名必须使用数字序号_英文slug.md的格式,例如01_rea-introduction.md、02_file-system-as-database.md。前缀数字的作用有两个:一是直接决定文章在目录列表中的排序,二是用来避免文件名出现中文导致URL编码混乱。slug部分用短横线连接的小写英文词组,表达内容主题。这种规则牺牲了一点写文件的随意性,换来了构建脚本极大简化——我不用在每次生成页面时再去分析乱七八糟的文件名,逻辑判断变得非常稳定。
排序策略是按“数字前缀升序”,即我默认数字小的内容先创建、位置靠前,但这个规则并不是死板的。有些文章我会特意用00_开头,作为某个子目录里的置顶条目;有些系列文章会连续编号,这样浏览时从列表顶部就能看出开发脉络。更细节的做法是:构建脚本里定义了一个NAV配置,允许在每个子目录下放一个_index.md作为目录首页说明。这个文件不会生成独立页面,而是作为该目录列表页的导语呈现在顶部,相当于提供了一个手工控制的“栏目简介”区域。对于想更自由调整导航顺序的人来说,唯一要掌握的就是“数字前缀”这一套约定,其余全都由脚本自动处理。
3. 核心构建链路:从Markdown到静态页面的关键实现
3.1 Markdown转换管线是怎么设计的
构建的核心算是一条流水线:读取Markdown文件→解析内容→填充模板→写入HTML文件。我选用了一款成熟的开源Markdown解析库来承担格式转换,同时自己只写了大约300行代码来把解析结果和模板引擎衔接起来。为什么不全自己写解析器?我的理由是,Markdown语法边界太多:普通段落、代码块、嵌套列表、表格、引用,甚至嵌入HTML,都是不能出错的功能点,自研解析器的成本远高于收益。反而是模板引擎这部分,因为输出格式完全由我自己掌控,并不需要复杂的逻辑,所以自己定义一个极简的占位符替换机制就够了。
模板渲染我采用的思路是“两级拼接”:第一级把Markdown解析出的HTML整段包进<article>骨架,第二级把<article>内容再嵌入整个页面的布局层。相当于每个页面有两个模板文件:post.html负责文档内部结构,layout.html负责外围的导航、页脚和样式引用。这样做的好处是改样式不需要碰文档内容结构,改单篇文章的观感也不会影响到全局布局。构建时还会对每篇文章做“上一篇/下一篇”的导航计算,计算逻辑很简单:按照目录遍历顺序找到当前文档的前后邻居,把标题链接渲染在页面底部。这些细节虽然不复杂,但能明显提升连续阅读的体验——浏览笔记时不必每次返回到列表页重新找下一篇。
3.2 目录索引与关键词关联的实现思路
生成目录索引页是rea和单纯“逐页转换”的重要区别。构建脚本每处理完一个子目录,就会为该目录生成一个index.html,里面列出该分类下的全部分文章,按数字前缀排序,标题下方附一行摘要。摘要怎么来?我规定Markdown文件开头除了可选的YAML块之外,第一个自然段就是摘要,把它截断到120个字符展示。这样做省去了“手动维护摘要字段”的麻烦。因为摘要本身就存在正文开头里,写内容时没人会刻意空着开头,所以几乎所有页面都有自动摘要可用。
跨目录的关键词关联是另一个有意思的实现。我不是通过FrontMatter维护标签,而是构建时给每个内容目录维护一个词频表,统计所有文档中出现频次较高但不在停用词列表里的词汇,构建完成后生成一个统一的keywords.json。用户在页面侧栏点击“相关关键词”,就能看到包含该词的其他文章链接。这里踩过的最深的一个坑,是中文分词问题——直接按空白字符分割对中文句子完全无效。我的临时解决方式是接一个轻量的分词模块,配合自定义词典迭代补充。尽管这个方案不算特别智能,但对我个人文档库规模来说已经够用,而且完全不依赖在线服务,离线状态下照样能构建。
3.3 主题与样式接入的具体做法
rea的视觉呈现走的是“极简文档风格”路线。我把样式集中在theme/static/css里,包括几部分:基础排版样式、代码高亮样式、响应式布局定义。代码高亮我选择在构建阶段直接生成带CSS类名的HTML,而不是运行时用JavaScript高亮,这样页面在关闭JS后依然能看到彩色代码,也减少了一个外部脚本依赖。实现方式很直接:Markdown解析库把代码块提取出来后,交给一个带语言识别能力的语法高亮器处理。这一步比较耗时,所以我会做一层缓存:如果源代码文件的哈希值没有变化,就直接沿用上次生成的高亮HTML片段,构建速度能提升很多。
导航栏则完全由目录结构派生——读取content/下的一级子目录列表,生成顶部导航链接。样式细节上我刻意不给每个分类分配不同的强调色,而是整站采用同一种主色调+少量中性色,因为这样做能在长期更新中避免“配色选择困难”,也更容易让读者把注意力放在文字本身。“内容比装饰重要”这句话在rea上不是口号,而是通过技术选型直接落实的。
4. 三个本地命令搞定全套流程
4.1 从编辑到预览:rea的日常闭环
整个rea使用流程,可以用三条命令概括:
rea init my-notes cd my-notes && rea new "backend/网络重试机制踩坑" rea serve第一行命令会按照上面说的目录规范创建一个空白内容库,第二行在对应子目录级联生成文件名带日期前缀的Markdown模板,第三行会执行本地预览——启动一个监听文件变化的小型HTTP服务,并把浏览器自动指向本地地址。日常写作时,我基本只用到rea new和rea serve,写完保存,浏览器自动刷新,所见即所得。这里有个很细节的设计:rea serve的监听只针对content/和theme/目录,构建缓存存在output/.cache里,这样当我编辑一个文档时,脚本只重新渲染相关页面,而不是全量刷新。实测下来,即使内容库有几百个文档,单次增量构建也能保持在几十毫秒级别,编辑体验接近实时。
4.2 离线发布与增量构建机制
发布时执行rea build,它会做三件事:清空output/下的旧HTML、全量重新生成目录索引和关键词索引、把assets/目录同步过去。听起来是“全量构建”,但因为有缓存层,实际需要完整渲染的只有变化过的页面;静态素材则通过哈希对比跳过重复复制。这里我觉得有一个值得分享的判断:为什么发布时用全量模型而不是纯增量模型?因为output目录本身是可丢弃的产物,保证“任何一次build都能从零恢复整个站点”比“构建速度最快”更重要。一旦output里混入上次没清掉的废弃文件,长此以往就会出现“本地显示正常、线上一直有脏页面”的诡异问题,这种问题排查起来比重新全量构建一次要费事得多。
发布到公网我采取的是一个极简的方案:直接用同步工具把output/推送到任意对象存储或普通Web服务器目录。没有引入预处理流程,没有后端渲染,也不需要CDN回源配置。每次推送完我习惯性地用浏览器无痕窗口检查一遍几个关键页面,确认HTML中的相对链接都正确。在rea的目录结构里,页面之间大量使用相对路径跳转(../notes/xxx.html),这样即使整个站点部署在一个子目录下也不会出现链接失效。很多时候人们喜欢用绝对路径图省事,但部署环境一换,绝对路径就成了最坑的东西。rea从一开始就强制相对路径策略,算是从根上杜绝了这类部署问题。
5. 使用rea过程中反复踩到的坑
5.1 中文文件名、URL编码和服务器兼容性
最早我把文件名中文化过一阵子,比如使用手册.md,本地构建一切正常,但部署到某台服务器后,发现部分Web服务器对非ASCII字符的文件URL处理方式不统一:有的会自动编码,有的直接返回404。后来我彻底放弃中文文件名,全面改用“数字前缀+英文slug”,标题的中文展示靠文件头部的title字段解决。这个问题看似细小,却花了我最多时间——因为本地一切正常,线上偏有问题,最后还是用排除法逐个请求URL才定位到编码环节。所以现在我的建议非常明确:源文件名尽可能只用[a-z0-9_-],展示信息放头部元数据,别指望文件系统替你处理国际化文件名。
5.2 相对路径与资源引用的三角冲突
静态站点里最常见的两个资源引用场景是:页面内引用图片,以及引用其他页面。rea的目录结构有层级,如果直接在Markdown里写,最终页面生成的HTML相对路径会跟Markdown源文件所在的相对位置有关,必须要经过一次路径换算。我一开始用简单字符串替换处理,很快发现子目录深一两层之后路径就变得乱七八糟。后来我改用“以assets/为根目录”的统一约定:在Markdown里引用图片时一律写/assets/xxx.png这种根相对路径,然后在构建阶段根据目标页面的输出深度自动转换../层级。这一步虽然让脚本逻辑变复杂,但换来的是所有文档里引用图片的写法和页面输出的深度无关。如果你要模仿这个方案,我强烈建议在一开始就引入这个转换逻辑,否则后期页面层级一变深,你会被一堆修不完的相对路径折磨。
5.3 增量构建里隐藏的“幽灵页面”
综合前面提到的“全量发布+增量渲染”,我遇到过一次最典型的问题:某篇文章被移除了,但它的HTML页面还残留在output目录中,导致搜索引擎和本地浏览都能看到一篇已经不存在的文档。我初期没有太在意清理问题,直到一次域名迁移时用爬虫抓链接,才发现几十个陈旧页面占了不少空间,也干扰了站点结构。之后我在rea build里加入了一项清理机制:全量计算当前内容库应当生成的页面清单,然后删掉output中所有不在清单内的HTML文件,再把新生成的页面写入。说白了就是每个构建任务结束前做一次“期望状态与真实状态的收敛”,这才彻底解决了幽灵页面问题。这条经验对任何静态站点的发布流程都有参考价值:不要只关心构建生成了什么,更要关心这次构建之后该删除什么。
6. 进一步改造rea的可能方向
经过这轮实践,我自己对rea的核心成果进行了总结,其实最重要的不是代码实现,而是那套“用最少规则约束内容组织方式”的思维方式。它让我意识到,一个工具如果让你在写作时还要想着维护元数据,你就已经在承担隐形税了。rea主动把元数据负担丢给文件系统和目录规则,每次新增文章的成本趋近于零。这套逻辑思路完全可以移植到别的项目里:无论你是做一个个人看板、一个内部工具文档站,还是整理电子书素材库,“让文件系统替你记住结构”都是一个极其有效的基本原则。
目前我还在尝试给rea补上两个能力:一是支持短代码,方便在不写HTML的前提下嵌入视频、代码块折叠等富交互元素;二是做一个简单的阅读进度统计,通过本地存储记录哪些文档读过、哪些还没读。考虑到单人使用的规模,这些功能都不需要做成通用能力,只要能解决我自己的需求并保持不超过一千行代码的实现体重,就算是在rea的设计约束之内成功了。现在我每次打开笔记库写新文档时,几乎已经感觉不到“工具”的存在,这就是我觉得这个项目值得写下来的原因。