atproto as a static site's comment section
Publish on your own site, discuss on bluesky by Luna Novaskip to §comment-section to see it in action.
Accepting comments is cool, swapping from a very cacheable and easy to keep alive static site to something interactive can be a pain.
Embedding a live comment section into a static site has become fairly popular in the last decade;embed_history these solutions don't typically integrate well with social media that viewers are familiar with.
I was inspired to implement comment embedding on lunnova.dev after seeing gracekind.net's comment section.gracekind
atproto is the decentralizedish protocol behind bluesky.
why atproto?§
Core reason: I like interacting on bsky. Some less personal justifications:
- there's a free API I can call without worrying about traffic to it from my small site being a problem
- bsky's built-in moderation tools for disconnecting replies allow pruning bad comments
- atproto data is extremely scrapeable and exportable
- if the platform dies out, is superceded, or public APIs become hard to find we can scrape old comments to make them a permanent archive
how atproto?§
We'll be focusing on a very minimal interaction with an atproto AppView today. An AppView is responsible for redistributing content to clients.
For today's project we will make one call to the public.api.bsky.app
AppView, getPostThread
.
We could call an alternative AppView that supports the app.bsky
RPCs like the one associated with futur.blue's zeppelin.social. If you do, I urge you to fund the AppView you're relying on to ensure that it's sustainable.
invoking app.bsky.feed.getPostThread§
The bsky AppView's app.bsky.feed.getPostThread docs show that we need a uri, and optionally a depth.
Let's try it out by writing a minimal example that can display a recent tangled.sh update post. without replies.
JSX fetch & markup example
Simplified result and JSX example, styling and unused fields omitted for brevity.
$ let resp = await fetch("https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=at%3A%2F%2Ftangled.sh%2Fapp.bsky.feed.post%2F3lptwcb47kc2u&depth=0");
$ let thread = await resp.json(); thread
{"thread": { "post": {
"author": {
"handle": "tangled.sh", "displayName": "Tangled",
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:wshs7t2adsemcrrd4snkeqli/bafkreieng4ts4h6g7prbdiaseag3lmsewxwzbxmexy2qqd5uebq5pe567e@jpeg"
},
"record": {
"text": "gm tanglers, we have got an assortment of updates for y'all! first up: \n\nnative support for stacked PRs with #jj-vcs!\n\nwhat this means:\n- you can break down that mega PR into smaller ones\n- reviewers can review/comment/merge each one individually\n- ✨nobody is blocked✨"
},
}}}
JSX:
<><div>
<img src={thread.post.author.avatar.replace("/avatar/", "/avatar_thumbnail/")} alt={thread.post.author.displayName}/>
<div>
<div>{thread.post.author.displayName}</div>
<div>@{thread.post.author.handle}</div>
</div>
</div>
<div>
{thread.post.record.text}
</div>
</>
native support for stacked PRs with #jj-vcs! what this means: - you can break down that mega PR into smaller ones - reviewers can review/comment/merge each one individually - ✨nobody is blocked✨
For our full implementation we need to render replies, facets (@mentions, liks) and embeds.
The rest of the owl can be found at github:LunNova/x/atproto-comments.
integrating this into your site§
- Make atproto-comments.js and its associated css file available on your site
- Publish your post normally.
- Start a thread on bluesky about your post.
- Add the comment section with a reference to that postat_uri
<div id="comment-section"></div>
<script type="module" defer>
import Comments from '/atproto-comments.js';
new Comments(
document.getElementById('comment-section'),
"/comments.css",
'https://public.api.bsky.app/',
'at://did:plc:j3hvz7sryv6ese4nuug2djn7/post/3ltikv7zewc2l'
).render();
</script>
If you find atproto-comments
useful, let me know. This project is intended mostly as a fire and forget finished project; I encourage copying it to your own site and changing it to fit your site as needed. PRs welcome if you find something generally applicable to fix.
You might be able to comment on this post on the fediverse @ fed.brid.gy/bsky/lunnova.dev thanks to bridgy fed.
You'll need to follow @bsky.brid.gy@bsky.brid.gy
to activate bridging your replies back to atproto for them to show up here.
A mix of solutions have become popular ranging from commercial projects like disqus, to repurposing github comments, to purpose built self hostable embeddable comment sections like statique or uncomment
gracekind.net's blog section has a bsky comments section on all posts as of 202506. See Why Aren't Human-Bot Conversations More Engaging? for an example
atproto is theoretically quite decentralized. In practice the majority of people who may see your content are using a Bluesky PBC web app that connects to services owned by the same company, but in cases of bsky PBC service outages people on their own infra have been able to keep posting and interacting through the outages. Hoping to avoid a bunch of nitpicking about how decentralized it is by putting this ref note in; I may be sowing my own demise here.
You can find the permanent at:// URI from the bsky webapp by looking at the uri field in the response to the getPostThread call the webapp makes when you view the post. It should start at://did:
. If you're doing this often, at://wormhole by @alice.mosphere.at makes this more convenient. Pick the pdsls option on its dropdown then copy the URI.
Cite as BibTeX
@misc{atproto-static-site-comments,
author = {Luna Nova},
title = {atproto as a static site's comment section},
year = {2025},
url = {https://lunnova.dev/articles/atproto-static-site-comments/},
howpublished = {https://lunnova.dev/articles/atproto-static-site-comments/},
urldate = {2025-07-13},
note = {lunnova.dev - Publish on your own site, discuss on bluesky}
}