Halo 主题开发:为访客评论适配 Gravatar 头像

分类记录
标签Halo
Halo 主题开发:为访客评论适配 Gravatar 头像

背景

Halo 的 评论 Finder API 返回的 CommentVo 中,owner.avatar 只有注册用户设置了头像才有值——访客评论拿不到头像。公开查询还会对评论者标识、邮箱和 IP 脱敏,看起来似乎没有可用的头像来源。

但文档里有一个容易被忽略的字段:spec.owner.annotations 中的 email-hash,即评论邮箱的 SHA-256(仅在邮箱存在时提供)。这正是 Gravatar 体系的标准入参,主题可以用它向任意 Gravatar 兼容服务请求访客头像。

最终方案为三级回退:

  1. owner.avatar —— 注册用户已设置的头像,优先使用

  2. {镜像地址}/{email-hash}?s=尺寸&d=identicon —— 访客经 Gravatar 兼容服务获取

  3. 昵称首字符 —— 前两者都没有时的兜底

镜像地址做成主题设置项,方便按网络环境切换。

第一步:settings.yaml 添加镜像设置

在 settings.yaml 中新增「评论」分组,提供预设镜像下拉与自定义地址:

- group: comment
  label: 评论
  formSchema:
    - $formkit: select
      name: avatar_mirror
      id: avatar_mirror
      key: avatar_mirror
      label: 访客头像服务
      value: https://cravatar.cn/avatar
      options:
        - value: https://cravatar.cn/avatar
          label: Cravatar(国内推荐)
        - value: https://gravatar.com/avatar
          label: Gravatar 官方
        - value: https://weavatar.com/avatar
          label: WeAvatar
        - value: https://cdn.sep.cc/avatar
          label: sep.cc 镜像
        - value: custom
          label: 自定义镜像
      help: 访客评论不提供头像,主题使用评论邮箱的 SHA-256 向所选 Gravatar 兼容服务请求头像……
    - $formkit: url
      name: avatar_mirror_custom
      id: avatar_mirror_custom
      key: avatar_mirror_custom
      label: 自定义头像镜像地址
      value: ''
      placeholder: https://example.com/avatar
      help: Gravatar 兼容服务地址,末尾不要带 /;仅在上方选择「自定义镜像」后生效,留空时回退 Cravatar。
      if: '$get("avatar_mirror").value === "custom"'

预设项的 value 直接存服务地址,模板里就不需要做枚举到 URL 的映射,只需处理 custom 一个分支。

第二步:模板中解析镜像地址

在列表外层元素的 th:with 中把镜像地址解析成一个干净的基础 URL:

<div
  class="comments-wall"
  th:with="pageSize=10,
    commentsWall=${commentFinder.list(null, pageNo, pageSize)},
    avatarMirrorRaw=${theme.config?.comment?.avatar_mirror == 'custom' ? theme.config?.comment?.avatar_mirror_custom : theme.config?.comment?.avatar_mirror},
    avatarMirror=${#strings.isEmpty(avatarMirrorRaw) ? 'https://cravatar.cn/avatar' : (#strings.endsWith(avatarMirrorRaw, '/') ? #strings.substring(avatarMirrorRaw, 0, #strings.length(avatarMirrorRaw) - 1) : avatarMirrorRaw)}"
>

要点:

  • theme.config?.comment?.avatar_mirror 全程安全导航——老用户升级主题后尚未保存设置时配置为空,表达式自动回退 Cravatar,不会拼出空地址

  • 同一个 th:with 内的赋值按顺序执行,后定义的 avatarMirror 可以引用先定义的 avatarMirrorRaw

  • 顺手去掉末尾 /,避免拼出 //hash 的双斜杠

第三步:头像三级回退渲染

在 th:each 的迭代元素上先用 th:with 提取一次哈希(th:each 优先级高于 th:with,迭代变量可用),避免三处条件各写一遍长表达式:

<li
  class="comments-wall__item"
  th:each="comment : ${commentsWall.items}"
  th:with="emailHash=${comment.spec.owner?.annotations['email-hash']}"
>
  <!-- 1. 注册用户头像 -->
  <img
    th:if="${comment.owner != null and !#strings.isEmpty(comment.owner.avatar)}"
    class="comments-wall__avatar"
    th:src="${comment.owner.avatar}"
    th:alt="${comment.owner.displayName}"
    width="40" height="40" loading="lazy" referrerpolicy="no-referrer"
  />
  <!-- 2. 访客:邮箱 SHA-256 拼装 Gravatar 兼容服务地址 -->
  <img
    th:if="${(comment.owner == null or #strings.isEmpty(comment.owner.avatar)) and !#strings.isEmpty(emailHash)}"
    class="comments-wall__avatar"
    th:src="|${avatarMirror}/${emailHash}?s=80&d=identicon|"
    th:alt="${!#strings.isEmpty(comment.owner?.displayName) ? comment.owner.displayName : '匿名'}"
    width="40" height="40" loading="lazy" referrerpolicy="no-referrer"
  />
  <!-- 3. 兜底:昵称首字符 -->
  <span
    th:if="${(comment.owner == null or #strings.isEmpty(comment.owner.avatar)) and #strings.isEmpty(emailHash)}"
    class="comments-wall__avatar comments-wall__avatar--fallback"
    th:text="${#strings.substring(comment.owner?.displayName, 0, 1)}"
  >匿</span>
</li>

回复列表同理,仅尺寸参数改为 s=56。侧边栏「最新评论」小组件也做同样适配(评论 s=40、回复 s=32),保证两处表现一致。

几个细节:

  • 注解读取:annotations 是 Map<String, String>,键名含连字符,必须用 ['email-hash'] 下标语法;owner 或 annotations 为 null 时 SpringEL 安全导航直接返回 null,#strings.isEmpty 判空即可

  • s= 尺寸:按显示尺寸 ×2 请求(40px 头像用 s=80),兼顾高分屏

  • d=identicon:邮箱未在头像服务注册时返回服务生成的几何图案,而不是 404 或破图

  • 字面量替换:th:src="|...|" 比字符串拼接可读性好;属性里的 & 会被 Thymeleaf 输出为 &amp;,是合法 HTML