Comments, reactions & views
Let readers take part — from the browser, with the publishable key.
These calls come from your readers' browsers, so they use the publishable key, and your site's origin must be listed on it.
GET /v1/public/blog tells your UI what is switched on: whether comments need approval, which reactions to offer, and more.
A stable reader id
Reactions toggle per reader, and views are counted once per reader per 30 minutes. Give each reader a random id and send it as visitorId:
const KEY = 'hpk_…';
const BASE = 'https://api.horbit.ng/v1/public/blog';
const visitorId = (localStorage.hbVisitor ??= crypto.randomUUID());
const call = (path: string, body?: object) =>
fetch(`${BASE}${path}`, {
method: body ? 'POST' : 'GET',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
body: body && JSON.stringify(body),
}).then((res) => res.json());Without a visitorId, Horbit falls back to the reader's IP address and browser.
Views
Count a view once per page load, from the browser — bots that never run script do not inflate the number:
call(`/posts/${slug}/views`, { visitorId });
// { data: { counted: true, views: 1285 } }Reactions
// counts, and which ones this reader already left
const { data } = await call(`/posts/${slug}/reactions?visitorId=${visitorId}`);
// data.counts → { like: 51, love: 20, clap: 12, insightful: 4 }
// data.mine → ['like']
// data.types → [{ type: 'like', label: 'Like', emoji: '👍' }, …]
// toggle one
await call(`/posts/${slug}/reactions`, { type: 'love', visitorId });Only the types the workspace offers are accepted. DELETE /posts/{post}/reactions/{type} removes one explicitly.
Comments
// approved comments, threaded
const { data: thread, meta } = await call(`/posts/${slug}/comments`);
// thread[i].replies → nested replies
// thread[i].bodyHtml → escaped, split into <p class="hb-comment__paragraph">
// meta.commentsOpen → show the form or not
// leave one
const { data: comment } = await call(`/posts/${slug}/comments`, {
authorName: 'Ada Obi',
authorEmail: 'ada@example.org',
body: 'Great write-up!',
parentId: undefined, // set to reply
visitorId,
website: '', // honeypot — see below
});
// comment.status → 'approved' (live now) or 'pending' (awaiting a moderator)Tell the reader which happened: depending on the workspace's moderation setting, a comment goes live at once or waits for review.
Emails are never returned; avatarUrl is a Gravatar.
Spam protection
Render a hidden website input in your form and send its value. People never see it; bots fill it in, and anything in it files the comment as spam.
Comments with too many links or a blocked word are held for moderation too.
Comments close when the workspace switches them off, when a post closes its own, or after the auto-close period — the API then answers 403.