
第一次看到caveman这个名字我以为是某个洞穴生存游戏的角色设定。点开项目仓库才发现它其实是一个用 Go 写的极简静态博客生成器没有数据库、没有后台、没有几十个配置开关一条命令就能把 Markdown 变成整站 HTML。我大概用了十分钟就喜欢上这种“原始但够用”的调性。这篇文章想把 caveman 从安装到自动化部署完整讲一遍适合那些觉得 Hugo、Hexo 太大太重、只想安静写稿并控制发布流程的人。1. caveman到底是个什么一个把所有依赖都砍掉的博客生成器1.1 为什么叫“洞穴人”名字本身就是态度。Hugo 这类工具功能强大但伴随而来的是主题生态、内嵌模板语言、几十种输出格式和几乎永远调不完的配置。caveman 的定位恰好相反它要求你把所有精力放在 Markdown 内容上构建只是“扔石头一样简单”的机械动作。作者用 cavity / cave 这个意象提醒你一个好的写作环境应该像洞穴一样安静、封闭、没有多余装饰。我把它用在实际项目里的感受是caveman 并不试图取悦所有人它默认你懂一点命令行懂一点 HTML 模板也愿意自己处理 CSS。这反而成了它的优势——不会给你塞进一大堆用不到的抽象层。打开生成后的 public 目录里面每个 HTML 文件都是干净的、可以被任何静态服务器直接托管的产物。1.2 核心特性与设计哲学caveman 的定位非常清晰核心特性可以用下面这张表格概括特性说明对比 Hugo / Hexo 的差异单二进制发行安装后只有一个可执行文件无运行时依赖不需要 Node.js也不需要装一堆 npm 包Markdown 直出用 front matter 写元信息内容就是纯 .md 文件少了概念层学习成本低Go 原生模板模板系统基于标准库 html/template变量少、逻辑少但够用全静态输出生成 HTML/CSS/JS/RSS不依赖后端部署到任意静态托管都行极快构建几百篇文章构建耗时几百毫秒增量渲染做得不错后面细说设计哲学说白了就是“少即是多”。它没有在线编辑器没有草稿箱没有自动目录树。你写稿、构建、上传三步结束。如果你喜欢的是 Notion 那种所见即所得caveman 不适合你如果你希望写作环境回到“一个编辑器 一个文件夹”caveman 的体验相当顺滑。1.3 什么场景下我会推荐 caveman接触下来我觉得 caveman 最适合三类人个人博客维护者文章更新不频繁需要免费的静态托管想完全掌控最终 HTML。技术文档写作者仓库里已经有大量 Markdown想快速生成一套可检索的文档站点而不想引入 VuePress / Docusaurus 那一套复杂依赖。喜欢折腾主题的人caveman 的模板只有一层改主题本质是改 HTML 和 CSS比改 Hugo 的嵌套模板轻松太多。反过来如果网站有一千篇文章、几十种内容类型、复杂权限管理、多语言切换那 caveman 就不合适。它不是全能框架而是为“写作”这个单一目标服务的工具。2. 5分钟跑通第一个caveman站点安装、初始化、出第一篇文章2.1 安装从 release 二进制到 go install安装 caveman 最省事的方式是去 GitHub Releases 页面下载对应平台的压缩包。比如在 Linux 服务器上我通常这样操作wget https://github.com/cavemanrepo/caveman/releases/download/v0.9.0/caveman_0.9.0_linux_amd64.tar.gz tar -xzf caveman_0.9.0_linux_amd64.tar.gz sudo mv caveman /usr/local/bin/ caveman version如果你本地有 Go 环境也可以直接go install github.com/cavemanrepo/cavemanlatestgo install方式的好处是能保证二进制与最新源码同步缺点是需要等 Go 编译器跑一会儿。对于只偶尔更新的工具来说我倾向于直接下载 release因为不用在机器上维护 Go toolchain。提示在 macOS 上如果遇到“已损坏无法打开”的提示通常是因为没有设置执行权限或 Gatekeeper 拦截先执行chmod x再右键打开一次即可。2.2 初始化站点与目录结构caveman 没有交互式初始化向导参数化命令反而更利落。我一般这样新建站点caveman new my-site cd my-site caveman build执行完caveman new它会生成下面这个标准结构my-site/ ├── caveman.toml ├── content/ │ └── posts/ ├── templates/ │ ├── index.html │ ├── post.html │ ├── tag.html │ └── archive.html ├── static/ │ ├── css/ │ │ └── style.css │ └── img/ └── public/看到这个结构你应该能理解 caveman 为什么简单内容放 content模板放 templates静态资源放 static产物放 public。没有额外概念全部与现实文件夹一一对应。2.3 写第一篇文章并预览在content/posts/下新建hello-caveman.md--- title: 你好caveman date: 2025-01-15T10:00:0008:00 tags: [hello, meta] summary: 这是用 caveman 写的第一篇文章。 --- ## 一个简单的开始 如果一切顺利这句话会出现在我的博客首页。然后构建并预览caveman build cd public python3 -m http.server 8080打开浏览器访问http://localhost:8080你会看到本站的 index.html 已经渲染完成。构建过程非常快我第一次跑的时候甚至以为是缓存因为根本来不及读秒。如果想要开发时实时刷新可以开两个终端一个运行caveman build --watch一个运行本地静态服务器改完 Markdown 保存后刷一下页面就能看到效果。3. 目录、配置和模板撑起整个博客的三条腿3.1 caveman.toml 配置项拆解caveman 的所有站点配置都收敛在一个 TOML 文件里。下面是我用得最全的一份示例base_url https://example.com title Caveman 洞穴笔记 language zh-CN theme default description 一个安静的技术博客 default_ext .html [author] name Tom email tomexample.com [taxonomies] tag tags [params] github https://github.com/tom rss true paginate 10逐个解释关键项base_url生成 canonical 链接和 RSS 绝对地址时必需不要留空。部署到子路径时这里要带上子路径。default_ext控制生成的内链后缀默认.html。如果你的静态托管支持干净 URL可以改成空字符串但绝大多数情况保持.html最稳妥。[taxonomies]告诉 caveman 你有哪些分类维度。很多模板默认只写tag如果你要加series系列文章就补充一行series series。paginate首页每页展示篇数。这个配置决定后续模板里Paginator的行为。我不建议一上来配太多参数。先保持默认值跑通再按需开启 RSS、分页、数学公式等能力否则你会在还没写几篇文章时就被配置淹没。3.2 模板系统只有一层继承caveman 的模板继承非常朴素templates/base.html定义页面的公共骨架其他模板通过{{block content .}}和{{end}}占位。默认主题一般会有一个base.html!DOCTYPE html html langzh-CN head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title{{.Title}} · {{.Site.Title}}/title link relstylesheet href/static/css/style.css /head body header h1a href{{.Site.BaseURL}}{{.Site.Title}}/a/h1 /header main {{block content .}}{{end}} /main footer pPowered by caveman/p /footer /body /html注意变量的约定{{.Site.*}}是全局站点配置{{.Title}}、{{.Date}}等是当前页面变量。比如post.html只关心单篇文章index.html只关心文章列表。这种“单层模板”的设计让新手上手非常快。3.3 首页列表、分页与标签归档首页模板index.html的核心逻辑通常是{{define content}} ul classpost-list {{range .Pages}} li a href{{.Permalink}}{{.Title}}/a time datetime{{.Date}}{{.Date.Format 2006-01-02}}/time p{{.Summary}}/p /li {{end}} /ul !-- 分页 -- {{if .Paginator}} div classpagination {{if .Paginator.HasPrev}}a href{{.Paginator.PrevURL}}上一页/a{{end}} span第 {{.Paginator.PageNumber}} / {{.Paginator.TotalPages}} 页/span {{if .Paginator.HasNext}}a href{{.Paginator.NextURL}}下一页/a{{end}} /div {{end}} {{end}}而标签归档页只需要按tag过滤页面生成一个小列表即可。这里最容易被忽视的是Date.Format 2006-01-02里的2006是 Go 的出生年参考格式不是随便写的年份。我第一次写成了yyyy-MM-dd结果页面上一片空白这个问题在第六部分会再讲。4. 自定义主题实战用一套暗色模板替换默认皮肤4.1 主题目录约定caveman 的主题机制不复杂templates文件夹就是一个主题把它复制一份改名就是新皮肤。比如我想做一套叫dark的暗色主题cd my-site cp -r templates templates-dark然后修改caveman.toml里的theme dark实际上 caveman 的theme字段一般指向themes/下的目录。更规范的做法是mkdir -p themes/dark cp templates/* themes/dark/站点根目录的templates/可以看作是内置默认主题正式自定义时把文件放到themes/你的主题名/下并在配置里写theme 你的主题名。这样多个主题可以共存切换时不用覆盖文件。4.2 实现一套暗色主题的核心步骤暗色主题的本质是改 CSS 变量和控制文字对比度。我一般会在themes/dark/static/css/style.css里定义:root { --bg: #1e1e1e; --text: #d4d4d4; --link: #8ab4f8; --border: #333333; --code-bg: #2d2d2d; } body { background: var(--bg); color: var(--text); line-height: 1.8; max-width: 720px; margin: 0 auto; padding: 2rem; } a { color: var(--link); } pre { background: var(--code-bg); padding: 1rem; overflow-x: auto; }然后把base.html里的样式表路径改成/static/css/style.css。注意 caveman 的静态资源最终会按原样拷贝到public/static/下所以路径前面要带/static不要只写css/style.css否则部署到子目录后会出现样式丢失。4.3 让标题、摘要与页脚全部可配置很多人以为自定义主题只是换颜色其实把变量抽出来才是高效方案。我喜欢在caveman.toml的[params]里加自定义字段然后在模板里直接引用[params] footer_text 写于洞穴深处 show_reading_time true模板里这样用footer p{{ if .Site.Params.footer_text }}{{.Site.Params.footer_text}}{{ else }}Powered by caveman{{ end }}/p /footer以及阅读时间p预计阅读 {{div (len .Content) 400}} 分钟/p这里的div是 Go 模板内置函数len .Content返回文章字符数除以 400 是粗略的阅读速度。通过这样把信息抽到配置中以后换主题时不需要改动模板逻辑只需要改 TOML。5. 自动化发布让 Markdown 到线上只隔一次 push5.1 用 GitHub Actions 一键构建部署静态博客最好的发布方式就是 Git 工作流。我在项目根目录建.github/workflows/deploy.ymlname: build-and-deploy on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Setup caveman run: | wget -qO caveman.tar.gz https://github.com/cavemanrepo/caveman/releases/download/v0.9.0/caveman_0.9.0_linux_amd64.tar.gz tar -xzf caveman.tar.gz sudo mv caveman /usr/local/bin/ - name: Build run: caveman build - name: Deploy to Pages uses: peaceiris/actions-gh-pagesv4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./public这个流程的意图很明确每次 push 新文章服务器会自动构建 public 目录并发布到 Pages。整个流程里没有数据库迁移、没有环境变量、没有一堆npm install唯一要保证的是构建环境能访问 GitHub Release 下载二进制。5.2 增量渲染与缓存策略caveman 的增量渲染是我比较喜欢的一点。它会在项目根目录生成一个.caveman-cache文件记录每篇文章的哈希值。构建时如果文件没变就直接跳过渲染只有新增或修改过的文章才重新生成。实测下几百篇文章的站点第二次构建通常只有几十毫秒到一两百毫秒。这对日常写作非常重要你不需要每次保存后都全量构建也不会有“改了老文章但首页没更新”的困惑。caveman 会自行比对缓存。如果你希望强制全量构建可以先用caveman clean清掉缓存再执行caveman build。注意如果你把博客仓库和构建产物仓库分开管理务必将.caveman-cache加入.gitignore或存放在源仓库内。否则部署机上每次都是全新环境不能用增量能力。5.3 部署到自己的服务器如果你有自己的云服务器不用 GitHub Pages那流程更简单。在本地构建后caveman build rsync -avz --delete public/ userserver:/var/www/blog/--delete会删除服务器上多余的旧文件保证线上和本地完全一致。这一步看似简单但很多多人协作项目里都会忘记清理过期 HTML导致旧页面残留。使用 rsync 的删除语义就能避免这种问题。6. 踩坑实录caveman使用中常见的5个坑与排查方法6.1 日期格式解析失败页面直接不渲染在 front matter 里写date: 2025-01-15时caveman 对这种短日期格式的容忍度并不高。内部 Go 解析器默认期望 RFC3339 格式比如2025-01-15T10:00:0008:00。如果你只写日期部分版本会解析失败导致文章被跳过甚至在构建时直接报错。我的建议是一律写成date: 2025-01-15T10:00:0008:00如果嫌麻烦也可以在caveman.toml中配置date_format并统一规范但这依赖具体版本支持。最稳妥的还是在 Markdown 里写全一劳永逸。6.2 中文文件名与 URL 编码问题我曾经把文章命名为2025-01-15-你好世界.md构建后生成的 URL 变成了https://example.com/posts/2025-01-15-%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C.html。虽然浏览器能打开但分享时链接极长一些外部站点解析也不友好。caveman 不会自动把中文文件名转成 slug。最佳实践是文件名使用英文短横线连接标题写在 front matter 的title字段。比如文件叫hello-caveman.mdtitle: 你好caveman这样 URL 清爽页面标题也正常。6.3 模板变量明明写对了却显示为空模板变量为空最常见的原因是没有在 front matter 中定义对应字段。比如模板里写{{.Summary}}但文章没有summary渲染出来就是空字符串。这不算 bug但坑就在于有时候字段在模板里也能引用就是不显示因为变量名拼错了。排查方法很简单用caveman build --verbose查看生成的中间渲染信息或者直接影响在 post.html 里临时加上{{printf %#v .}}然后构建看输出把所有可用字段列出来。这个调试技巧在 Go 模板里非常有效。6.4 构建产物里混入 .DS_Store 与临时文件macOS 用户应该遇到过date 明明没改但 public 目录里突然出现.DS_Store。caveman 在拷贝 static 目录时默认会原样复制所以本地的隐藏文件也会被带进产物。这会导致 Git 仓库里多出一堆二进制垃圾。解决方法是全局 Git ignoreecho .DS_Store .gitignore或者在构建前用脚本清理find static -name .DS_Store -delete如果希望构建环境彻底干净可以在 GitHub Actions 的 build 步骤中加入同样的清理命令。6.5 修改主题后浏览器缓存不刷新caveman 生成的 CSS 文件名不会带哈希值所以如果你的浏览器缓存较狠更新主题后会发现样式没变化。最简单粗暴的方法是强制刷新但访客看不到更新。更工程化的做法是给样式文件加版本参数。比如模板里这样写link relstylesheet href/static/css/style.css?v{{.Site.Params.css_version}}然后在caveman.toml里维护一个css_version 20250115每次改样式就把版本号加一。这样既不需要破环缓存也不需要引入构建哈希工具操作直接有效。结尾一个“原始工具”给我带来的效率提升用 caveman 这半年我最大的感受是工具可以被快速替换但思路能留下。它让我重新审视博客系统到底需要什么。很多人过度设计博客其实核心只有两件事持续用 Markdown 写内容以及把内容稳定地发布出去。其余功能都可以用外部服务补上——评论用 Disqus统计用网站计数器搜索可以用静态索引脚本。如果你的博客栈已经复杂到需要写“部署配置说明”那我建议你试试 caveman 的极简路径。直接把所有文件交给 rsync 或 GitHub Actions线上照样稳定运行。反正对我来说回到“洞穴”里写作比维护一个大型框架快乐得多。