Halo 主题开发:为访客评论适配 Gravatar 头像
背景
Halo 的 评论 Finder API 返回的 CommentVo 中,owner.avatar 只有注册用户设置了头像才有值——访客评论拿不到头像。公开查询还会对评论者标识、邮箱和 IP 脱敏,看起来似乎没有可用的头像来源。
但文档里有一个容易被忽略的字段:spec.owner.annotations 中的 email-hash,即评论邮箱的 SHA-256(仅在邮箱存在时提供)。这正是 Gravatar 体系的标准入参,主题可以用它向任意 Gravatar 兼容服务请求访客头像。
最终方案为三级回退:
owner.avatar—— 注册用户已设置的头像,优先使用{镜像地址}/{email-hash}?s=尺寸&d=identicon—— 访客经 Gravatar 兼容服务获取昵称首字符 —— 前两者都没有时的兜底
镜像地址做成主题设置项,方便按网络环境切换。
第一步: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 输出为&,是合法 HTML
- 作者: 薛柔零
- 链接: https://blog.xuezero.com/archives/3xcGTW7N
- 版权: 除特别声明外,本文采用 CC BY-NC-SA 4.0 许可协议进行许可,转载请注明出处。