Ronalds Vilciņš

How to Add Bluesky Comments to Your Hugo Website



Adding Bluesky comments to your Hugo site is a great way to integrate decentralized discussions into your platform. This guide outlines the steps required, using JavaScript and Hugo templates to fetch and display comments from Bluesky.

Step 1: Prepare the Environment

Ensure your Hugo website is set up and ready for customization. Familiarity with Hugo templates and JavaScript is necessary.

Step 2: Create the Comment Display Section in Hugo Templates

Edit your single.html file (or equivalent). Add a placeholder for Bluesky comments, along with necessary scripts.

<div class="bluesky-stats">
  <small>
    💬 <span id="reply-count">0</span>
    🔄 <span id="repost-count">0</span>
    ♥️ <span id="like-count">0</span>
  </small>
</div>

<h2>Comments</h2>
<p class="comment-prompt">
    Reply on Bluesky <a href="#" target="_blank" rel="noopener noreferrer">here</a> to join the conversation.
</p>
<div id="bluesky-comments"></div>
<script src="/js/bluesky-comments.js"></script>

Step 3: Fetch and Display Comments Using JavaScript

Use a custom JavaScript file to interact with Bluesky’s API. Below is an overview of bluesky-comments.js, already optimized to fetch and display comments. Key functionalities include:

function unorphanize(element, count = 1) {
  // Get HTML content
  let html = element.innerHTML;
  
  // Store HTML tags
  const tags = html.match(/<([A-Z][A-Z0-9]*)\b[^>]*>/gi) || [];
  const placeholders = tags.map((_, i) => `__${i}__`);
  
  // Replace tags with placeholders
  tags.forEach((tag, i) => {
    html = html.replace(tag, placeholders[i]);
  });
  
  // Add non-breaking spaces
  for (let i = 0; i < count; i++) {
    const lastSpaceIndex = html.lastIndexOf(' ');
    if (lastSpaceIndex > 0) {
      html = html.substring(0, lastSpaceIndex) + 
             '&nbsp;' + 
             html.substring(lastSpaceIndex + 1);
    }
  }
  
  // Restore tags
  tags.forEach((tag, i) => {
    html = html.replace(placeholders[i], tag);
  });
  
  element.innerHTML = html;
}

// First, add a script variable to store the original post URL
let blueskyPostUrl = '';

async function loadBlueskyComments() {
  const currentUrl = window.location.href;
  const commentsDiv = document.getElementById('bluesky-comments');
  // Clear existing content
  commentsDiv.innerHTML = '';
  const commentsList = document.createElement('ul');
  commentsDiv.appendChild(commentsList);

  try {
    const searchParams = new URLSearchParams({ q: currentUrl });
    const searchResponse = await fetch(
      `https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts?${searchParams}`,
      { headers: { Accept: "application/json" } }
    );

    if (!searchResponse.ok) throw new Error("Failed to search posts");
    const searchData = await searchResponse.json();

    // Update stats and post URL for the first matching post
    if (searchData.posts && searchData.posts[0]) {
      const post = searchData.posts[0];
      // Update stats
      document.getElementById('reply-count').textContent = post.replyCount || 0;
      document.getElementById('repost-count').textContent = post.repostCount || 0;
      document.getElementById('like-count').textContent = post.likeCount || 0;
      // Update comment link
      blueskyPostUrl = `https://bsky.app/profile/${post.author.did}/post/${post.uri.split('/').pop()}`;
      const commentPromptLink = document.querySelector('.comment-prompt a');
      if (commentPromptLink) {
        commentPromptLink.href = blueskyPostUrl;
      }
    }

    // For each post found, fetch its thread
    const allComments = [];
    for (const post of searchData.posts) {
      const threadParams = new URLSearchParams({ uri: post.uri });
      const threadResponse = await fetch(
        `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?${threadParams}`,
        { headers: { Accept: "application/json" } }
      );

      if (threadResponse.ok) {
        const threadData = await threadResponse.json();
        if (threadData.thread?.replies) {
          allComments.push(...threadData.thread.replies);
        }
      }
    }

    // Sort all comments by time
    const sortedComments = allComments.sort((a, b) =>
      new Date(a.post.indexedAt) - new Date(b.post.indexedAt)
    );

    sortedComments.forEach(reply => {
      if (!reply?.post?.record?.text) return;
      const author = reply.post.author;

      const li = document.createElement('li');
      li.innerHTML = `
        <small>
          <a href="https://bsky.app/profile/${author.did}" target="_blank">
            ${author.displayName || author.handle}
          </a>
          <span class="author-handle">@${author.handle}</span>
        </small>
        <p>${reply.post.record.text}</p>
        <small>
          💬 ${reply.post.replyCount || 0}&nbsp;
          🔄 ${reply.post.repostCount || 0}&nbsp;
          ♥️ ${reply.post.likeCount || 0}&nbsp;
          <a href="https://bsky.app/profile/${reply.post.author.did}/post/${reply.post.uri.split('/').pop()}" target="_blank">
            Link
          </a>
        </small>
      `;
      // Apply unorphanize to the list item
      unorphanize(li);
      commentsList.appendChild(li);
    });
  } catch (error) {
    commentsDiv.innerHTML = `<p>Error loading comments: ${error.message}</p>`;
  }
}

document.addEventListener('DOMContentLoaded', loadBlueskyComments);

Step 4: Add CSS for Styling (Optional)

Customize the appearance of your comments section using CSS.

.bluesky-stats {
  margin: 10px 0;
}
.comment-prompt {
  margin-bottom: 20px;
}

Step 5: Test and Debug

Ensure the functionality works across different pages by testing:

  1. Comments load as expected.
  2. The stats (likes, replies, reposts) update correctly.
  3. Error handling displays meaningful messages if Bluesky’s API fails.


0  0  0

Comments

Reply on Bluesky here to join the conversation.