ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

caveman:用Go写的极简静态网站生成器,返璞归真的轻量建站体验

caveman:用Go写的极简静态网站生成器,返璞归真的轻量建站体验

前阵子给朋友的极简产品站做迁移,明明就是十几页文案加一个表单,结果我在建站工具的配置页里翻了一个多小时,只为了搞清楚“为什么默认字体在移动端会变细”和“导航栏那个滚动动画到底怎么关”。那会儿我就在想:我到底是来做内容的,还是来伺候工具的?

后来在某个仓库标签页里瞥见一个名字挺扎眼,caveman,直译过来就是“穴居人”。作者用它给一个静态网站生成器命名,理由也很直接:在大家把构建流程搞得越来越繁复的今天,做生成器的人反而决定开始做减法,一刀砍回原始时代。我用它重建了那个产品站,从下载到上线不到一个下午。这篇就来聊聊这个工具的原理、实操、部署,以及它适合谁、不适合谁。

1. 为什么一个静态站生成器要叫“caveman”——做减法的底层逻辑

先说清楚caveman是个什么东西。它本质上是一个用Go写的静态网站生成器,输入一堆HTML模板和内容文件,输出一个纯静态的站点目录,可以直接扔到Nginx、Caddy或者GitHub Pages上。这条路子本身不稀奇,Hugo、Jekyll都这么干。稀奇的是它把自己限制得很厉害:没有Node依赖、没有Ruby环境、没有预处理语言、没有一个庞大的插件市场,甚至配置项都少得可怜。

我一开始以为这种限制是“功能没做完”,用了一阵子才意识到,这是作者有意为之。caveman这个名字本身就是宣言:回到山洞里,用最原始的工具解决最核心的问题。什么最核心?对个人页面来说就是两件事:写内容、看效果。至于像自动合并资源指纹、按需加载组件、国际化路由这些能力,在作者看来属于“越来越复杂的工具链”的一部分,能砍就砍。这种设计取向跟不少开发者的习惯是冲突的,因为我们早就习惯了“框架帮我搞定一切”。

一个反直觉的点是:约束反而带来了某种自由。用Hugo那类工具时,官方主题深不见底,改一行样式可能牵连组件层;用caveman时,站点的表现层就是几个普通HTML文件,里面嵌着很朴素的模板变量,你可以直接查看浏览器渲染出来的结构,几乎不存在“黑盒”。这在调试上是句很大的话——你不会再遇到“这个样式为什么没生效”这类需要一路追到编译产物里找答案的情况,因为产物本身就是你的源码。

另外一个容易被忽略的面向是长期性。很多生成器绑定的生态会演进得很暴烈:上半年用版本A的语法写页面,下半年版本B换了配置格式,升级就是一场小型迁移。caveman很少在依赖上做文章,核心逻辑就是把模板和数据拼在一起,这个过程不太会受到互联网主流框架迭代的影响。换句话说,它不会因为某个上游插件停更就连累你的站点,这是“返祖”带来的天然抗风险能力。

我给朋友那个产品站做迁移时,把它之前纠缠不清的样式表全部清掉,重新手写了四个模板文件,一共不到200行。生成的整站打开,肉眼可见地干净利落。事实胜于雄辩,有些场景下,少即是多这句话不只是心灵鸡汤,它真的能降低你未来维护时可预期的成本。

2. 从零到上线:caveman的安装、目录结构和首个小站

2.1 安装方式和第一印象

caveman作为Go项目,发布物是一个单一二进制文件,这是它“洞穴级”简洁的第一步。安装路径我实测过两条。如果你本机有Go环境,一条go install命令就能搞定:

go install github.com/hagnerg/caveman@latest

不想装Go也没关系,直接从它的GitHub Releases页面下载对应平台的压缩包,解压后把可执行文件放到某个PATH目录就行。我当时的做法是放在~/bin下,然后用export PATH="$HOME/bin:$PATH"加进去,执行:

caveman --help

看到输出的命令列表那一刻我就大概明白了它的全貌——生成、初始化、构建,加上一个内置预览服务器,没有更花哨的子命令。如果你用过其他静态站生成器,这一步可能会恍惚一下:“就这?”没错,就这。

2.2 初始化一个项目的标准结构

