☰
caveman实战:用Django模板构建极简静态博客
2026/10/29 14:03:42 网站建设 项目流程

1. 为什么是caveman:一个“原始”博客框架的定位与核心逻辑

1.1 名字里的设计哲学

第一次看到“caveman”这个名字,我以为是某个复古游戏或者石器时代主题的营销项目。直到在一个技术社群里看到有人用它搭博客,我才意识到这其实是一个极简风格的静态网站生成器。它的核心思路很直接:不搞数据库、不搞后台管理、不搞复杂的插件体系,把内容写作回归到最原始的文件夹加模板文件。

这种“原始”不是简陋,而是一种刻意打磨后的克制。caveman 构建在 Django 的模板引擎之上,你写文章不是在编辑器里敲 Markdown,而是直接写 HTML 模板文件。这一个特性决定了它的受众画像:熟悉 Django 模板语法、想要一个轻量输出、又懒得折腾 Node.js 或 Go 工具链的人。

我最初看到这个框架时的反应是“有必要吗”,毕竟市面上的静态生成器一抓一大把。但在实际用了两个小项目之后,我反而理解了这种“穴居人”式的选择——当你的内容形态足够固定、页面结构足够简单时,背靠 Django 模板意味着继承、include、自定义 tag、模板变量这些能力开箱即用,不需要额外学一套新的模板语法。你只需要会 Django 的模板写法,整个生成逻辑对你就是透明的。

1.2 与传统静态站点生成器差异点在哪

把 caveman 和主流的 Hugo、Pelican、Jekyll 放在一起对比,能一眼看出它的定位非常特殊。Hugo 强在构建速度和主题生态,Pelican 强在 Python 原生和 Markdown 写作流程,Jekyll 强在 GitHub Pages 原生支持。这些工具都遵循一个模式:内容文件(Markdown)加主题模板,经过渲染引擎生成 HTML。

caveman 的逻辑走了一条不太一样的路。它的内容本身就是模板文件,你不需要“内容”和“展示”两层分离,而是两者合一。这样做的好处是自由度极高,你可以为每个页面写完全不同的布局结构,不需要照顾全局主题的抽象设计;坏处是一旦页面多了,重复的 HTML 结构会变多,必须靠继承和 include 自行约束。

这就像是从“装修好的精装房”回到“毛坯房自己动手刷墙”。精装房省事,但毛坯房能让每一个角落都符合自己的实际需求。caveman 适合的项目类型非常明确:个人博客、极客个人主页、内部文档站点、产品落地页。这些场景往往页面数量在几十个以内,结构简单,但需要高度定制化。

如果你要做一个几百篇文章的大型内容站、需要标签分类动态筛选、需要全文搜索,caveman 的“原始”模式会让你想砸键盘。它没有内置的 taxonomy 系统,没有动态分页组件,一切都要靠自己在生成逻辑里实现。所以选型时先掂量清楚体量和功能预期,不要盲目追求极简。

2. 快速上手:五分钟跑通一个最小站点

2.1 安装与最小化配置

caveman 的安装过程非常普通,直接通过 pip 拉到本地环境即可。我建议在任何项目里都先创建独立的虚拟环境,避免和系统 Python 环境的包互相污染,尤其是涉及 Django 版本依赖时。

mkdir caveman_demo cd caveman_demo python -m venv venv source venv/bin/activate pip install django caveman

装完后在项目根目录初始化一个 Django 工程。这一步可能会劝退一些习惯单文件工具的读者,但实际上只有两条命令:

django-admin startproject config .

接下来编辑config/settings.py,在INSTALLED_APPS里加一行:

INSTALLED_APPS = [ 'django.contrib.staticfiles', 'caveman', ]

然后添加 caveman 自己的配置块。我习惯把站点相关配置集中放到一个字典里,便于后续备份和迁移:

import os CAVEMAN = { 'SITE_ROOT': os.path.join(BASE_DIR, 'content'), 'BUILD_ROOT': os.path.join(BASE_DIR, 'build'), 'BASE_TEMPLATE': 'base.html', 'GENERATORS': [ ('pages', 'caveman.generators.pages'), ], }

这里的SITE_ROOT是内容源目录,BUILD_ROOT是最终生成静态文件的输出目录。GENERATORS是构建流程中要执行的页面生成器,后面会详细说。先记住一个原则:caveman 把“页面定义”和“输出位置”看成两件事,内容文件在源目录怎么组织,构建后就基本怎么映射到输出目录。

2.2 内容目录与基础模板搭建

