💾 Archived View for gmi.runtimeterror.dev › kudos-with-cabin › index.gmi captured on 2024-08-25 at 00:52:17. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2024-08-18)

🚧 View Differences

-=-=-=-=-=-=-

💻 [runtimeterror $]

2024-06-24 ~ 2024-06-26

Kudos With Cabin

I'm not one to really worry about page view metrics, but I do like to see which of my posts attract the most attention - and where that attention might be coming from. That insight has allowed me to find new blogs and sites that have linked to mine, and has tipped me off that maybe I should update that four-year-old post that's suddenly getting renewed traffic from Reddit.

In my quest for such knowledge, last week I switched my various web properties back to using Cabin [1] for "privacy-first, carbon conscious web analytics". I really like how lightweight and deliberately minimal Cabin is, and the portal does a great job of presenting the information that I care about. With this change, though, I gave up the cute little upvote widgets provided by the previous analytics platform.

[1] Cabin

I recently shared on my Bear weblog [2] about how I was hijacking Bear's built-in upvote button to send a "kudos" event [3] to Cabin and tally those actions there.

[2] on my Bear weblog

[3] event

Well today I implemented a similar thing on *this* blog. Without an existing widget to hijack, I needed to create this one from scratch using a combination of HTML in my page template, CSS to style it, and JavaScript to fire the event.

Layout

My Hugo [4] setup uses `layouts/_default/single.html` to control how each post is rendered, and I've already got a section at the bottom which displays the "Reply by email" link if replies are permitted on that page:

[4] Hugo

    <div class="content__body"> 
        {{ .Content }}
    </div>
    {{- $reply := true }}
    {{- if eq .Site.Params.reply false }}
      {{- $reply = false }}
    {{- else if eq .Params.reply false }}
      {{- $reply = false }}
    {{- end }}
    {{- if eq $reply true }}
    <hr>
    <span class="post_email_reply"><a href="mailto:replies@example.com?Subject=Re: {{ .Title }}">📧 Reply by email</a></span>
    {{- end }}

I'll only want the upvote widget to appear on pages where replies are permitted so this makes a logical place to insert the new code:

    <div class="content__body"> 
        {{ .Content }}
    </div>
    {{- $reply := true }}
    {{- if eq .Site.Params.reply false }}
      {{- $reply = false }}
    {{- else if eq .Params.reply false }}
      {{- $reply = false }}
    {{- end }}
    {{- if eq $reply true }}
    <hr>
    <div class="kudos-container"> 
      <button class="kudos-button">
        <span class="emoji">👍</span>
      </button>
      <span class="kudos-text">Enjoyed this?</span>
    </div>
    <span class="post_email_reply"><a href="mailto:replies@example.com?Subject=Re: {{ .Title }}">📧 Reply by email</a></span>
    {{- end }}

The button won't actually do anything yet, but it'll at least appear on the page. I can use some CSS to make it look a bit nicer though.

CSS

My theme uses `static/css/custom.css` to override the defaults, so I'll drop some styling bits at the bottom of that file:

/* Cabin kudos styling
.kudos-container {
  display: flex;
  align-items: center;
}
.kudos-button {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.2rem;
  padding: 0;
  margin-right: 0.25rem;
}
.kudos-button:disabled {
  cursor: default;
}
.kudos-button .emoji {
  display: inline-block;
  transition: transform 0.3s ease;
}
.kudos-button.clicked .emoji {
  transform: rotate(360deg);
}
.kudos-text {
  transition: font-style 0.3s ease;
}
.kudos-text.thanks {
  font-style: italic;
}

I got carried away a little bit and decided to add a fun animation when the button gets clicked. Which brings me to what happens when this thing gets clicked.

JavaScript

I want the button to do a little bit more than *just* send the event to Cabin so I decided to break that out into a separate script, `assets/js/kudos.js`. This script will latch on to the kudos-related elements, and when the button gets clicked it will (1) fire off the `cabin.event('kudos')` function to record the event, (2) disable the button to discourage repeated clicks, (3) change the displayed text to `Thanks!`, and (4) celebrate the event by spinning the emoji and replacing it with a party popper.

// manipulates the post upvote "kudos" button behavior
window.onload = function() {
  // get the button and text elements
  const kudosButton = document.querySelector('.kudos-button');
  const kudosText = document.querySelector('.kudos-text');
  const emojiSpan = kudosButton.querySelector('.emoji');
  kudosButton.addEventListener('click', function(event) {
    // send the event to Cabin
    cabin.event('kudos')
    // disable the button
    kudosButton.disabled = true;
    kudosButton.classList.add('clicked');
    // change the displayed text
    kudosText.textContent = 'Thanks!';
    kudosText.classList.add('thanks');
    // spin the emoji
    emojiSpan.style.transform = 'rotate(360deg)';
    // change the emoji to celebrate
    setTimeout(function() {
      emojiSpan.textContent = '🎉';
    }, 150);  // half of the css transition time for a smooth mid-rotation change
  });
}

The last step is to go back to my `single.html` layout and pull in this new JavaScript file. I placed it in the site's `assets/` folder so that Hugo can apply its minifying magic so I'll need to load it in as a page resource:

    <div class="content__body"> 
        {{ .Content }}
    </div>
    {{- $reply := true }} 
    {{- if eq .Site.Params.reply false }}
      {{- $reply = false }}
    {{- else if eq .Params.reply false }}
      {{- $reply = false }}
    {{- end }}
    {{- if eq $reply true }}
    <hr>
    <div class="kudos-container">
      <button class="kudos-button">
        <span class="emoji">👍</span>
      </button>
      <span class="kudos-text">Enjoyed this?</span>
    </div>
    {{ $kudos := resources.Get "js/kudos.js" | minify }} 
    <script src="{{ $kudos.RelPermalink }}"></script>
    <span class="post_email_reply"><a href="mailto:replies@example.com?Subject=Re: {{ .Title }}">📧 Reply by email</a></span>
    {{- end }}

You might have noticed that I'm not doing anything to display the upvote count on the page itself. I don't feel like the reader really needs to know how (un)popular a post may be before deciding to vote it up; the total count isn't really relevant. (Also, the Cabin stats don't update in realtime and I just didn't want to deal with that... but mostly that first bit.)

In any case, after clicking the 👍 button on a few pages I can see the `kudos` events recorded in my Cabin portal [5]:

Image: A few hits against the 'kudos' event

[5] Cabin portal

Go on, try it out:

---

📧 Reply by email

Related articles

SilverBullet: Self-Hosted Knowledge Management Web App

Generate a Dynamic robots.txt File in Hugo with External Data Sources

Automate Packer Builds with GithHub Actions

---

Home

This page on the big web