公开 API

主题和其他插件可以使用这些接口展示自己的评论列表、读者墙与排行榜。需要复用编辑器、登录、上传和验证码时,按开发者适配嵌入独立评论框即可,无需重写提交流程。

版本说明

运行环境为 Halo 2.27.0 及以上。本页读者榜中的 website、emailHash、repliesCount 从 1.1.1 开始提供;旧版安装包可能不返回这些字段。接入时安装 1.1.1 或更高的兼容版本,或在调用方兼容字段缺失。

接口约定

插件查询接口的公共前缀为:

/apis/api.commentnext.xhhao.com/v1alpha1
方法与路径用途
GET /comments按内容查询主评论,可预加载回复
GET /comments/{commentName}/replies查询某条主评论的回复
GET /comments/featured查询精选主评论与回复
GET /comments/ranking查询全站读者榜

这些路径允许匿名读取。评论、回复、精选查询会根据当前登录用户的权限过滤:匿名访客看到公开内容,登录作者和具有相应权限的管理员可能看到更多内容。自定义前端不应把带登录态读取的结果缓存给其他访客。

读者榜始终按公开、已审核且未删除的内容聚合,不因请求者是管理员而纳入私密内容。接口不会直接返回原始评论邮箱或完整 IP;邮箱哈希的用途与局限见读者关联。

所有示例默认同站访问。登录态读取采用 credentials: 'include';接口错误时先检查 HTTP 状态,不能把错误响应当作分页对象使用。

评论列表

GET /apis/api.commentnext.xhhao.com/v1alpha1/comments?group=content.halo.run&kind=Post&version=v1alpha1&name=post-xxx&page=1&size=20&withReplies=true&replySize=10&sort=latest
参数默认值说明
group空内容所属 API 组;文章和独立页面传 content.halo.run
kind必填Post、SinglePage 或已接入 Halo 评论的内容类型
version必填内容 API 版本,常见值为 v1alpha1
name必填内容的 metadata.name
page1普通主评论页码,从 1 开始
size20普通主评论每页条数,传正整数
withRepliestrue是否为每条主评论预加载第一页回复
replySize10预加载的回复数,传正整数
sortlatest支持 latest、earliest、hot;未知值按 latest 处理

返回分页对象,主要结构为:

{
  "page": 1,
  "size": 20,
  "total": 1,
  "totalPages": 1,
  "hasNext": false,
  "hasPrevious": false,
  "first": true,
  "last": true,
  "firstCommentName": "comment-xxx",
  "pinnedItems": [],
  "items": [
    {
      "metadata": { "name": "comment-xxx" },
      "spec": {
        "content": "<p>感谢分享!</p>",
        "approved": true,
        "hidden": false,
        "creationTime": "2026-11-05T02:00:00Z",
        "owner": {
          "name": "",
          "kind": "Email",
          "displayName": "读者",
          "annotations": {
            "website": "https://reader.example.com",
            "email-hash": "d108b279434fe1d54ac0f1da633564604b26c2e0e221d108b0fbadb87aba02c0"
          }
        }
      },
      "owner": {
        "displayName": "读者",
        "kind": "Email",
        "role": "anonymous",
        "activeCommentCount": 5,
        "badges": []
      },
      "stats": { "upvote": 3 },
      "top": false,
      "priority": 0,
      "featured": false,
      "replies": {
        "page": 1,
        "size": 10,
        "total": 0,
        "totalPages": 0,
        "hasNext": false,
        "hasPrevious": false,
        "first": true,
        "last": true,
        "items": []
      }
    }
  ]
}

示例只保留与展示相关的字段,实际响应可能包含其他扩展信息。firstCommentName 在存在首条可见评论时返回,列表为空时可能省略。

置顶评论放在 pinnedItems,普通分页放在 items。total、totalPages 和 size 只针对普通主评论,置顶项不占分页名额;不要将置顶数量再次加入普通列表总数。withReplies=false 时不返回预加载的 replies 对象。

前端请求示例:

const query = new URLSearchParams({
  group: 'content.halo.run',
  kind: 'Post',
  version: 'v1alpha1',
  name: 'post-xxx',
  page: '1',
  size: '20',
  withReplies: 'false',
  sort: 'latest',
});

const response = await fetch(
  `/apis/api.commentnext.xhhao.com/v1alpha1/comments?${query}`,
  { credentials: 'include', cache: 'no-store' },
);
if (!response.ok) throw new Error(`评论加载失败:${response.status}`);

const page = await response.json();
console.log('置顶评论', page.pinnedItems ?? []);
console.log('普通评论', page.items ?? []);

回复列表

GET /apis/api.commentnext.xhhao.com/v1alpha1/comments/comment-xxx/replies?page=1&size=10