在项目根目录创建content目录,然后新建一个base.html作为全局母版。这里我直接用 Django 的模板继承语法:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}{% endblock %}</title> <link rel="stylesheet" href="/static/style.css"> </head> <body> <header> <a href="/">我的极简博客</a> </header> <main> {% block content %}{% endblock %} </main> <footer> <p>Powered by caveman</p> </footer> </body> </html>

注意我引用 css 时用了绝对路径/static/style.css。静态资源的处理有一个关键点:caveman 构建时默认不会自动拷贝你项目里的静态文件到输出目录。所以我在content下面单独建了static文件夹,然后在部署阶段用 Nginx 直接把这个文件夹指到站点的静态资源路径上。这种方式比折腾 caveman 的静态文件收集机制要省心得多。

接着在content目录下创建一个首页文件index.html:

{% extends "base.html" %} {% block title %}首页{% endblock %} {% block content %} <h1>你好,洞穴</h1> <p>这是一个用 caveman 生成的静态页面。</p> {% endblock %}

2.3 本地预览和构建输出

caveman 提供了一个内置的开发服务器,和 Django 的runserver类似,会在本地起一个服务让你检查渲染效果:

python manage.py caveman serve

浏览器打开http://127.0.0.1:8000就能看到首页。开发模式下改了模板文件会实时刷新,这个体验和 Django 开发基本一致。

确认效果没问题后,构建正式静态文件:

python manage.py caveman build

构建完成后,build目录下会生成对应的index.html。用命令看一下输出结构:

find build -type f

你会看到build/index.html。如果之前建了content/about.html,构建后会是build/about/index.html。这个 URL 映射规则是 caveman 比较关键的机制,后面的章节专门展开讲。

3. 核心机制拆解:从模板文件到静态页面的转换链路

3.1 URL 与文件路径的映射规则

对一个静态站点生成器来说,最核心的问题就是:源文件如何变成最终浏览器访问的 URL。不同的工具映射规则不同,Hugo 支持丰富的 frontmatter 控制,Jekyll 大致按文件名和目录结构生成固定路径。caveman 的规则极其朴素,只有一条主逻辑:content下的模板文件名和目录层级,直接决定输出路径。

我们可以把它理解为“一个模板文件对应一个展示页面”。比如:

  • content/index.html构建后就是站点的根路径/,实际文件是build/index.html
  • content/about.html构建后是/about/,实际文件是build/about/index.html
  • content/blog/first.html构建后是/blog/first/,实际文件是build/blog/first/index.html

这种“把.html文件名转换为目录加index.html”的处理方式,让我们不需要额外的路由配置文件就能获得干净的 URL 结构。它天然避免了.html后缀暴露在浏览器地址栏,也让 Nginx 部署时不需要任何 rewrite 规则。

我在实际使用时发现,这种映射规则有一个需要注意的地方:你想生成一个纯文件资源、而不是一个 HTML 页面时,不能直接把文件叫.html或放在会被当作模板的目录下。比如放一张图片、一个 favicon,应该单独放到静态资源目录,否则构建时会被奇怪地当成页面处理。

3.2 生成器如何注入动态数据

模板文件解决了“长什么样”的问题,但博客首页通常要展示文章列表,架构页面要展示项目卡片,这些数据从哪来?caveman 的答案是生成器(Generators)。

从我的理解角度,生成器是一个“在构建时运行的 Python 函数”。它在每次构建页面之前被调用,负责把动态数据塞进模板上下文,模板里用变量渲染这些数据。这种设计其实和 Django 的 view 逻辑如出一辙,只是没有 HTTP 请求和响应过程,纯粹是静态生成。

先看一个最基础的例子。假设我们要给首页注入一个当前日期的变量:

import datetime def inject_date(): return { 'now': datetime.date.today(), }

然后把这个函数配置到CAVEMAN的GENERATORS里:

CAVEMAN = { 'GENERATORS': [ ('pages', 'caveman.generators.pages'), ('date', 'myapp.generators.inject_date'), ], }

这是我一直推荐的命名方式:生成器字段名就是模板中可用的变量前缀。上面的'date'会让模板里出现{{ date.now }}。

生成器函数不仅限于返回一个普通字典。它还可以拿站点配置、扫描源目录、解析文件内容,做更复杂的聚合操作。比如实现一个标记了tag: python文章自动归档的功能:

import os import re def scan_articles(site_root): articles = [] for root, dirs, files in os.walk(site_root): for name in files: if not name.endswith('.html'): continue path = os.path.join(root, name) content = open(path, encoding='utf-8').read() match = re.search(r'tag: (\w+)', content) if match: articles.append({ 'title': re.search(r'<h1>(.*?)</h1>', content).group(1), 'path': os.path.relpath(path, site_root), 'tag': match.group(1), }) return articles

