Featured image of post Use your Bluesky post for comments on your Hugo Blog

Use your Bluesky post for comments on your Hugo Blog

In this blog post, I will explain how I set up my Hugo blog to use Bluesky posts as my commenting engine and how you can do the same.

Since Bluesky is getting really popular by people I interact with, I decided to switch my blog comments to Bluesky posts.
In this blog post, I will explain how I set up my blog comments using Bluesky posts and how you can do the same. This involves several steps, including setting up the necessary HTML file(s), integrating everything, and styling the comments. Fornuately I didn’t had to start from scratch, as I had previouusly used the solution from Carl Schwan. Please check out his solution if you prefer to do your comments with Mastodon Adding comments to your static blog with Mastodon

Step 1: Creating the HTML File

First, you need to create a partial template that will be included in your article layout.

Create a new file named comments.html in the components directory:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
{{ if and (isset .Params "comments") (isset .Params.comments "id") }}
<div class="article-content">
  <h2>Comments</h2>
  <p>
      You can use your Bluesky account to
      <a class="button-link" href="https://bsky.app/profile/{{ .Site.Params.article.bluesky }}/post/{{ .Params.comments.id }}"
          target="_blank">reply</a>
      to this post. Learn how this is implemented
      <a class="link" href="https://www.menzel.it/post/2024/11/set-comments-experience-bluesky-posts/">here</a>.
  </p>
  <div id="bluesky-comments-list"></div>
  <noscript>You need JavaScript to view the comments.</noscript>
  <script src="/assets/js/purify.min.js"></script>
  <script type="text/javascript">

  document.addEventListener("DOMContentLoaded", function () {
      const commentList = document.getElementById('bluesky-comments-list');
      commentList.innerHTML = "Loading comments...";

      fetch(`https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=at://{{ .Site.Params.article.bluesky }}/app.bsky.feed.post/{{ .Params.comments.id }}&depth=10`)
          .then(response => response.json())
          .then(data => {
              const replies = data.thread.replies || [];
              replies.sort((a, b) => new Date(a.post.record.createdAt) - new Date(b.post.record.createdAt)); // Sort by date

              commentList.innerHTML = ""; // Clear loading text

              const renderComments = (comments, parentElement) => {
                  comments.forEach(reply => {
                      const author = reply.post.author;
                      let content = reply.post.record.text;
                      const createdAt = new Date(reply.post.record.createdAt).toLocaleString();

                      // Validate counts, ensure they are numbers
                      const replyCount = Number(reply.post.replyCount) || 0;
                      const repostCount = Number(reply.post.repostCount) || 0;
                      const likeCount = Number(reply.post.likeCount) || 0;

                      // Process facets to embed links and mentions
                      const facets = reply.post.record.facets || [];
                      facets.sort((a, b) => a.index.byteStart - b.index.byteStart); // Ensure facets are in order

                      let offset = 0;
                      facets.forEach(facet => {
                          const start = facet.index.byteStart + offset;
                          const end = facet.index.byteEnd + offset;
                          const originalText = content.slice(start, end);
                          let replacementText = originalText;

                          facet.features.forEach(feature => {
                              if (feature.$type === 'app.bsky.richtext.facet#link') {
                                  replacementText = `<a class="link" href="${feature.uri}" target="_blank" rel="noopener noreferrer">${originalText}</a>`;
                              } else if (feature.$type === 'app.bsky.richtext.facet#mention') {
                                  replacementText = `<a class="link" href="https://bsky.app/profile/${feature.did}" target="_blank" rel="noopener noreferrer">${originalText}</a>`;
                              }
                          });

                          content = content.slice(0, start) + replacementText + content.slice(end);
                          offset += replacementText.length - originalText.length;
                      });

                      const safeContent = DOMPurify.sanitize(content); // Sanitize the content

                      const commentHtml = `
                  <div class="comment-container">
                      <img src="${author.avatar}" alt="${author.displayName}'s avatar" class="comment-avatar">
                      <div class="comment-details">
                          <div class="comment-header">
                              <a href="https://bsky.app/profile/${author.did}" target="_blank" class="username-link">${author.displayName}</a>
                              <span class="comment-handle">@${author.handle}</span>
                          </div>
                          <div class="comment-text">${safeContent}</div>
                          <div class="comment-timestamp">${createdAt}</div>
                              <div class="comment-meta">
                                  <!-- Comment Icon and Count -->
                                  <span class="meta-item">
                                      <svg class="icon icon-comment" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
                                          <path d="M12 2C6.48 2 2 5.58 2 10c0 2.5 1.64 4.71 4.11 6.13L5 21l5.11-2.11c.61.08 1.24.11 1.89.11 5.52 0 10-3.58 10-8s-4.48-8-10-8zm0 14c-.55 0-1.1-.05-1.64-.14l-.36-.07-3.09 1.27.64-2.73-.24-.14C5.14 13.88 4 12.03 4 10c0-3.31 3.58-6 8-6s8 2.69 8 6-3.58 6-8 6z"/>
                                      </svg>
                                      <span class="icon-text">${replyCount}</span>
                                  </span>

                                  <!-- Reshare Icon and Count -->
                                  <span class="meta-item">
                                      <svg class="icon icon-reshare" viewBox="0 0 24 24" width="21" height="21" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                          <path d="m13.5 13.5 3 3 3-3"/>
                                          <path d="M9.5 4.5h3a4 4 0 0 1 4 4v8m-9-9-3-3-3 3"/>
                                          <path d="M11.5 16.5h-3a4 4 0 0 1-4-4v-8"/>
                                      </svg>                                        
                                      <span class="icon-text">${repostCount}</span>
                                  </span>

                                  <!-- Like Icon and Count -->
                                  <span class="meta-item">
                                      <svg class="icon icon-like" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                          <path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3 9.24 3 10.91 3.81 12 5.09 13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
                                      </svg>                                        
                                      <span class="icon-text">${likeCount}</span>
                                  </span>
                              </div>                        
                          </div>
                  </div>
              `;

                      const commentElement = document.createElement("div");
                      commentElement.classList.add("comment");
                      commentElement.innerHTML = commentHtml;
                      parentElement.appendChild(commentElement);

                      // Render child comments recursively
                      if (reply.replies && reply.replies.length > 0) {
                          const childContainer = document.createElement("div");
                          childContainer.classList.add("child-comments");
                          commentElement.appendChild(childContainer);
                          renderComments(reply.replies, childContainer);
                      }
                  });
              };

              renderComments(replies, commentList);
          })
          .catch(error => {
              console.error("Error fetching comments:", error);
              commentList.innerHTML = "<p>Error loading comments.</p>";
          });
  });