路径中的 comment-xxx 是所属主评论的 metadata.name,不是被回复的另一条回复的 ID。构造路径时对该值使用 encodeURIComponent()。

参数默认值说明
page1页码,从 1 开始
size10每页条数,传正整数

返回同类分页对象,items 中是回复。回复按置顶优先、优先级升序、发表时间升序排列;此接口不接收评论列表的 sort 参数。

回复的 spec.commentName 指向所属主评论,spec.quoteReply 在回复另一条回复时指向被回复的回复。主评论不存在、已删除或当前用户无权查看时,返回 404,调用方不应继续展示该线程的历史缓存。

评论与回复的数据结构

字段用途
metadata.name当前主评论或回复的 ID,可传给评论框的回复目标
spec.content评论 HTML 内容;自定义渲染时按自己的安全策略处理,不能直接信任任意富文本
spec.creationTime发表时间
spec.approved / spec.hidden审核与隐藏状态,列表仍应尊重接口的可见性规则
spec.commentName回复所属主评论 ID,仅回复适用
spec.quoteReply被回复的回复 ID,仅嵌套回复适用
spec.owner.annotations.website评论者随内容提交的网站,可能缺失
spec.owner.annotations['email-hash']匿名评论邮箱的哈希,可能缺失;不要使用被清空的 spec.owner.name 关联身份
owner公开作者信息,例如昵称、头像、角色、累计评论数、徽章
stats.upvote当前点赞数,字段名为单数 upvote
top / priority / featured置顶、优先级、精选状态
replies主评论预加载的回复分页,仅在请求预加载时提供

这些是 HTTP 原始响应字段。评论框的 created 事件使用归一化结构,例如原始 metadata.name 对应 result.id,原始 stats.upvote 对应 result.stats.upvotes。接入自己的列表时,以使用的那一种结构为准。

精选评论

GET /apis/api.commentnext.xhhao.com/v1alpha1/comments/featured?target=all&page=1&size=10
参数默认值说明
targetallall、comment、reply,分别查询全部、主评论、回复
page / size1 / 10正整数页码与每页条数
keyword空按评论者昵称或内容关键词筛选
group、kind、version、name不限内容可按内容过滤;传入内容过滤条件时应完整提供内容归属

分页 items 仍沿用评论或回复结构,额外包含 targetType、parentName、subject,以及存在时的 featuredAt。targetType 为 comment 或 reply;parentName 在回复时为所属主评论 ID,主评论时为空字符串。适合主题展示精选评论卡片。

读者榜

GET /apis/api.commentnext.xhhao.com/v1alpha1/comments/ranking?period=month&limit=10
参数默认值说明
periodallweek、month、quarter、year、all;忽略大小写和首尾空白,其他值按 all 处理
limit10返回人数,限制在 1–50;超出范围取边界值,无法解析为整数时取 10

周期采用 Halo 服务端时区的自然周期:本周从周一零点开始,本月从 1 日开始,本季从 1、4、7、10 月的第一天开始,本年从 1 月 1 日开始。all 表示全部。

响应

{
  "period": "month",
  "limit": 10,
  "items": [
    {
      "displayName": "读者",
      "avatar": "https://example.com/avatar.webp",
      "kind": "Email",
      "commentCount": 5,
      "upvoteCount": 8,
      "latestComment": "感谢分享!",
      "latestCommentAt": "2026-11-05T02:00:00Z",
      "levelBadge": null,
      "website": "https://reader.example.com",
      "emailHash": "d108b279434fe1d54ac0f1da633564604b26c2e0e221d108b0fbadb87aba02c0",
      "repliesCount": 3
    }
  ]
}

period 和 limit 为规范化后的实际参数,items 没有数据时为 [],有数据时也可能少于 limit。

items 字段含义
displayName公开昵称,登录用户优先使用当前用户资料
avatar头像地址,接入方可提供缺省头像
kindEmail 表示匿名评论者,User 表示 Halo 登录用户
commentCount所选周期内本人发表的公开主评论与回复总数
repliesCount上述总数中本人发表的回复数,不是其评论收到的回复数;主评论数为 commentCount - repliesCount
upvoteCount所选周期内发表的公开主评论与回复的当前获赞总数,不是所选周期内新增的点赞数
latestComment周期内最近一条公开主评论或回复的纯文本摘要,超长时截断并追加省略号
latestCommentAt上述内容的发表时间,ISO 8601 格式
levelBadge与评论区一致的累计等级徽章,没有时为 null;对象包含 id、label、tone、icon、color、title
website周期内最近一条公开主评论或回复的 owner.annotations.website,移除首尾空白;没有时为 null,不回退到更早的网址
emailHash邮箱去首尾空白、转小写后,以 UTF-8 计算 SHA-256 得到的小写十六进制字符串;邮箱不可用时为 null