然后配合模板循环,就能在归档页面把文章列表渲染出来。当然,真正的生产环境要处理 HTML 转义、相对链接等细节,不过从这里可以看出生成器这个机制的上限很高。它本质上允许你“在模板里写逻辑,在 Python 里写数据准备”,动静结合,灵活度远大于纯模板工具。

3.3 模板继承和组件化复用

说完动态数据,再讲静态结构。Django 模板最强大的能力就是继承和 include。caveman 完整保留了这一点,使得站点的公共头尾、侧边栏、卡片组件可以被抽离复用。

我的习惯是把复用片段放到content目录下的partials文件夹,并用前缀区分职责:

  • partials/header.html:站点导航栏
  • partials/sidebar.html:侧边栏模块
  • partials/article_card.html:文章卡片

然后在页面中引入:

{% include "partials/article_card.html" %}

配合with参数传递变量:

{% include "partials/article_card.html" with article=item %}

这种组件化写法的收益在页面数量增多后会非常明显。维护一套公共组件的样式和结构,远比复制粘贴二十个页面要好。尤其是调整间距、修改标签样式时,改一处即可生效,不会漏改。

不过我踩过一个大坑:caveman 的模板加载顺序和普通 Django 略有差异。它默认从SITE_ROOT下查找模板,也就是说如果你写了{% extends "base.html" %},这个base.html必须能在content目录或 Django 的模板目录中找到。我一开始把base.html放在了项目根目录,导致所有页面报模板找不到错误。解决办法很简单,把母版模板也放进content目录即可。这类细节官方文档写得不显眼,很容易被忽略。

4. 进阶实操:用 caveman 搭一个带标签归档的极简博客

4.1 页面结构规划和导航设计

理论讲完,现在完整走一遍搭建流程。我们要做一个功能完整的极简博客,包含首页、文章详情页、标签聚合页和关于页面。结构如下:

content/ ├── base.html ├── index.html ├── about.html ├── partials/ │ ├── header.html │ └── article_card.html ├── blog/ │ ├── first-post.html │ ├── second-post.html │ └── tags.html └── static/ ├── style.css └── favicon.ico

导航栏每个页面都要展示,所以抽到partials/header.html:

<nav> <a href="/">首页</a> <a href="/blog/first-post/">文章</a> <a href="/about/">关于</a> </nav>

在base.html中通过 include 引入:

{% include "partials/header.html" %}

4.2 标签聚合与文章列表的实现

标签页是最能体现生成器价值的地方。我们在content/blog/tags.html里放一个空壳模板,预留循环结构:

{% extends "base.html" %} {% block title %}标签归档{% endblock %} {% block content %} <h1>标签归档</h1> <ul> {% for tag, article_list in tags.items %} <li> <h2>{{ tag }}</h2> <ul> {% for article in article_list %} <li><a href="{{ article.url }}">{{ article.title }}</a></li> {% endfor %} </ul> </li> {% endfor %} </ul> {% endblock %}

接下来在myapp/generators.py里写一个标签聚合生成器。它要做三件事:扫描源目录下所有 HTML 文件、从内容里提取标签信息、组装成以标签为键的文章列表字典。

import os from collections import defaultdict def build_tag_archive(site_root): tags = defaultdict(list) for root, _, files in os.walk(site_root): for name in files: if not name.endswith('.html'): continue path = os.path.join(root, name) with open(path, encoding='utf-8') as f: content = f.read() tag_match = re.search(r'<!-- tags: (.*?) -->', content) if not tag_match: continue tag_list = [t.strip() for t in tag_match.group(1).split(',')] title_match = re.search(r'<title>(.*?)</title>', content) article_rel = os.path.relpath(path, site_root).replace(os.sep, '/') article_url = '/' + os.path.splitext(article_rel)[0] + '/' for tag in tag_list: tags[tag].append({ 'title': title_match.group(1) if title_match else name, 'url': article_url, }) return dict(tags)

把这个生成器挂到配置里:

CAVEMAN = { 'GENERATORS': [ ('pages', 'caveman.generators.pages'), ('tags', 'myapp.generators.build_tag_archive'), ], }

然后在文章页面里通过 HTML 注释标记标签:

<!-- tags: django, python --> {% extends "base.html" %} ...

这种“HTML 注释作为元数据”的做法是受 caveman 极简哲学启发的妥协方案。它不需要 frontmatter 解析逻辑,也不会影响浏览器渲染,但能被生成器扫描读取。用起来清爽,比引入 YAML 解析省事很多。