</script>
</div>
{{ end }}

Here’s how it works, step by step:

  • Initial Setup
    When the web page loads, the script immediately starts preparing to fetch comments after checking if the id in the comments section in Front Matter is set.
    It shows a “Loading comments…” message while it’s working behind the scenes.

  • Fetching Comments
    The script makes a request to Bluesky’s public API to retrieve comments for a specific post. This is done using a fetch request to a special Bluesky URL that includes:

  • The Bluesky account identifier
    The specific post’s unique ID
    A request to retrieve up to 10 levels of comment threads (replies to replies)

  • Processing Comments
    Once the comments are retrieved, the script does several smart things: Sorts the comments by their creation date (oldest to newest) Clears the initial “loading” text Prepares to render each comment with its details

  • Comment Rendering
    For each comment, the script creates a structured view that includes:

    1. The author’s avatar (profile picture)
    2. Author’s display name (clickable to show the profil) and handle (username)
    3. The actual text of the comment
    4. The timestamp when the comment was created
    5. Meta information like the number of replies, reposts, and likes

Link Handling & Rich Text Processing
Instead of just treating comment text as plain words, the script understands the “meaning” behind parts of the text:

  • Links Become Clickable: If someone pastes a web address, it automatically becomes a link you can click
  • User Mentions Get Special Treatment: When someone mentions another user (like @username), that becomes a link to their profile Complex Text Parsing: The script carefully goes through the text, replacing specific parts with enriched, interactive versions
    Here’s a simple example of how it works:
