Denys Isaichenko

Software Engineer

Adding reactions to Astro (SSG) website

When I was replacing the comments system in my pet Astro project, there was one feature from the Hyvor comments system that was missing in the new solution: the reactions system.

Reactions

I also wanted the site to be statically generated twice a day, but it needed to be interactive enough for users.

Here’s how I implemented the reactions system:

Moving reactions from Hyvor to Supabase

The first step was exporting the existing reactions from Hyvor. They provide an export feature in JSON format, so exporting was straightforward.

With this JSON file, I followed a similar migration process to the one I used for comments, but this time, I imported the data into a different table in Supabase.

API to fetch/update reactions

My idea was to pre-generate the website with reaction counts twice a day, but to fetch the latest numbers from Supabase dynamically once the page loads, updating the frontend as needed.

Initially, I considered creating a Netlify function to handle the fetch API, but their limit is 125k requests per month, which I feared might not be sufficient. Instead, I opted to use Cloudflare Workers, which offer a 100k requests per day limit.

Working with Cloudflare Workers was fairly easy. They have a CLI tool called wrangler that allows seamless deployment and development of workers. I added Supabase as a dependency and implemented a simple fetch API:

export default {
  async fetch(request, env, ctx): Promise<Response> {
    try {
      const { path, method } = await request.json<{
        method: "GET" | "POST";
        path: string;
        reaction: string;
      }>();

      if (!path || !method) {
        throw new Error("Incorrect Request");
      }

      const supabase = createClient<Database>(
        env.SUPABASE_URL,
        env.SUPABASE_KEY,
      );

      const { data } = await supabase
        .from("reactions")
        .select()
        .eq("path", path);

      if (!data?.length) {
        return Response.json(
          {
            superb: 0,
            love: 0,
            wow: 0,
            sad: 0,
            laugh: 0,
            angry: 0,
          }
        );
      }

      const reactions = data[0];

      return Response.json(reactions.reactions);
    } catch (e: any) {
      return new Response(e?.message, { status: 422 });
    }
  }
} satisfies ExportedHandler<Env>;

Later, I updated this API to allow updating reactions when someone clicked on a specific reaction (hence the “POST” and “GET” methods).

As a precaution, I also connected a rate-limiter per-path from Cloudflare.

Connecting Frontend

With the API in place, I needed to implement the frontend. My initial instinct was to connect a framework or library to manage the state in a more abstract way, but logically, it didn’t make sense to add ~50kb of JavaScript just to make six buttons interactive.

So, I opted for a pure JavaScript implementation, which I don’t do often due to the prevalence of frameworks. I won’t provide any code snippets because I don’t think the implementation is great, but I used fetch, querySelectorAll, addEventListener, and data- attributes to achieve it without external libraries.

As a small safeguard against multiple clicks on the same reaction, I added pointer-events: none to the reaction icon after a click. Initially, I considered implementing a localStorage solution to remember which posts a user had already reacted to, but I decided it was overkill for a small project like this.

Final Thoughts

I’m glad I re-implemented the reactions myself; it reminded me that it’s okay to use pure JavaScript — you don’t need a framework for everything.