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:
{{ if and (isset .Params "comments") (isset .Params.comments "id") }}
<divclass="article-content"><h2>Comments</h2><p> You can use your Bluesky account to
<aclass="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
<aclass="link"href="https://www.menzel.it/post/2024/11/set-comments-experience-bluesky-posts/">here</a>.
</p><divid="bluesky-comments-list"></div><noscript>You need JavaScript to view the comments.</noscript><scriptsrc="/assets/js/purify.min.js"></script><scripttype="text/javascript">document.addEventListener("DOMContentLoaded",function(){constcommentList=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=>{constreplies=data.thread.replies||[];replies.sort((a,b)=>newDate(a.post.record.createdAt)-newDate(b.post.record.createdAt));// Sort by date
commentList.innerHTML="";// Clear loading text
constrenderComments=(comments,parentElement)=>{comments.forEach(reply=>{constauthor=reply.post.author;letcontent=reply.post.record.text;constcreatedAt=newDate(reply.post.record.createdAt).toLocaleString();// Validate counts, ensure they are numbers
constreplyCount=Number(reply.post.replyCount)||0;constrepostCount=Number(reply.post.repostCount)||0;constlikeCount=Number(reply.post.likeCount)||0;// Process facets to embed links and mentions
constfacets=reply.post.record.facets||[];facets.sort((a,b)=>a.index.byteStart-b.index.byteStart);// Ensure facets are in order
letoffset=0;facets.forEach(facet=>{conststart=facet.index.byteStart+offset;constend=facet.index.byteEnd+offset;constoriginalText=content.slice(start,end);letreplacementText=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>`;}elseif(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;});constsafeContent=DOMPurify.sanitize(content);// Sanitize the content
constcommentHtml=`
<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>
`;constcommentElement=document.createElement("div");commentElement.classList.add("comment");commentElement.innerHTML=commentHtml;parentElement.appendChild(commentElement);// Render child comments recursively
if(reply.replies&&reply.replies.length>0){constchildContainer=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:
The author’s avatar (profile picture)
Author’s display name (clickable to show the profil) and handle (username)
The actual text of the comment
The timestamp when the comment was created
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 <ahref="https://example.com">https://example.com</a> and mention <ahref="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:
<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:0px4px6pxrgba(0,0,0,0.1);border:1pxsolidvar(--border-light);transition:all0.3sease;}.child-comments{margin-left:30px;border-left:2pxsolidvar(--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-headerspan{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:4px12px;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-color0.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.
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.