1
2
CopyOriginal text: "Check out this cool site https://example.com and mention @friend"
Transformed text: "Check out this cool site <a href="https://example.com">https://example.com</a> and mention <a href="https://bsky.app/profile/friend">@friend</a>"hile fetching comments (like network issues), the script will display an error message instead of breaking the page.

Step 2: Including the Comment Section in Articles

Next, include the comments.html partial in your article layout. Open the article.html file in the _default directory and add the following line where you want the comments to appear:

1
{{ partial "comments.html" . }}

Step 3: Styling the Comment Section

To style the comment section, add the necessary CSS to your custom stylesheet. You can include the styles directly in the custom.html file or in a separate CSS file.

Here is an example of the CSS for the comment section:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
<style>
.comment-container {
display: flex;
align-items: flex-start;
margin-bottom: 15px;
padding: 15px;
background-color: var(--card-background-light);
border-radius: 10px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border-light);
transition: all 0.3s ease;
}

.child-comments {
margin-left: 30px;
border-left: 2px solid var(--border-light);
padding-left: 15px;
margin-top: 10px;
}

.comment-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 10px;
}

.comment-details {
max-width: calc(100% - 60px);
}

.comment-header {
font-weight: bold;
margin-bottom: 5px;
}

.comment-header span {
font-size: 0.9em;
color: gray;
}

.comment-text {
margin-bottom: 10px;
}

.comment-timestamp {
font-size: 0.8em;
color: gray;
}

.button-link {
display: inline-block;
padding: 4px 12px;
font-size: 14px;
font-weight: bold;
text-align: center;
text-decoration: none;
border-radius: 4px;
background-color: var(--button-bg-light);
color: var(--button-text-light);
transition: background-color 0.3s;
}

.button-link:hover {
filter: brightness(1.1);
}

@media (prefers-color-scheme: dark) {
.button-link {
  background-color: var(--button-bg-dark);
  color: var(--button-text-dark);
}

.button-link:hover {
  filter: brightness(0.9); /* Slightly decrease brightness on hover */
}
}
.username-link {
  font-weight: bold;
  color: var(--link-color);
  text-decoration: none;
}

.username-link:hover {
  text-decoration: underline;
}

.comment-meta {
  display: flex;
  gap: 15px;
  margin-top: 10px;
  font-size: 0.9em;
  color: gray;
}

.meta-item {
  display: flex;
  align-items: center;
  gap: 5px;
}
</style>

Step 4: Configuring Hugo Parameters

Ensure that your config.toml file includes the necessary parameters for comments. For example:

1
2
[params.article]
  bluesky = "YOUR_BLUESKY_USERNAME"

This makes it easy to customize the Bluesky account used for comments across your site if you decide to change your username in the future.

Step 5: Setting the Comment ID

For each article where you want to enable comments, you need to set a unique ID for the comments. This ID is used to fetch the correct comments from Bluesky. You can get the post ID from the Bluesky post like this.
Bluesky Post menu
Click on Copy link to post to get the post ID There you need to copy the post ID and paste it into the Front Matter section of your article.
https://bsky.app/profile/ollim365.menzel.it/post/3laoyekihlo2j

This is how it looks in the Front Matter section of your article:

1
2
comments:
  id: "3laoyekihlo2j"

Okay, that’s it! You’ve hopefully set up your blog comments using Bluesky posts. Now you can enjoy a more interactive and engaging experience with your readers. If you have any questions or need further assistance, feel free to reach out to me. I’m always happy to help!

Comments

You can use your Bluesky account to reply to this post. Learn how this is implemented here.