I’ve been running this blog for a while, and I’ve always wanted a better way to engage with readers. The problem with most comment systems is that they either require users to create yet another account, are bloated with ads and trackers, or require a significant amount of infrastructure to self-host. Not worth IMO.

Recently, I’ve been spending more time on Bluesky and have been impressed with the community and the underlying AT Protocol. Inspired by a HackerNews post linking to a post from natalie.sh, I decided to build my own comment system for this Hugo blog using Bluesky threads. Full disclosure: much of this post is a summary of the content from that blog post, but tweaked for my specific Hugo use case.

The Goal: Simple, Decentralized Comments

The idea is straightforward:

  1. I write a blog post.
  2. I post about it on Bluesky.
  3. The replies to my Bluesky post automatically show up as comments on my blog.

This approach has several advantages:

  • No Backend Needed: The comments are fetched directly from Bluesky’s public API, so I don’t need to run a database or manage user accounts.
  • Meet Readers Where They Are: People can comment using their existing Bluesky identity, which means less friction and more authentic conversations.
  • Own Your Content: My blog posts remain mine, and commenters’ replies remain theirs, all on an open protocol.

In this post, I’ll walk through exactly how I added Bluesky comments to my Hugo site.

The High-Level Plan

Here’s the approach we’ll take:

  1. Configure Frontmatter: We’ll add a new field to the frontmatter of our Hugo posts to store the link to the corresponding Bluesky thread.
  2. Update Hugo Templates: We’ll modify the blog post template to include a container for the comments and the necessary JavaScript.
  3. Write the JavaScript: We’ll write a client-side script to fetch the thread data from the Bluesky API and render it as HTML.
  4. Add Some Style: We’ll write some CSS to make the comments look good.

Let’s get started.

Step 1: Configure The Frontmatter

The first step is to link our Hugo posts to their corresponding Bluesky threads. We’ll do this by adding a new bluesky_thread_uri key to the frontmatter of the markdown file. You can use either the full URL to the post or the at:// URI. Both will work.

Here’s an example using the URL:

---
title: "My Awesome Blog Post"
date: "2025-09-21"
# ... other frontmatter ...
bluesky_thread_uri: "https://bsky.app/profile/did:plc:xxxxxxxx/post/yyyyyyyyy"
---

And here’s an example using the AT URI:

---
title: "My Awesome Blog Post"
date: "2025-09-21"
# ... other frontmatter ...
bluesky_thread_uri: "at://did:plc:xxxxxxxx/app.bsky.feed.post/yyyyyyyyy"
---

To faciliate this workflow, I wrote a small Python script that I can invoke before deploying my blog. This will post to my Bluesky account and give me back the URL I can use in the frontmatter above.

Step 2: Update The Hugo Templates

Next, we need to modify our Hugo theme to display the comments. We’ll create a new comments.html partial that our post template will include. You can just stick this in layouts/partials/comments.html and it should override the theme’s implementation.

Here’s what it should look like:

{{ if .Params.bluesky_thread_uri }}
<section class="bsky-comments-section">
  <h2>Comments</h2>
  <div id="bsky-comments-host" data-thread-uri="{{ .Params.bluesky_thread_uri }}">
    <p>Loading comments...</p>
  </div>
</section>
<script src="{{ "js/bsky-comments.js" | absURL }}" defer></script>
{{ end }}

This code does a few important things:

  • It only renders the comment section if bluesky_thread_uri is set in the frontmatter.
  • It creates a container with the ID bsky-comments-host and passes the URI to it using a data-thread-uri attribute. Our JavaScript will read this.
  • It includes our bsky-comments.js script.

We also need to load our stylesheet. I’ve added the following to the theme’s head.html partial (themes/terminal/layouts/partials/head.html):

{{ if .Params.bluesky_thread_uri }}
  <link rel="stylesheet" href="{{ "css/bsky-comments.css" | absURL }}">
{{ end }}

Step 3: The JavaScript Renderer

This is where the magic happens. To solve the problem of our theme’s styles conflicting with our comment styles, we will use a Shadow DOM. This is a modern browser feature that creates a completely isolated “black box” for our comments, guaranteeing that the theme’s CSS can’t interfere.

The full source code for the renderer is available on GitHub: static/js/bsky-comments.js.

Step 4: Styling the Comments

Because we are now using a Shadow DOM, our styles are completely protected from the main theme. This allows us to have a much cleaner and more direct stylesheet.

The full stylesheet is available on GitHub: static/css/custom.css.

Conclusion

Overall, I’m thrilled with how this turned out. This is functionality that I’ve been lacking since I started this blog and it feels great to finally have this box checked. This barely took any time at all and I’m super happy with the result; thanks @natalie.sh!!!

If you have any questions or comments, just reply on BlueSky!