初始化项目可以用caveman init,也可以直接自己手动创建目录。我建议还是走一下init,因为默认骨架会把模板目录、内容目录、公共资源目录一次性铺好,省得自己记结构。

一个典型的caveman项目布局大概长这样:

my-site/ ├── public/ # 构建产物输出目录 ├── src/ # 模板和内容所在目录 │ ├── static/ # 图片、CSS、JS这些静态资源 │ ├── templates/ # HTML模板 │ └── content/ # 页面和文章内容文件 └── caveman.yaml # 全局站点配置

你在源目录里维护页面内容,模板负责把内容包成完整的HTML。构建时caveman会读取全局配置、模板和内容,把它们合成到public/里,这样一个干净的静态站点就诞生了。

2.3 定义你的第一个页面

具体生成一个页面的流程,以我那个产品站为例。首先在caveman.yaml里写好站点的基本信息,通常就是站点标题、根域名、语言这些,字段名很直白,几乎不需要翻文档。然后进入src/content/,新建一个页面文件,比如index.md,文件顶部的元信息块,也就是front matter,长这样:

--- title: "首页" # 页面生成后的路径,默认会按照目录结构决定 --- 这里是首页的主要内容,支持用Markdown或纯HTML编写。

模板这边,在src/templates/下建index.html,里面就是一个普通HTML结构的骨架,需要插入内容的地方标注模板变量,类似这样:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>{{.Title}} | {{.Site.Title}}</title> </head> <body> <main> {{.Content}} </main> </body> </html>

我第一眼看到这种写法时,心里踏实了一半。它没有任何“魔法”——{{.Title}}就是把页面标题放进来,{{.Content}}就是把正文放进来。如果你之前用过Go的html/template,这语法几乎无缝衔接;就算你没用过,看多了也能猜到个七八。

2.4 本地预览和首次构建

内容写好后,启动本地预览非常直接:

caveman serve

执行后终端会打印一个本地地址,比如http://localhost:8080,浏览器打开就是站点效果。改完内容刷新页面就能看到变化,开发体验跟直接写静态HTML几乎一样,但多了一套模板变量和内容分离的便利性,这是这对组合最讨喜的地方。

正式构建更简单:

caveman build

完成后把public/目录里的文件全部扔到Web服务器根目录,站点就上线了。整个目录结构就是这么一层,没有隐藏的缓存目录,没有需要安装依赖的node_modules,也没有生成那堆你根本看不懂的中间文件。这个清晰度在我用过的同类工具里确实排得上号。

3. 模板与内容机制——它跟Hugo/Jekyll的底层逻辑差在哪

一个静态站点生成器好不好用,一半的分数都体现在模板和内容的配合方式上。caveman采用了Go语言自带的template引擎,这让它跟Hugo、Jekyll拉开了一个很明显的身位。

3.1 模板继承的取舍

先拿Hugo来说,它提供一整套主题系统,里面有baseof.html、partials、blocks,层次分明却也厚重。为了一个博客,你可能要先学习布局继承、自定义变量作用域、资源管道处理。Jekyll则是通过Liquid模板和一堆内置filter来渲染内容,同样有自己的学习曲线。caveman没有继承布局的概念,它把整个页面当作一个独立模板来写,公共部分——比如页头、页脚——会用include这类方式引进来。我自己的做法是维护好头部和尾部两个局部模板,然后在每个页面里显式引入,结构是横平竖直的,谁包含谁一目了然。

这带来的好处是:当你只改了首页排版样式时,不会因为一个全局布局的设变意外波及所有子页面。代价则是你得在多个模板文件里写重复的头部引用了,如果站点有七八种完全不同布局的大页面,这个手动重复确实比较啰嗦。但对于个人博客、产品介绍页这类页面形态相对固定的场景,这个代价几乎不存在。

3.2 内容循环和列表页的原始写法

比如要生成一个“所有文章列表”的索引页。在caveman里,你可以在模板中遍历内容集合:

{{range .Pages}} <article> <h2><a href="{{.Permalink}}">{{.Title}}</a></h2> <p>{{.Date.Format "2006-01-02"}}</p> <p>{{.Summary}}</p> </article> {{end}}

页面文件front matter里设置好标题、日期、摘要,构建时它们就变成一个个结构体的字段,模板里可以直接取。这个模型跟主流生成器是一致的,差别在于中间层的薄厚度。caveman的字段命名相对朴素,你能看到的就那么几个,理解成本会小很多。没有过多的默认分类体系,需要通过front matter自己约定标签和分组逻辑。

