验证说明(读前必看):本文技术细节于 2026-07-30 对照官方文档 docs.halo.run(版本 2.24)及一篇 2026 年官方示例整理。Halo 迭代较快,落地前请以你实际安装版本的官方文档为准。
版本红线:本文以 Halo 2.x 为准。Halo 1.x 的模板引擎是 FreeMarker(.ftl 文件),与 2.x 的 Thymeleaf(.html 文件)不兼容——1.x 用户请勿直接照抄第 4 章代码。
写作状态:advisory。配置字段、全局变量已对照官方 2.24 文档;模板引擎与静态资源引用写法建议落地前再核一次你所用版本。


用 Halo 建站很爽,但套用现成主题总差点意思:想要的首页布局没有,配色改不动,作者信息块位置别扭。这时候与其继续找主题,不如自己做一个。本文带你从零做出一个能安装的 Halo 主题——不假设你精通 Java,只需要会写 HTML/CSS,并愿意看一点模板语法。读完你不仅能做出第一个主题,还会明白 Halo 到底是怎么把“主题文件”变成用户看到的页面的。

1. 先搞清 Halo 主题到底是什么

一句话:Halo 主题就是一个文件夹,里面放着一份“身份证”(theme.yaml)、若干模板文件和静态资源。Halo 启动时读取这个文件夹,根据你访问的页面(首页 / 文章页 / 分类页),挑对应的模板文件,把数据填进去,渲染成 HTML 发给浏览器。

两个关键角色你需要记住:

  • 全局变量:Halo 在渲染时自动塞进模板的“现成数据”,比如站点标题 site.title、当前主题信息 theme。不用你查数据库,直接用。
  • Finder API:一组查询函数,比如 postFinder(查文章)、categoryFinder(查分类)。你想“取某分类下的前 5 篇文章”,就调它。

理解了“主题 = 文件夹 + 声明文件 + 模板(吃变量、调 API)”,后面的事就都是填空题了。

2. 开发前准备

确定主题放哪。 Halo 2.x 的主题住在运行目录的 themes/ 下。如果你用 Docker 启动(官方推荐),数据卷通常是 ~/.halo2,那么主题路径就是:

~/.halo2/themes/你的主题名/

直接在这个目录下建文件夹,Halo 后台就能“看到”它。

开实时预览。 开发时最怕“改一下刷新没反应”。Docker 运行请加环境变量 SPRING_THYMELEAF_CACHE=false 关掉模板缓存;用源码跑则在配置里加 spring.thymeleaf.cache: false。这样改完模板刷新即可生效。

编辑器与起点。 用 VS Code 即可。强烈建议别从空文件夹开始——官方提供了起步模板,省去目录约定的踩坑:

  • halo-sigs/theme-starter:最基础的主题模板,含标准目录结构。
  • halo-sigs/theme-vite-starter:集成了 Vite,适合想用现代前端构建流程的人。

先把 theme-starter 克隆下来跑通,再在上面改,效率最高。

3. 主题骨架与必需文件

一个最小可加载的主题,根目录必须有 theme.yaml。下面是一个完整可直接用的示例(字段含义见注释):

apiVersion: theme.halo.run/v1alpha1
kind: Theme
metadata:
  name: theme-foo        # 必填,且必须与主题文件夹名一致,否则资源可能加载异常
spec:
  displayName: 示例主题   # 必填,后台显示的名称
  author:
    name: halo-dev
    website: https://halo.run
  description: 一个示例主题
  homepage: https://github.com/halo-sigs/theme-foo
  repo: https://github.com/halo-sigs/theme-foo.git
  issues: https://github.com/halo-sigs/theme-foo/issues
  settingName: "theme-foo-setting"      # 可选,配合 settings.yaml 做后台配置
  configMapName: "theme-foo-configMap"  # 可选,配置持久化名
  customTemplates:                       # 可选,注册“自定义模板”供后台选用
    post:
      - name: 文档
        description: 文档类型的文章
        screenshot:
          file: post_documentation.html
    page:
      - name: 关于
        description: 关于页面
        screenshot:
          file: page_about.html
  version: 1.0.0          # 必填,主题版本
  requires: 2.0.0         # 必填,要求的最低 Halo 版本
  license:
    - name: "GPL-3.0"
      url: "https://github.com/halo-sigs/theme-foo/blob/main/LICENSE"

必填四项metadata.namespec.displayNamespec.versionspec.requires。其余都是锦上添花。

推荐目录结构(2.x 常见约定,具体以官方“目录结构”文档为准):

theme-foo/
├── theme.yaml          # 必需,主题身份证
├── settings.yaml       # 可选,后台可配置项
├── templates/          # 模板文件(.html)
│   ├── index.html      # 首页 / 文章列表
│   ├── post.html       # 文章页
│   ├── category.html   # 分类页
│   └── page.html       # 独立页面
├── static/             # 静态资源
│   ├── css/
│   ├── js/
│   └── images/
├── i18n/               # 可选,多语言
└── screenshot.png      # 后台预览图

提示:改完 theme.yaml 后,去后台「主题 → 重载主题配置」才会生效,因为它会被持久化进数据库。

4. 模板怎么写

2.x 的引擎是 Thymeleaf。 模板就是普通 .html 文件,用 th:* 属性把数据“绑”上去,表达式写在 ${...} 里。这和 1.x 的 FreeMarker .ftl 完全不同,别混。

先用全局变量。 最常用的是 site

  • site.title 站点标题
  • site.subtitle 副标题
  • site.url 站点访问地址
  • site.logo Logo 地址
  • site.seo SEO 相关设置

