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:
- Fetching Post Data: Use
app.bsky.feed.searchPosts
to locate posts linked to your page’s URL. - Fetching Threads: Retrieve threads of comments with
app.bsky.feed.getPostThread
. - Sorting and Displaying: Sort comments chronologically and dynamically populate the comment section.
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) +
' ' +
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}
🔄 ${reply.post.repostCount || 0}
♥️ ${reply.post.likeCount || 0}
<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:
- Comments load as expected.
- The stats (likes, replies, reposts) update correctly.
- Error handling displays meaningful messages if Bluesky’s API fails.
Comments
Reply on Bluesky here to join the conversation.