3.3 元数据的使用方式

内容分离还有一个收益:维护成本被压下来了。经常更新的是正文,模板保持不变。一个人维护一个多页面站点时,这种内容/设计分离的价值会被放大,因为你不需要为了换一句话而进到HTML的汪洋里找那一个节点。我当时就是靠着这个特性,把产品站的三个卖点段落拆分到不同的内容文件里,后面改价格、改联系方式、改FAQ,全都是打开对应文件改几行文字就完事,不用担心误伤版式。

3.4 静态资源和样式引用的处理

关于样式表和脚本的处理,caveman给了一套非常直接的做法:把它们原样拷进static/目录里,构建时直接复制到public/,不做合并压缩。首次看到时你可能会怀疑“这不太专业吧”,但仔细想想——现在很多站点还会用浏览器请求多个CSS文件,合并压缩带来的收益在HTTP/2并发环境下已经大不如前,反而会导致调试时需要面对一串乱码长名。对于小规模流量站点,源文件级别的CSS既直观又完全够用。我在实际部署中还特意连一个形如/assets/css/index.css?v=20250101的版本参数来对抗缓存问题,实现了预期的灵活度。

4. 部署实测:一台小内存VPS上的完整配置记录

4.1 构建产物和Web服务器配置

整个部署链路里最舒服的部分是,caveman的输出就是无状态的静态文件。我把它构建出来的目录直接放在一台内存只有512MB的轻型VPS上,配了一个Nginx。这个配置骨架几乎是通用模板,具体是这样:

server { listen 80; listen 443 ssl http2; server_name demo.example.com; root /var/www/my-site/public; index index.html; location / { try_files $uri $uri/ =404; } }

没有PHP进程,没有反向代理,也没有数据库连接,一个纯静态文件的页面在被访问时对服务器资源的占用小到可以忽略不计。用简单工具测过并发,在这样一台小内存VPS上撑住几百个并发轻量请求毫无压力。瓶颈完全在外网带宽,而不在应用本身。

4.2 用脚本打通过自动更新

日常更新内容的流程,我最终把它变成了一段短脚本。因为caveman的构建和部署动作天然适合被自动化,解决方案很稳定:本机执行构建,生成最新产物,再用rsync推到服务器。我的发布脚本大致是这个套路:

#!/bin/bash caveman build rsync -avz --delete public/ user@vps:/var/www/my-site/public/

推送完成即更新。第一次执行后,我就把“部署”这件事从记忆中删掉了,后续每次改完内容,运行一个命令就完成发布。这个流程跟那些带后台编辑器、Webhook的复杂方案没法比,但它也意味着你不需要为一个十几页的站点搭建一整套现代发布管道。

4.3 用CDN还是边缘防护的概念

至于要不要再接一层CDN,取决于你站点的读者分布。我个人测试阶段的经验是:小站直接暴露源站就够了,静态文件的缓存命中率天然很高,Nginx端给静态资源加上expires指令,效果立竿见影。远程服务器上简单测一下页面首字节响应,基本都在几十毫秒内。如果网站将来流量增长,再在前面挂一层CDN也很容易,因为是纯静态物,缓存策略几乎不用调整。

4.4 域名和HTTPS的处理

域名解析和HTTPS证书的处理,我用的是一套非常省心的方案:DNS解析到VPS的IP,然后通过Caddy来做自动HTTPS。这里也算我走的一个小弯路:我开始用的是Nginx加certbot,证书续期脚本手动设置了一个定时任务,结果后来去翻服务器时发现证书过期了一次,原因就是续期计时器忘了启动。换成Caddy以后,证书管理完全自动化,域名指向哪台机器,它就会自动签发并续期证书,少了一个需要人类定时记住的操作,对个人站来说省心不少。

5. 哪些项目适合用caveman,哪些场景该绕开它,以及我绕过的坑

5.1 它真正适合的土壤:个人博客和轻量站点

