在折腾过七八个静态站点生成器之后,我已经懒得再为一个博客维护一套Node生态或者一堆YAML配置了。Caveman是我目前最满意的一个——它是一个用Python写的极简静态站点生成器,核心思路简单到有点原始:模板直接用Python来写,内容用Markdown写,构建命令一敲,整个站点就是一堆纯静态的HTML文件。这个工具能帮你把个人博客、文档站、作品集丢到任何静态托管平台上,同时把注意力从工具配置拉回到内容本身。适合谁呢?适合有一定Python基础、不想学第三套模板语言、又想要完全掌控输出的开发者。下面我用一次真实的搭建过程,把这个工具的脾气、坑和玩法都过一遍。
1. 项目概览:为什么我最终选了Caveman
1.1 它帮我解决的三个真实痛点
先说说我换到Caveman之前的状态。用过Hugo和Jekyll的朋友都知道,Go模板和Liquid模板本身功能并不弱,可日常写来写去就那么几个用法:{{ .Title }}、{% for post in site.posts %}、{% if page.xxx %}。但真到了要表达复杂逻辑的时候,比如按月份分组文章列表、做双语内容切换、或者给某个页面定制一套特殊布局,你就得去翻模板引擎的文档,搞清楚这个DSL的边界条件到底在哪。这种心智负担很消耗写作心情。
Caveman直接把这个负担砍掉了。它的模板层就是Python本身,{% for post in posts %}就是普通for循环,{{ post.title }}就是普通的变量表达式。我不用学第二套模板语言,Python里怎么写逻辑,模板里就怎么写。这种感觉非常接近"把HTML和Python脚本混在一起",初看有点野,用习惯了反而觉得自由。
第二个痛点是配置系统。很多SSG会把站点名、主题、菜单、分页、多语言全部塞进一个几百行的配置文件里,改一个导航栏可能要同时动三四个文件。Caveman的配置量接近于零,它连网站标题、URL前缀这些都不替你预设,留给你在模板里自己定义。第一次用会有点懵,因为一切都要自己动手;但用起来之后,项目里每一行配置你都认识,因为那本来就是你亲手写的。
第三个痛点是构建过程不透明。Hexo和Hugo这种成熟工具会把模板、主题、插件分成好几层,构建时你看不出一个页面的最终HTML到底是怎么拼出来的。Caveman粗暴也直白:读内容文件、解析元信息、把文章对象交给模板上下文、渲染、写文件,这个链路短到我可以顺着源码一口气读完。出问题的时候,排查成本低得感人。
1.2 优缺点和适用边界
| 优点 | 缺点 |
|---|---|
| 模板直接写Python,Python用户零学习成本 | 完全不懂Python的纯前端使用者会卡壳 |
| 几乎无配置,项目结构一目了然 | 功能朴素,标签、归档、分页都要自己动手 |
| 构建链路短,输出纯静态、部署随意 | 社区生态小,没什么现成主题和插件可用 |
| 依赖极少,一个pip命令就能装 | 全量构建,站点规模大了之后速度会下降 |
我给Caveman画的使用边界非常清楚:个人博客、项目文档站、小型作品集这三类场景是它的主场。大型内容平台、多人编辑系统、需要可视化后台的站点就别指望它了。它更像是一把手工刀具,适合精细制作,不适合流水线生产。
2. 环境准备与快速上手:10分钟建立第一个Caveman站点
2.1 安装Caveman与依赖检查
安装之前先确认一下Python环境。Caveman本身是Python项目,我用它在Python 3.8到3.12上都跑过,建议至少Python 3.8起步。安装命令很简单:
pip install caveman如果是在macOS或者Linux上遇到权限报错,加一个--user参数就行:
pip install --user caveman装完验证一下:
cave --version如果提示命令找不到,先检查一下~/.local/bin是不是在PATH里。另一种安装方式是从GitHub克隆源码,然后以可编辑模式安装,适合想读源码或者二次开发的朋友:
git clone https://github.com/honza/caveman.git cd caveman pip install -e .我个人的建议是直接用pip安装。安装过程中会自动带上markdown这个依赖,因为Caveman需要把Markdown正文转换成一HTML片段。如果安装完发现导入报错,大概率是依赖没装全,后面第5章有具体排查方法。
实操心得:不要图省事把Caveman直接扔进系统Python环境,建议建一个虚拟环境。虽然Caveman自己很轻,但个人博客的构建链路上通常还有其他包,虚拟环境能隔离掉"A项目升级markdown版本导致B项目渲染崩了"这种连锁事故。
2.2 初始化项目目录结构
用cave init创建一个新站点目录:
cave init my-blog cd my-blog初始化之后,目录大致长这样:
my-blog/ ├── content/ │ └── index.md ├── templates/ │ └── base.html └── output/content/用来放Markdown和HTML内容文件,你的文章、页面都在这。templates/放页面模板,模板里可以直接写Python。output/是构建产物目录,执行cave build之后生成的静态HTML都在这里。
目录结构是Caveman给的最少预设,完全可以按自己的习惯调整。我见过有人把模板按_layouts、_includes这种Jekyll风格组织,也有人一个模板文件打天下。Caveman不规定你必须怎么组织,一切都归你管,这是它的极简哲学。
2.3 写第一篇文章并启动本地预览
在content/下新建一个hello.md,内容如下:
--- title: 你好,Caveman date: 2025-06-01 --- 这是我的第一篇文章。Caveman会把这段Markdown渲染成HTML页面。用---包裹起来的是front matter,用来声明文章元信息,比如标题、日期、标签等。Caveman会把这些字段解析成文章对象的属性,供模板调用。front matter的写法比YAML简单,就是普通的字段名: 值格式,不需要引号之类的东西。
然后写一个最简单的模板templates/base.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>{{ page.title }}</title> </head> <body> {{ page.body }} </body> </html>跑构建和本地预览:
cave build cave serve浏览器访问http://127.0.0.1:8000,就能看到这篇"你好,Caveman"的文章了。cave build负责把内容渲染成静态文件放进output/,cave serve起一个简单的本地静态服务器。需要注意,cave serve只是文件服务,不会自动监听内容变化并重新构建。我习惯开两个终端,一个跑serve,另一个改完内容就跑build。
3. 模板语法深度解析:用Python写HTML的真实体验
3.1 模板引擎的工作方式
Caveman的模板引擎可以说是我见过最"不做封装"的模板引擎。它不像Jinja2那样有精密的词法分析和抽象语法树,而是直接把模板里允许的片段当成Python代码来执行。模板里出现的两类符号:
{{ 表达式 }}:计算表达式的值,转成字符串输出。{% 语句 %}:执行一段Python代码。
这句话的信息量很大,意味着你在模板里不只是写"模板语言",而是真的在写Python代码。{% for post in posts %}就是普通的for循环,{% if post.visible %}就是普通的if判断。好处显而易见,你不必记Jinja2的过滤器叫什么名字,不必研究Liquid的运算符优先级,你的Python知识可以直接迁移过来。
坏处也有一点点:模板文件本质上是"带HTML标签的Python代码",而Python是缩进敏感的。所以在模板里写逻辑时要注意缩进对齐,{% if %}和{% endif %}必须配对。新手阶段我踩过好几次缩进坑,构建报错的行号有时候还指歪了,得自己顺着模板往下找。
模板上下文中,Caveman通常会提供几个基础的变量,这是我实际使用中稳定出现的:
| 变量 | 含义 |
|---|---|
site | 站点级信息,比如站点名、URL等,可以自己在配置里定义 |
posts | 所有文章的列表,一般按日期或文件名排序 |
pages | 非文章页面的列表 |
page | 当前正在渲染的页面对象 |
page对象身上挂着title、url、body、date这类属性,全部来自内容文件front matter里写的字段。字段名写错的话,模板里输出空字符串,Caveman不会因此报错,这种静默失败是最坑的。
3.2 三个能直接用的页面模板
直接给一个首页模板的完整示例templates/index.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>{{ site.name }}</title> <link rel="stylesheet" href="/style.css"> </head> <body> {% for post in posts %} <article> <h2><a href="{{ post.url }}">{{ post.title }}</a></h2> <time>{{ post.date }}</time> <div>{{ post.body }}</div> </article> {% endfor %} </body> </html>再写一个文章详情页模板templates/post.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>{{ page.title }}</title> </head> <body> <header> <a href="/">返回首页</a> </header> <article> <h1>{{ page.title }}</h1> <p class="meta">{{ page.date }}</p> <div class="content">{{ page.body }}</div> </article> </body> </html>页面越简单越能体现Caveman的特点:没有主题包结构,没有主题切换,模板就是模板。如果你嫌每个页面都要重复写<head>这段公共部分,可以把公共代码抽成一个partials/目录,或者直接在配置里注册一个partial(name)函数注入上下文,然后在模板里调用{{ partial('head.html') }}。Caveman本身不强制,把公共模块化的方法交给你自己选。
3.3 一次构建,内容怎么变成静态页面
Caveman构建一个站点的流程可以拆成四步。
第一步,扫描content/目录,找出所有.md和.html文件。第二步,解析每个文件,把---之间的元信息读成字段,正文如果是Markdown就用markdown库转换成HTML。第三步,把文章对象放进模板上下文,按文件约定的模板渲染。第四步,按照URL规则生成目录结构,写入output/。
这个流程对小型站点来说是全量扫描、全量构建,没有增量缓存的概念。我见过一个几百篇文章的博客,构建时间也就几秒,完全在可接受范围。但如果站点膨胀到几千篇,或者每篇文章还带着大图压缩任务,全量构建就会变得迟钝。这也是我把Caveman定位成"内容量不大、结构简单"的个人站工具的原因。
构建速度快还有个原因,Caveman内部几乎没有抽象层级,就是"读-渲染-写"三件事。读它的源码时,我全程没遇到需要硬啃的复杂架构,所有逻辑都摊在眼前。这种透明度对排查问题有巨大价值,因为你能精确说出"哪一步、哪个文件、哪一行代码"出了问题。
4. 进阶定制:把Caveman改造成一个完整的个人博客
4.1 标签页、归档页和搜索的实现思路
文章列表加正文页,Caveman已经能支撑一个基础博客了。但博客一般还想要标签分类、按月归档、全文搜索这些"标配能力"。Caveman默认不提供,可它也不限制你实现,因为模板层能跑Python,写起来反而顺手。
我的做法是在构建脚本里给模板上下文注入辅助函数。比如标签云,写一个all_tags()函数扫描所有文章的front matter字段tags,返回一个带计数器的字典。模板里直接调用:
<div class="tags"> {% for tag in all_tags() %} <a href="/tags/{{ tag.name }}.html">{{ tag.name }}({{ tag.count }})</a> {% endfor %} </div>归档页的思路完全一样,写一个archive()函数按月份分组,输出月份标题和对应的文章列表。
搜索这块,比较务实的方案是生成一个search.json,把每篇文章的标题、URL、摘要放进去,前端用一个几十行的小脚本做本地匹配。Caveman可以直接在模板里遍历posts生成JSON文件,再配一个搜索页,部署的时候不需要任何后端服务,个人博客的量级完全够用。
实操心得:不要试图在模板文件里写一大坨Python逻辑,那样模板很快就烂成一锅粥了。我后来把所有辅助函数集中放进
site_helpers.py,构建的时候统一注入上下文,模板里只留简短的函数调用。这样模板好看,函数还能写单元测试,一举两得。
4.2 部署到GitHub Pages、Netlify或对象存储
构建产物是output/下的一组纯静态文件,部署方式其实就是把文件拷到任何能托管静态资源的地方。我用过三种方案,各有各的适用场景。
第一种是GitHub Pages。把output/的内容推送到仓库的gh-pages分支或者docs/目录,访问地址就是用户名.github.io/仓库名。这里最需要注意的是base URL,如果站点不是挂在域名根路径而是子路径下,模板里的绝对路径必须带上/仓库名前缀,否则CSS和链接全会404。我的做法是在配置里定义site.base_url,模板里所有URL都用它来拼。
第二种是Netlify或者Vercel。这些平台支持直接连接Git仓库,构建命令填cave build,发布目录填output,每次推代码自动构建部署,还免费送HTTPS。对个人项目来说,这是性价比最高的方案,基本做到一个push就发布。
第三种是对象存储或自建Nginx。把output/同步到OSS或者S3,或者直接rsync到服务器,改一下Nginx的root配置指向发布目录就行。适合手上有云服务器的朋友,部署自由度最高。
| 平台 | 发布目录 | 优点 | 注意点 |
|---|---|---|---|
| GitHub Pages | output/推仓库 | 免费稳定 | 子路径下要配好base_url |
| Netlify / Vercel | output | 自动构建、HTTPS免费 | 构建命令和发布目录别写错 |
| OSS / Nginx | output同步 | 自由可控 | HTTPS和CDN要自己配 |
4.3 一个Makefile把发布流程固化下来
加了辅助函数之后,构建命令会从一句cave build变成"跑辅助脚本、调构建、再发布"的复合流程。为了避免每次都手动敲一长串,我用一个Makefile把所有操作固化起来。
.PHONY: build serve publish clean build: cave build serve: cave serve -p 8000 publish: build rsync -avz --delete output/ user@your-server:/var/www/blog clean: rm -rf output/*如果用GitHub Pages,publish这个目标改成git push相关逻辑就行。有了Makefile之后,日常更新只需要make build本地预览、make publish一键上线,整个过程不会再让人心态烦躁,也不容易漏步骤。
5. 常见问题与排查技巧实录
5.1 环境与依赖问题速查
先说环境层面最容易碰到的三个问题。pip install caveman报权限错误,解决方案是加--user,或者干脆建一个虚拟环境,这是Python生态里的标准姿势。安装完成后运行cave提示命令找不到,检查~/.local/bin是否在PATH环境变量里,很多Linux发行版默认不会把用户级bin目录加进来。构建时遇到ImportError: No module named 'markdown',直接执行pip install markdown,或者重新运行一次完整安装,把依赖补全。
还有朋友问过Python版本过高会不会有兼容问题。我这个倒没怎么踩过,但是从源码安装如果报错,先去GitHub的issues里搜一下是否有对应的版本记录,基本能找到答案。这类问题不神秘,和常规Python工具链的坑都差不多。
5.2 模板渲染报错的经典案例
模板变量输出为空,这是所有Caveman新手的头号问题。最常见的原因是front matter字段名不一致,比如模板里写{{ post.tags }},文章里写的是tag,缺失和单复数拼写差异就会输出空白。Caveman遇到这种情况不会报错,只会让你看到空字符串,排查思路是先在模板里临时输出{{ post }},看看这个对象到底持有哪些字段,然后再逐一比对。
模板缩进导致的语法错误也很典型。由于模板直接执行Python,少写了一个{% endif %},或者{% for %}内部的HTML缩进和Python要求的缩进层级对不上,解释器就会抛IndentationError。报错信息有时候只给行号,不告诉你具体是哪个文件,遇到这种情况就逐个模板做二分排查,先注释一半代码,看报错是否还在,缩小范围到出错的那个模板。
中文乱码的问题同样常见,解决办法是确认三个地方都是UTF-8:内容文件保存编码、模板文件保存编码、HTML里<meta charset="utf-8">。三处缺一不可,最省心的做法是把编辑器默认编码设为UTF-8,之后就不再纠结。
5.3 构建与部署路径的坑
本地预览一切正常,部署上线后样式全部404,这种问题绝大多数出在base_url上。我把规则归纳成一句话:如果站点访问地址是https://example.com/blog/,那么所有CSS、JS、图片的引用路径都要加上/blog前缀,或者干脆用相对路径,而不是直接写/css/style.css。
首页打得开、文章页404,这种情况要检查文章URL的生成规则。如果你按日期组织了目录结构,比如/2025/06/hello.html,就要确认模板里拼出来的链接和实际生成的文件路径是一致的。Caveman默认的URL规则比较朴素,一旦自定义了目录结构,这里最容易对不上。
还有个特殊字符的问题值得一提。如果模板正文里出现了{{或者{%,引擎会尝试解析它,而不是把它当普通文字输出。我曾经写一篇介绍模板引擎的文章,正文里全都是{{ var }}示例,构建直接崩了。最终的解决方法是把这些符号拆开写,或者用HTML实体转义,让引擎别去解析。
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 变量输出空白 | front matter字段名不一致 | 打印{{ post }}检查字段结构 |
| IndentationError | 模板缩进或标签未闭合 | 检查{% if %}/{% for %}配对 |
| 中文乱码 | 文件编码不一致 | 统一UTF-8,外加meta标签 |
| 部署后样式404 | base_url路径没配好 | 调整base_url或改用相对路径 |
模板里{{被解析 | 未转义 | 拆开写法或用HTML实体 |
6. 我的实际工作流和后续打算
6.1 我现在写一篇博客的完整流程
现在的日常是:在content/里新建一个Markdown文件,写好front matter和正文,跑make serve在本地预览。排版确认没问题,直接make publish发布。整个过程不需要离开编辑器,也不需要打开任何后台面板。配图的文章我习惯把图片丢进content/images/,模板里用相对路径引用,部署的时候图片会跟着output/一起上传。这套流程跑顺之后,我基本上不再想框架本身的事,注意力全在写内容上。对我而言,这是静态博客最大的价值:工具不挡路,内容最优先。
6.2 我打算继续给Caveman补的能力
Caveman留给使用者的自定义空间非常大,我最近在计划给它加一个"最近更新"板块,在首页渲染最近5篇有过修改的文章;还打算给文章页加上一篇/下一篇导航,实现思路就是按日期排序之后,在当前page对象周围找相邻的两个文章对象。这些都是几十行代码能搞定的事,但Caveman不会替你写,必须自己动手。也正因为这种"什么都要自己来"的性格,用过Caveman之后,你会对自己站点的理解上升一个层次——你不再只是"套了个主题",而是清楚每一个页面从内容到HTML的完整流动过程。说实话,这个收获比博客本身的流量重要多了。