4.3 部署到 Nginx 的完整步骤

全部页面构建完成后,部署反而成了最简单的环节。caveman 输出的是纯静态文件,不依赖 Python 进程,所以 Nginx 配置和普通静态站点没有区别。

先把构建产物同步到服务器:

rsync -avz --delete build/ user@server:/var/www/caveman_site/

然后配置一个 Nginx server 块:

server { listen 80; server_name example.com; root /var/www/caveman_site; index index.html; location /static/ { alias /var/www/caveman_site/static/; } location / { try_files $uri $uri/ $uri/index.html =404; } }

这段配置里的try_files非常关键。因为 caveman 生成的 URL 没有.html后缀,浏览器访问/blog/first-post/时,Nginx 需要先查找/var/www/caveman_site/blog/first-post/目录,再回退到目录下的index.html。try_files $uri $uri/ $uri/index.html正好覆盖了这三种情况。

我实际部署时一开始漏了$uri/index.html这一段,结果访问/blog/first-post/返回 404,而直接访问/blog/first-post/index.html又正常。排查了半天才意识到是 try_files 写得不完整。如果你遇到类似问题,先检查 Nginx 的这个配置项。

5. 实战排查:我踩过的坑与高频问题速查

5.1 模板继承失效与相对路径陷阱

使用 caveman 时最容易出问题的就是模板继承。症状非常典型:页面内容空白、样式丢失、或者报出TemplateDoesNotExist错误。

这类问题九成是模板查找路径配置错误。caveman 的模板查找机制和 Django 标准机制有个差异:它把SITE_ROOT当作优先查找目录。也就是说,你的base.html必须放在content目录下,而不是 Django 项目的templates目录里。

我当初把base.html放到项目根目录下一个叫templates的文件夹,结果构建时提示找不到母版模板。后来把文件移动到了content/base.html,页面立刻正常。这个折腾过程让我意识到,读文档时不能想当然地认为“所有静态生成器都是同一套逻辑”。

相对路径也是一个坑。如果你在一个子目录页面里引用了../static/style.css,构建后生成的页面在/blog/first-post/下,浏览器解析相对路径时会去找/blog/first-post/../static/style.css,最终变成/blog/static/style.css,而不是/static/style.css。解决办法是始终在模板里使用以/开头的绝对路径。

5.2 页面死活不更新的缓存问题

开发模式下改了模板内容,刷新浏览器发现页面还是旧的。这个问题典型到几乎每个用 caveman 的人都会遇到。

caveman 的serve命令并不是每次都全量重新构建所有页面,它有简单的缓存机制,在文件变更时才触发重新渲染。如果你的模板文件被修改了但关联的页面没变,缓存可能没有命中,导致页面保持旧版本。

我的排查流程是这样:

# 先删除 build 目录强制全量重建 rm -rf build python manage.py caveman build

全量构建后如果页面更新了,说明是缓存问题而非代码问题。如果全量构建后仍然旧内容,就要检查是不是浏览器缓存了,按Ctrl + F5强制刷新看看。

开发过程中建议把它当成纪律来执行:写完内容先构建再看,不要过度依赖serve的自动刷新。

5.3 其他高频问题与排查要领

下面是我使用过程中收集到的高频问题汇总,整理成表格方便对照:

问题现象可能原因解决思路
中文乱码模板文件编码不是 UTF-8统一用 UTF-8 编码保存源文件
文章列表里有重复项生成器扫描了构建目录build扫描时排除BUILD_ROOT路径
页面没有样式静态资源路径用了相对路径改为/static/绝对路径引用
标签页是空的元数据注释格式写错检查是否写了<!-- tags: ... -->且逗号为英文
构建报模板不能继承base.html不在SITE_ROOT移动到content目录下
首页点击文章跳 404Nginxtry_files配置不完整加上$uri/index.html回退

遇到问题时建议按这个思路排查:先确认源文件内容正确,再检查生成器的扫描逻辑,最后看部署层的路径配置。绝大多数情况下问题出在这三层中的某一个位置,很少存在多个问题同时爆发的场景。

如果你准备把旧站点迁移到 caveman,我最后再给一条经验:先写一个脚本扫描旧站所有文章的标题、链接、发布时间,输出 CSV 清单,再批量生成对应的模板文件。迁移之前先理顺内容架构,远比迁移过程中反复修补更高效。这个习惯帮我节省了大量重复劳动,也让你在动手迁移前对内容资产有一个整体把握。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询