Caveman 这个名字,是我给一个用了六个月的极端项目起的代号。它不指穴居人,也不代表什么原始主义运动,而是我的一次反抗——在同时被十几个在线工具、三个笔记软件和两套博客框架折腾到几乎崩溃之后,我决定往回走,用最土、最笨、最原生的办法,重新构建一个属于自己的个人站点和知识库。
这个项目没有用任何现代前端框架,没有依赖任何在线笔记平台,没有复杂的自动化流水线。核心就是一台能跑 Python 的电脑、一个文本编辑器、一堆 Markdown 文件,以及一个不到两百行的构建脚本。听起来像考古现场,但它确实解决了我的核心问题:内容永远属于自己、系统永远不会因为厂商停服而报废、写东西的流程短到打开文件就能开始。这篇文章会把整个项目的思路、选型、实操步骤和踩坑记录完整拆开,如果你也在被复杂工具折磨,或者想找回那种"一块木头也能生火"的掌控感,应该能从这里拿走一套可以直接上手的方法。
1. Caveman项目:一次对抗工具焦虑的复古实践
1.1 项目的缘起:被工具绑架之后的反向实验
事情的起因很普通。我在过去几年里换过四款笔记软件,从最早的本地纯文本到后来的云同步笔记,再到带数据库和关系图谱的"第二大脑"工具。每一款软件刚上手时都让人兴奋,但时间一长,问题就出来了:有的同步开始出错,有的收费策略突然变更,有的笔记数据导出格式越来越封闭。最让我崩溃的一次,是我发现自己依赖的一款在线写作平台的导入导出功能变得异常复杂,我花了整整一个周末才把几百篇文章折腾出来,格式还丢了一半。
那之后我意识到,工具越智能、越庞大,用户被绑架得越深。我需要一个反方向的方案,做一个"穴居人"级别的系统——像原始人一样用最基础的东西,把复杂现代生活里真正重要的内容重新攥在自己手里。于是 Caveman 诞生了。
1.2 Caveman不是拒绝科技,而是重新认识科技
这个项目叫 Caveman,容易让人误解为反技术崇拜。其实恰恰相反,我用了很多现代工具,尤其是 Git 版本控制、Python 脚本和命令行工具,它们都是这个项目的骨干。真正的区别在于,我选择了"低依赖、高掌控"的路线:尽量让系统由普通的文本文件和简单脚本组成,而不是由黑盒服务组成。
这种思路和很多极简主义的数字生活实验一脉相承,在海外社区里有个相近的概念叫"数字园艺"和"手写博客",很多人都在尝试用最少的工具、最透明的流程来维护自己的内容。Caveman 就是这类探索的务实版本:不追求"回归原始"的形式感,只求每一步都可理解、可修复、可迁移。
1.3 这个项目适合哪些人
如果你符合下面任何一条,Caveman 的思路会非常适合你。
- 写过博客但被 Hexo、Hugo、WordPress 这些系统折腾过,想要更轻量的写作环境。
- 依赖 Notion、印象笔记等平台,但担心数据被锁定、编辑体验过于笨重。
- 对命令行和脚本有兴趣,想搭一个属于自己的极简工具链。
- 纯粹是厌倦了被各种通知、图表和 AI 助手包围,想回到"打开文件就写作"的安静状态。
说到底,Caveman 不是一套软件,而是一条思路:你完全可以不依赖大型工具生态,用最朴素的元件打造一个稳定、高效的内容系统。接下来的部分,我会从设计原则到具体代码,把这条思路完整还原出来。
2. 整体设计与方案选型的底层逻辑
2.1 三条设计原则:最小依赖、纯文本优先、可迁移
Caveman 在设计阶段就定死了三条原则,之后的每一个选型都围绕它们展开。
第一条原则是最小依赖。这里的"依赖"包括运行依赖、维护依赖和心理依赖。运行依赖是指系统运行需要什么环境——我只要求一台装了 Python 3 的电脑,连数据库都不需要。维护依赖是指日后升级和修复的成本——尽量不用第三方库和复杂配置,出问题一眼就能看穿。心理依赖是指我对工具的信任——我要求自己对系统的每一个零件都心里有数,哪怕有一天开发者生态变了,我也能自己维护个十年八年。
第二条原则是纯文本优先。所有内容正文都以 Markdown 格式存储,不塞进数据库,不搞私有格式。纯文本的好处多得说不完:任何编辑器都能打开,任何系统都能处理,Git 可以完美地追踪每一行变更,而且永远不会因为软件版本过新打不开老文件。很多人在几年前用 Word 写文档,现在打不开了;但二十年前用纯文本写的日记,现在仍然清晰可读。这就是纯文本的力量。
第三条原则是可迁移。我在选型时反复问自己:如果这个系统必须搬家,我需要多久?答案永远是"十分钟"——因为所有内容都在几十个 .md 文件里,构建脚本用 Python 标准库就能跑,模板是几个孤立的 HTML 片段。我可以很轻松地把整套东西搬到一个新服务器,甚至可以彻底放弃构建脚本,直接用文本编辑器手动生成页面。这种"随时可以走人"的安全感,是任何在线服务都给不了的。
2.2 技术选型:为什么是"脚本+模板"而非静态站点生成器
很多人一听要搭个人网站,第一反应是去装一个静态站点生成器,比如 Hugo、Hexo、Jekyll,或者更现代的 Astro。这些工具本身很好,但在 Caveman 的项目里,我刻意没有使用它们。
原因分三方面。其一,静态站点生成器本身就是一层抽象,而且是一层不小的抽象。以 Hugo 为例,它的目录结构、模板语法、shortcode 机制,加起来需要学习很多内容。一旦某个主题不满足需求,你就要深入源码去找问题。这和"Caveman"背道而驰——它把简单的事情重新弄得复杂了。其二,生成器有固定的数据模型,比如一些工具要求使用分段、分类、权重等配置字段,这会约束内容的组织方式。而用自写脚本,我可以决定"文章就是文件路径+正文",其他什么都不需要。其三,我自己的技术栈足够写好一个构建脚本,这比去学习一个庞大框架更符合项目的精神——保持掌控。
我最终采用的方案,本质上是一个约 200 行的 Python 脚本:读取 content 目录里的 Markdown 文件,解析头部信息和正文,套用模板,输出静态 HTML 到 public 目录。它没有任何插件系统,也没有配置语言,却完全够用。
2.3 放弃动态网站和在线笔记的理由
在早期设计时,我也做过参与式讨论:是不是应该用 WordPress,或者用 Next.js 搭一个带后台的博客?最后我全盘否掉了。
动态网站的代价是服务器、数据库、安全补丁和漫长的页面加载时间。我写博客只是想发布内容,不想每半年担心一次 PHP 版本升级或数据库备份失败。在线笔记平台的代价,则是数据所有权和灵活性。如果我把文章存在别人的数据库里,即便能导出,也始终隔着一层"许可"。Caveman 的做法是彻底本地化:文件和生成结果都留在自己手里,发布到服务器时也只是把静态文件丢上去,服务器上根本不需要运行任何应用代码,自然也不存在被攻击的大面积风险。
这套方案的应用效果,我六个月跑下来的感受是:它不只是一个博客系统,更是一个所有内容资产的本地仓库。无论是一篇文章、一张会议纪要,还是一个临时记录,都统一落到纯文本里,统一的、可搜索的、永久的。
3. 核心细节解析:Caveman的运转方式
3.1 目录结构与文件组织
Caveman 的项目目录非常简单,一眼就能看明白:
caveman/ ├── content/ │ ├── posts/ │ │ ├── 2024-05-01-hello-world.md │ │ └── 2024-05-15-caveman-notes.md │ ├── pages/ │ │ └── about.md │ └── drafts/ │ └── unfinished-thought.md ├── templates/ │ ├── base.html │ ├── post.html │ └── list.html ├── scripts/ │ └── build.py ├── public/ │ └── (构建后生成的静态文件) ├── assets/ │ ├── style.css │ └── favicon.ico └── README.mdcontent 是内容的唯一来源,posts 放文章,pages 放固定页面,drafts 放草稿。templates 存放 HTML 模板,scripts 里是构建脚本,public 是构建产物。assets 放样式表等静态资源。
这种布局的优点是职责单一、路径即分类。想发一篇新文章,只要往 posts 目录里放一个新的 Markdown 文件即可,不需要在数据库里插入记录,也不需要注册路由。目录本身就是数据库,文件系统的层级天然表达了内容的归属。
3.2 核心构建脚本:把Markdown变成网页
构建脚本是全项目的心脏,我把它的核心逻辑分成四块:遍历文件、解析元信息、转换格式、渲染模板。
遍历文件部分,采用 Python 标准库的pathlib遍历目录,按文件名排序。解析元信息部分,不需要引入复杂的 frontmatter 解析库,我约定文件头部的三行固定字段:title:、date:、desc:,用纯手工解析即可,两百行以内就能搞定。转换格式部分,我使用 Python 的markdown库把 Markdown 文本转换成 HTML。这个库本身是唯一的外部依赖,但它是纯 Python 的、广泛使用且常年稳定的。渲染模板部分,用 Python 标准库的string.Template做简单的变量替换,同样不引入 Jinja2 之类的重型模板引擎。
我理解很多读者这会想问:为什么不直接用现有的库把这几步组合起来?原因很简单,如果我用现成的静态生成器,就不会真正理解这个系统。自己写过一遍之后,我才知道每一步发生了什么,以后无论它出什么问题,我都能在最底层找到答案。这是"穴居人"式的固执,也是它最稳固的原因。
3.3 写作与发布流程设计
在 Caveman 里,写作和发布是两个完全分离的环节。写作时,我只需要用任何文本编辑器打开content/drafts/下的文件,沉浸式地写。没有后台编辑器,没有富文本工具栏,没有自动保存冲突提示。写完后,把文件移动到content/posts/目录,顺手把文件名改成带有日期的格式,然后在命令行运行一条命令:python scripts/build.py。
这条命令会把所有草稿以外的 Markdown 文件处理成 HTML,生成全新的 public 目录。之后用一条 rsync 命令把 public 目录同步到云服务器,发布就完成了。整个流程从开始写作到上线,最短只要一分钟。
这个流程的价值在于它的"短"。我不需要打开浏览器转悠、不需要登录后台、不需要考虑插件更新。写作和发布之间的摩擦小到接近于零,这直接决定了我更新内容的频率——当你不需要面对繁琐流程时,会愿意写更多的东西。
3.4 搜索与归档的低科技替代方案
现代内容系统通常自带全文搜索和标签功能,Caveman 里我选择了更"笨"的方案。搜索方面,站点提供的是一个简单的本地搜索脚本,利用 Python 的grep逻辑扫描 content 目录,返回匹配的文件列表;部署后的服务器上也有一个静态索引,在构建时自动生成,访客可以在网页里用浏览器自带的查找功能做关键词定位。说实话,对一个几百篇文章的个人博客来说,这个方案已经完全够用。
归档方面,我不使用数据库式的动态归档页,而是按年份生成静态列表。构建脚本读取每篇文章的日期字段,生成一个按时间倒序排列的文章目录页。标签页的思路也一样,从文章头部的desc和正文里提取关键词,生成一个简单的文本清单。这样就没有"标签云"和"关联文章"那些花哨功能,但胜在页面加载飞快,且没有任何交互成本。
很多朋友问我为什么不加一个真正的搜索框,我反问:你觉得你自己博客上的搜索框,真正用到的机会有多少?大多数来访者是通过搜索引擎点进来,又在几分钟内离开的。为一个几乎没人用的功能增加维护负担,不值得。
4. 实操过程:从零搭建属于你的Caveman
4.1 第一步:初始化目录与文件
我建议你从现在开始,照着下面的步骤搭一套自己的 Caveman。首先,在本地创建一个项目目录,并生成最基础的文件夹结构:
mkdir caveman cd caveman mkdir -p content/posts content/pages content/drafts templates scripts public assets创建完成后,在templates/base.html里写一个最简的页面骨架,包含 HTML 头、一个导航栏区域、一个内容占位符和页脚。我的第一个模板长这样:
<!doctype html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>$title</title> <meta name="description" content="$desc"> <link rel="stylesheet" href="/assets/style.css"> </head> <body> <header> <a href="/">$site_title</a> </header> <main> $content </main> <footer> <p>由 Caveman 构建</p> </footer> </body> </html>这里$title、$desc、$content等会在构建时被替换成实际内容。为什么用string.Template?因为它不会自动执行任何逻辑,只做纯粹的字符串替换,这让我确信模板不会成为攻击面或意外行为的温床。
4.2 第二步:编写构建脚本
构建脚本build.py是全项目的核心,下面这个精简版本是我实际运行过的,你完全可以照抄后修改。
#!/usr/bin/env python3 """Caveman - 极简静态站点构建脚本""" import re import shutil from pathlib import Path from datetime import datetime from string import Template import markdown BASE_DIR = Path(__file__).resolve().parent.parent CONTENT_DIR = BASE_DIR / "content" TEMPLATE_DIR = BASE_DIR / "templates" PUBLIC_DIR = BASE_DIR / "public" SITE_TITLE = "我的Caveman" def parse_meta(text: str) -> tuple: """解析 Markdown 文件头部元信息""" lines = text.split("\n") title = "未命名" date = datetime.now().strftime("%Y-%m-%d") desc = "" content_start = 0 for i, line in enumerate(lines): if line.startswith("title:"): title = line.split(":", 1)[1].strip() elif line.startswith("date:"): date = line.split(":", 1)[1].strip() elif line.startswith("desc:"): desc = line.split(":", 1)[1].strip() elif line == "---": content_start = i + 1 break body = "\n".join(lines[content_start:]) return title, date, desc, body def render_template(template_name: str, variables: dict) -> str: path = TEMPLATE_DIR / template_name tmpl = Template(path.read_text(encoding="utf-8")) return tmpl.safe_substitute(**variables) def build_post(path: Path, base_html: str): raw = path.read_text(encoding="utf-8") title, date, desc, body = parse_meta(raw) html_body = markdown.markdown(body, extensions=["extra", "sane_lists", "nl2br"]) rendered = render_template("base.html", { "title": title, "desc": desc, "content": html_body, "site_title": SITE_TITLE, }) dest = PUBLIC_DIR / "posts" / path.stem dest.mkdir(parents=True, exist_ok=True) (dest / "index.html").write_text(rendered, encoding="utf-8") def build_index(): ... def build_page(): ... def main(): if PUBLIC_DIR.exists(): shutil.rmtree(PUBLIC_DIR) (PUBLIC_DIR / "assets").mkdir(parents=True, exist_ok=True) shutil.copy(BASE_DIR / "assets" / "style.css", PUBLIC_DIR / "assets" / "style.css") (PUBLIC_DIR / "posts").mkdir(parents=True, exist_ok=True) for post in sorted((CONTENT_DIR / "posts").glob("*.md")): build_post(post, None) print(f"构建完成: {post.stem}") if __name__ == "__main__": main()需要注意的是上面代码里我留了build_index和build_page的空函数,你看着就能明白这是在哪里生成首页和固定页面的。这个脚本里其实暗含一个取舍:构建时直接清空 public 目录再重建,保证线上永远不会有多余的旧文件。好处是绝对干净、不会留僵尸页面;坏处是如果你在服务器上直接构建,一瞬间会有一篇请求不到的窗口,但我通常是在本地构建好再上传,所以这个缺点完全不存在。
4.3 第三步:设计极简页面模板
Caveman 的页面模板走的路线是"尽量少、尽量窄":一个用于文章页的base.html就够用,列表页可以单独用list.html来展示所有文章的标题和摘要。我贴一下自己实际用的列表页片段:
<main> <h1>文章列表</h1> $post_list </main>这个$post_list是在构建脚本里生成的 HTML 片段,逻辑特别简单:遍历内容目录,输出一个带链接的列表。每个条目就两样东西:日期和标题,点击进去就是正文页。这里我没有做分页,也没有做分类筛选,因为对个人博客来说,全文罗列反而更尊重信息本身——任何一篇文章都在列表里,不会有文章因被"精选"而沉底。
样式方面,我写了一份不到 80 行的style.css,参考了瑞士国际主义排版的基本路数:系统字体栈、白底黑字、留白充足、链接下方画下划线。为了让它在手机上也能舒服地看,我加了一条max-width: 720px; margin: auto;的处理,这样整个页面不管在什么设备上都能保持一栏的阅读宽度。我建议你也把样式做得尽量克制,因为 Caveman 的意义不在视觉炫技,而在内容持久。
4.4 第四步:部署与更新
部署用的是最直接的 rsync。我有一台位于普通机房的小型云服务器,不需要任何额外的软件环境,因为线上只需要一个静态文件服务,用 Nginx 就能承载。构建完成后,我在项目目录里运行:
rsync -avz --delete public/ user@server:/var/www/caveman/这条命令会把整个 public 目录同步到服务器,--delete参数保证在本地删掉的文件,线上也不会残留。更关键的是,服务器上没有任何应用程序进程,没有数据库连接,没有可以注入的漏洞点。运行三个月的统计显示,我的 Nginx 访问日志里几乎没有异常扫描尝试,不像过去跑 WordPress 时每天都能看到几十次攻击探测。这就是把动态逻辑全部挪到本地构建端的最大好处——公网只需要"展示"静态文件,不需要"计算"任何东西。
4.5 第五步:日常写作与维护
日常写作流程已经被我练成了肌肉记忆。打开终端,进入 Caveman 目录,在content/drafts/里新建一个带当日日期的 Markdown 文件,正文第一行写title:,空一行后开始打字。写完后移动文件到posts/目录,运行python scripts/build.py,再执行那条 rsync 命令。全程不超过两分钟,没有任何打断心流的环节。
我甚至会为了"无打扰"故意不用 GUI 编辑器,直接在本地的 Sublime Text 里打开 Markdown 文件编辑,一屏之内搞定所有操作。有些朋友会问那图片怎么办,很简单——把图片放在内容旁边的一个images目录里,文中用相对路径引用,构建时同步复制到 public。这也是最原始而可靠的资产管理方式,图片永远和你文章同在一个仓库里,永远不会丢失。
5. 常见问题与避坑指南
5.1 常见问题速查表
在我把 Caveman 分享给十多个朋友之后,大家实际跑起来遇到的最集中问题,我整理成了一张速查表,你直接按图索骥就行。
| 问题现象 | 原因 | 解决方案 |
|---|---|---|
构建后页面全是$title没被替换 | 模板里使用了Template但没调用safe_substitute | 检查渲染函数,必须用safe_substitute而非substitute |
| Markdown 代码块里出现错位 | Python 的markdown库需要开启extra扩展 | markdown.markdown(text, extensions=["extra", "sane_lists"]) |
| 中文文件名在 rsync 后乱码 | 本地与服务器文件系统编码不一致 | 所有内容文件名统一用英文加日期,中文只出现在正文里 |
| 本地能打开页面,线上 404 | 相对路径写成了绝对路径 | 链接统一使用根路径/posts/xxx/格式 |
| 草稿误发布 | 文件放在了posts/而不是drafts/ | 构建脚本里跳过drafts路径,发布前检查文件位置 |
这份速查表里的每一条都是我或是朋友实际踩过的。特别是第一条,很多人刚上手时用substitute就会因为模板里有未定义变量直接抛异常,而safe_substitute则不会,它会安静地保留占位符,这更容易暴露问题。
5.2 我踩过的坑和总结的窍门
第一个真正的坑,是我早期试图给构建脚本加"标签系统"。我天真地在 Markdown 头部加了一个tags:字段,然后在构建脚本里写了一大段逻辑来生成标签页。结果标签页反复出问题,而且写着写着发现自己在解决一个根本不存在的问题——我的文章量不到一百篇,标签的意义实在有限。花了一个下午做完后,第二天就把它从代码里咔嚓掉了。不要为了功能而功能,需求要跟着自己的真实使用习惯走。
第二个坑是备份。我一开始觉得"反正文件都在本地,没什么可丢的",直到有一次我误删了一个content/posts/目录,幸好 Git 提交历史里还有记录,才救回来。从那以后,我给 Caveman 定了三重备份规则:Git 仓库推到代码托管平台、外接硬盘每周冷备份一次、重要文章还额外导出成一份纯文本放在网盘里。任何系统都需要备份,纯文本的好处是备份成本低到可以忽略。
第三个窍门,是我在写博客列表页时总结出来的:列表页只显示标题加日期,不要显示摘要。很多系统默认显示摘要,反而逼着读者做额外判断。在 Caveman 里,一段极简的列表让用户能纯粹地按时间浏览,想看哪篇点哪篇,回归了"目录"的本质属性。这个方法的手段极其简单,但阅读体验比那些堆满了摘要和标签的列表好了非常多。
最后说说对这个项目的后续想法。我现在正在给 Caveman 加一个简单到极致的评论机制:不是接入任何现成论坛框架,而是让每篇页面底部提供一个相对路径的投稿链接。访客通过这个链接发送的信息,会被一个极轻的小程序收集后转成邮件发给我,我再手动选择是否将它追加到文章底部的"回应"列表里。这个方案完全不依赖第三方服务,隐私关系干净,也保留了对话的克制感和筛选权。这样一篇"原始博客",反而比很多现代系统的交流更加真实和有态度。
我在实际使用中发现,Caveman 解决的不只是技术问题,更重要的是它给了我一种"内容主权"的心理安全。六个月前我开始做这件事时,只是想摆脱工具瘫痪的焦躁;现在回头看,这个极简系统成了我最稳定的数字基础设施。它让我愿意重新开始写长文,愿意把很多想法落在文本里,而不是散落在各种软件的碎片记录中。如果你也被工具压得喘不过气,不妨试着亲手搭一套自己的 Caveman,不需要多复杂,从一篇 Markdown 和一个小脚本开始就行。