Horbit Developers

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.

On this page