Modules
Reactions
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
- Build time —
buildStaticExport()reads KV keys (reactions:{slug}:{emoji}) for each content node and writesdist/data/reactions/{slug}.json. It also writesdist/data/reactions/_totals.jsonwith aggregate counts across all pages. - Page load — The island receives
initialCountsfrom the pre-built JSON and renders immediately. IfinitialCountsis empty, it fetches/data/reactions/{slug}.jsonfrom the CDN. - React — Clicking an emoji triggers an optimistic update (count increments in the UI immediately), then fires
POST /_modules/reactions/reactto increment the KV counter. - Per-user cap — Each user's reactions are tracked in
localStorageundersf-reactions:{pageSlug}. OncemaxPerUserPerReactionis reached for an emoji, that button is disabled. - Active state — Emojis the user has already reacted with are shown at full opacity (
opacity: 1); unreacted emojis show atopacity: 0.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 |