一个最小首页(文章列表)index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title th:text="${site.title}">站点标题</title>
  <!-- 静态资源引用见第 5 章;路径写法各版本略有差异,请核对官方文档 -->
  <link rel="stylesheet" href="/themes/theme-foo/static/css/style.css">
</head>
<body>
  <header>
    <a th:href="${site.url}" th:text="${site.title}">站点名</a>
    <p th:text="${site.subtitle}">副标题</p>
  </header>

  <main>
    <!-- posts.items 是当前页文章列表 -->
    <article th:each="post : ${posts.items}">
      <h2>
        <a th:href="${post.status.permalink}" th:text="${post.spec.title}">文章标题</a>
      </h2>
      <time th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}">日期</time>
    </article>

    <!-- 分页:posts 自带 prevUrl / nextUrl / hasPrevious() / hasNext() -->
    <nav th:if="${posts.hasPrevious() || posts.hasNext()}">
      <a th:if="${posts.hasPrevious()}" th:href="${posts.prevUrl}">上一页</a>
      <a th:if="${posts.hasNext()}" th:href="${posts.nextUrl}">下一页</a>
    </nav>
  </main>
</body>
</html>

一个最小文章页 post.html

<article>
  <h1 th:text="${post.spec.title}">标题</h1>
  <time th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}">日期</time>

  <!-- 正文:Halo 会把 Markdown 渲染成 HTML 后放进内容变量,具体变量名以官方“模板变量”文档为准 -->
  <div th:utext="${post.spec.content}">正文</div>

  <!-- 用 Finder 做分类面包屑 -->
  <nav th:with="breadcrumbs = ${categoryFinder.getBreadcrumbs(post.categories[0].metadata.name)}">
    <a href="/">首页</a>
    <span th:each="bc : ${breadcrumbs}" th:text="${bc.spec.displayName}">分类</span>
  </nav>
</article>

Finder API 小结(来自官方示例,2.x 通用):

  • postFinder.listByCategory(page, size, categoryName) 取某分类文章
  • categoryFinder.getByNames(names) 按名取分类
  • categoryFinder.getBreadcrumbs(name) 取面包屑路径
  • pluginFinder.available('插件名') 判断某插件是否安装(用来条件渲染)

说明:上面 post.spec.contentpost.spec.title 等字段名取自官方 2026 年示例;若你的版本报错,以官方「模板变量」文档列出的字段为准。

自定义模板是 Halo 主题很香的能力:在 theme.yamlspec.customTemplates 下注册后,后台就能给某篇文章 / 分类 / 页面选不同模板(比如给“文档”类文章套一套知识库样式)。注册方式见第 3 章的 customTemplates 示例。

5. 样式与静态资源

CSS / JS / 图片放进 static/ 目录。模板里引用时,最稳的写法是按“主题名 + 路径”拼绝对地址,例如:

<link rel="stylesheet" href="/themes/theme-foo/static/css/style.css">
<script src="/themes/theme-foo/static/js/main.js"></script>

注意:各版本对“静态资源 URI 变量”(如是否存在 theme.staticUri 之类的快捷变量)表述不一。上面用硬编码路径最不容易出错;若想用变量,请核对官方「静态资源」文档中与你的版本对应的写法。

本地预览很简单:开着模板缓存关闭的开发模式,改完 static/ 下的文件刷新页面即可;若没生效,先在后台「重载主题配置」一次。

6. 安装、调试与上线

本地启用:把主题文件夹放进 ~/.halo2/themes/(Docker)或对应运行目录的 themes/,后台「主题」里就能看到,点启用即可。

改配置要重载:只要动过 theme.yaml(包括 customTemplates),必须后台「重载主题配置」,否则改的不生效——这是新手最高频的“为啥没变化”原因。

常见排查

  • 页面空白 / 500:多半是模板表达式写错(变量名拼错、Finder 参数不对)。看后台或运行日志的报错行号。
  • 样式没加载:检查 static/ 路径和文件名;确认引用路径与主题文件夹名一致。
  • 变量不识别:先确认你用的是 2.x 的 Thymeleaf 写法,而不是 1.x 的 FreeMarker。

发布出去

  • GitHub:仓库名建议 halo-theme-主题名,打上 halohalo-theme 两个 topic,方便别人搜到。
  • 官方应用市场:按官方提交流程上传,可被更多用户一键安装。
  • 打包分发:把整个主题文件夹压缩成 zip 即可分享。

7. 进阶与可复用经验

  • 设置项(settings.yaml:把“每页文章数”“页脚文案”之类做成后台可填的字段,别人用你的主题不用改代码。在 theme.yamlspec.settingName 关联。
  • 多语言(i18n/:面向国际用户时,把文案抽到语言文件,比硬编码强太多。
  • 性能:列表页别一次性查全站文章;利用 posts 自带的分页参数。静态资源尽量合并、压缩。
  • 踩坑清单(收好)
    1. metadata.name 必须和文件夹名一致,否则资源加载异常。
    2. theme.yaml 后一定要“重载主题配置”。
    3. 1.x 与 2.x 模板引擎不互通,clone 别人主题先看版本。
    4. 文档迭代快,落地前拿你实际版本的官方文档再对一遍字段。

做完第一个主题,你会发现自己不再被现成主题绑架:想要什么布局,写一段 HTML;想要什么数据,调一个 Finder。主题开发本质上是“用 Halo 给你的变量和 API,拼出你脑子里的页面”——门槛不在语法,在于先跑通一遍上面这七步。

本文写作状态为 advisory:配置字段与全局变量已对照官方 2.24 文档;模板引擎(Thymeleaf)与静态资源引用写法建议结合你安装的具体版本再确认一次。如用于正式发布,请标注“基于 Halo vX.Y 验证”。