caveman到底适合什么场景?以我一个多月的实际使用体会来看,最适合它的有三类:

  • 个人博客和线上简历。这类站点内容单一,更新频率不高,不需要复杂的分类聚合,关键是要保证加载快、不宕机。
  • 产品的介绍页或落地页。页面数量通常十几到几十个,重点是文案和转化的视觉表现,用CMS完全是杀鸡用牛刀。
  • 临时活动页面。比如一场分享会的报名页、一次项目投票的展示页。做完上线、活动结束直接下架,不留历史包袱,维护成本低得可以忽略。

5.2 该绕开的场景:复杂需求就是硬边界

反过来,三种情况我会直接绕开它:

  • 需要多人协作更新内容的团队站点。没有可视化的后台,编辑者而是直接改内容文件,这会逼着你的团队全部掌握Git。非技术合作者来维护时,这套工具反而会变成内容生产的瓶颈。
  • 依赖复杂数据关系的博客系统。比如要按作者、按标签、按年份交叉检索文章,还要输出各种聚合页,caveman的原生能力有点吃力,最后多半会变成在模板里写一堆判定逻辑,维护不太优雅。
  • 需要用无头CMS或者数据库驱动的动态交互页面。静态生成的架构先天不适合;这时候你需要的是一套服务端程序,贸然选静态生成器,后续会处处受限。

这是我给出建议的核心:caveman不是取代Hugo或Jekyll的万能选择,它更像是一个回到起点的选项,适合那些对“一个页面就是一堆文件”这种朴素的部署模型有天然好感的技术从业者。

5.3 我踩过的坑:绝对路径缺失和排序问题

顺着使用过程说几个我踩过的实际坑。第一个是链接路径问题。刚开始我把内容里的内部链接写成相对路径,比如href="about.html",在某些深层目录页面里打开,浏览器解析的基准目录不一样,结果样式和链接全部失效。折腾了几分钟后,我把模板里所有资源的引用都改成从站点根目录出发的绝对路径,根路径变量很好用,形如/assets/css/style.css,同时内容页面里的站内跳转也全部套上根路径变量。改完以后,不管页面在哪一层目录,都稳得很。

第二个坑是文章列表的排序。caveman默认的列表排序方式和我预期的日期的倒序不太一样。我第一版博文列表输出的顺序是错乱的,后来才注意到需要在front matter里显式声明权重字段,再在模板循环前用排序函数处理。这个细节在你的文章数量超过十篇之后会变得格外明显,建议一开始就把权重或者日期统一规范好,省得将来返工。

第三个坑在标题的中文转义上。因为模板引擎对HTML做了转义处理,如果标题里携带中文冒号、引号这类符号,在布局里没有用安全文本的方式输出,页面显示就会多出乱七八糟的编码字符。这个防不胜防,最稳妥的做法是任何源于用户可控内容的文本,都要在模板里标记为安全的HTML文本来源,避免不必要的自动转义。

5.4 内容扩展方向

最后说一个我后来摸索出来的玩法:如果你想要一个“回复评论”的现代功能,不必换工具,可以直接接一个第三方评论服务。因为caveman生成的页面结构非常标准,在模板里嵌入一段评论系统的脚本,等于给静态页面插上了动态模块。同样的思路也可以用在站内搜索、浏览量统计、表单提交上。静态底子加第三方动态部件,这套组合既保住了部署的轻量,又补上了现代站点必备的交互能力。

一些没被写进官方文档的体会

这几周用下来,我最大的感受是caveman把“工具感”降得很低。它不需要你像运维一样操心进程,不需要你像前端工程化专家一样维护依赖,更不需要你在每次写文章前,先花十分钟去想“这个功能在这套框架里该怎么做”。它就是平铺直叙地告诉你:内容放这里,模板放那里,生成完事。

我个人在最开始搭建时曾试图把整个文件体系变得很“优雅”,比如加了一套分类标签的生成规则,结果内容不多时反而显得冗余。后来删掉一半设计,站点反而更清爽。这个经历也让我更笃定:在“洞穴人”的语境里,精准的克制本身就是一种价值——你需要什么就拿什么,它不会提前把整个工具箱都塞给你。

如果你手头恰好有一个很轻量的内容型站点,或者你被某套复杂建站系统折磨得怀疑人生,试试caveman不会吃亏。它的天花板确实不高,但在够用的范围内,那份一切尽在掌握的直觉,会是你愿意长期维护它的理由。端点踩过的坑记录在这,剩下的就交给你自己动手走一遭了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进