Stedefast

Modules

Reactions

3 min read

The reactions module lets visitors react to any page with one or more configurable emojis (e.g. 👍 ❤️ 🔥 💡). Counts are stored in Cloudflare KV, pre-built to per-page static JSON at build time, and updated by lightweight Worker handlers. The island handles optimistic updates and per-user caps via localStorage.

Installation

pnpm add @stedefast/module-reactions

Setup

// stedefast.config.ts
import { defineConfig } from "@stedefast/core";
import { ReactionsModule } from "@stedefast/module-reactions";

export default defineConfig({
  // ...
  modules: [
    ReactionsModule({
      reactions: ["👍", "❤️", "🔥", "💡"],   // emoji set (max 8)
      maxPerUserPerReaction: 1,               // per-user cap, enforced via localStorage
      showCounts: true,                       // show count next to each emoji
    }),
  ],
  cloudflare: {
    projectName: "my-site",
    kvNamespaces: [{ binding: "REACTIONS_KV", namespaceId: "YOUR_KV_NAMESPACE_ID" }],
  },
});

Cloudflare bindings

Create a KV namespace:

wrangler kv namespace create REACTIONS_KV

Copy the id into your config.

Binding Type Purpose
REACTIONS_KV KV namespace Per-page, per-emoji reaction counts

KV key schema

reactions:{pageSlug}:{emoji}  →  count (number as string)

Example: reactions:hello-world:👍"42"

Using the island in a template

<div
  data-island="ReactionsIsland"
  data-props={JSON.stringify({
    pageSlug: page.slug,
    initialCounts: {},
    reactions: ["👍", "❤️", "🔥", "💡"],
    maxPerUserPerReaction: 1,
  })}
/>

Pass initialCounts from the pre-built static JSON to avoid a fetch on first render:

// In a React template, load from the module data map
const initialCounts = moduleData?.reactions?.[page.slug] ?? {};

<div
  data-island="ReactionsIsland"
  data-props={JSON.stringify({
    pageSlug: page.slug,
    initialCounts,
    reactions: ["👍", "❤️", "🔥", "💡"],
    maxPerUserPerReaction: 1,
  })}
/>

How it works

  1. Build timebuildStaticExport() reads KV keys (reactions:{slug}:{emoji}) for each content node and writes dist/data/reactions/{slug}.json. It also writes dist/data/reactions/_totals.json with aggregate counts across all pages.
  2. Page load — The island receives initialCounts from the pre-built JSON and renders immediately. If initialCounts is empty, it fetches /data/reactions/{slug}.json from the CDN.
  3. React — Clicking an emoji triggers an optimistic update (count increments in the UI immediately), then fires POST /_modules/reactions/react to increment the KV counter.
  4. Per-user cap — Each user's reactions are tracked in localStorage under sf-reactions:{pageSlug}. Once maxPerUserPerReaction is reached for an emoji, that button is disabled.
  5. Active state — Emojis the user has already reacted with are shown at full opacity (opacity: 1); unreacted emojis show at opacity: 0.6.
  6. Next build — Static JSON is regenerated with updated counts.

Configuration reference

Option Type Default Description
reactions string[] ["👍","❤️","🔥","💡"] Emoji set to display (max 8)
maxPerUserPerReaction number 1 Max times a user can add each reaction per page
showCounts boolean true Whether to show counts next to emojis

Difference from Claps

The Claps module has a single cumulative counter per page — users can clap multiple times up to a configured maximum. The Reactions module has per-emoji binary reactions: a user either has or hasn't reacted with each emoji (controlled by maxPerUserPerReaction, default 1). This makes reactions more like GitHub emoji reactions and claps more like Medium applause.

Feature Claps Reactions
Emoji types One (👏) Multiple (configurable)
Per-user limit Up to maxClapsPerPage total Up to maxPerUserPerReaction per emoji
Storage key claps:{pageId} reactions:{slug}:{emoji}
Static export Single counts.json Per-page {slug}.json + _totals.json