统计规则

  • 全站聚合已审核、非隐藏、未删除的主评论与回复;回复所属主评论也必须公开且未删除。
  • 回复根据自己的发表时间计入周期,即使主评论发表于更早的周期。
  • 按 commentCount 降序,同数时按昵称不区分大小写升序排列;新增 repliesCount 不改变原来的总数含义和排序。
  • 统计结果按周期缓存一分钟。新增内容、审核或资料变更可能稍后反映到榜单。
  • 插件“读者排行”中的排除用户/邮箱设置,在每次请求时、截取人数之前生效,会自动补齐后续读者;不影响正常评论。
  • 等级徽章按累计评论统计,与当前排行周期及名次独立;每次请求读取徽章快照,不属于上述榜单聚合缓存。

读者关联

匿名评论按规范化邮箱归并,登录用户按用户名归并。两类身份不会自动合并,同一邮箱可能在榜单中对应多行。因此 emailHash 是邮箱关联键,不是榜单行的唯一 ID。

匿名评论的哈希来自其评论邮箱;登录用户的哈希来自当前用户邮箱。没有邮箱、对应用户资料不可用时返回 null;邮箱变化后哈希也会变化。

主题可用 emailHash 对齐按相同规则生成的 spec.owner.annotations['email-hash'],正常邮箱数据也可与 Halo commentFinder 的哈希关联。历史邮箱数据包含额外首尾空白时,应先核对两侧的归一化规则。不要凭相同昵称合并身份,也不要在缺少哈希时把所有空值归并成一个读者。

SHA-256 哈希用于脱敏关联,并不是不可反推的匿名凭证。接口不直接公开原始邮箱;自定义前端应只展示业务需要的信息。

请求示例

const response = await fetch(
  '/apis/api.commentnext.xhhao.com/v1alpha1/comments/ranking?period=month&limit=10',
);
if (!response.ok) throw new Error(`读者榜加载失败:${response.status}`);

const { items = [] } = await response.json();
for (const reader of items) {
  // 兼容尚未提供 repliesCount 的旧版,不能将缺失值直接当作 0。
  const mainComments = Number.isFinite(reader.repliesCount)
    ? reader.commentCount - reader.repliesCount
    : undefined;
  console.log(reader.displayName, mainComments, reader.upvoteCount);
}

榜单组件接入

不需要自己实现榜单 UI 时,可以在 Halo 文章编辑器中插入“评论读者榜(页面)”或“评论读者榜(侧边栏)”,也可直接在主题中放置:

<comment-ranking
  period="month"
  limit="10"
  title="本月活跃读者"
  subtitle="感谢大家的参与"
></comment-ranking>

<comment-ranking-sidebar
  period="month"
  limit="5"
  title="评论读者墙"
  ranking-url="/readers"
  link-text="查看完整榜单"
></comment-ranking-sidebar>

页面榜单支持 1–50 人,侧边栏支持 1–10 人。两者均支持 accent-color、surface-color、text-color,也可由主题 CSS 变量 --comment-ranking-accent-color、--comment-ranking-surface-color、--comment-ranking-text-color 统一设置。show-header="false" 隐藏标题区,暗色外观跟随主题的暗色容器。

提交与兼容性

本插件的评论框使用 Halo 自身的评论提交接口:

接口用途
POST /apis/api.halo.run/v1alpha1/comments创建主评论
POST /apis/api.halo.run/v1alpha1/comments/{commentName}/reply创建回复,可通过 quoteReply 指定被回复的回复

它们属于 Halo API,和本页插件查询前缀不同。普通适配直接复用评论框,让组件处理登录、匿名信息、验证码、上传、错误提示与审核状态;不要向本插件的 GET /comments 查询路径发送创建请求。自己实现提交前端时,需要完整实现相应的 Halo 提交契约。

启用评论邮箱限制后,匿名提交使用受限邮箱时会在创建前返回 400,问题类型为 https://www.xhhao.com/probs/comment-email-rejected。主评论和回复均适用,不能靠直接调用 Halo 提交接口绕过。接入方应展示响应中的提示并保留草稿;不要将此错误误判为验证码错误或已提交成功。

/comments/ranking 作为公开接口供主题与插件使用,兼容扩展采用增加字段的方式;删除既有字段或改变既有含义时使用新的 API 版本。调用方应忽略未知字段,兼容可空值、空列表及旧版缺失字段。

昵称和摘要按文本渲染;website 作为链接使用前限制为 HTTP/HTTPS,外链按需添加 noopener noreferrer nofollow ugc。内容正文属于富文本,应采用接入方自己的安全渲染策略。