Blog Comments, Powered by Bluesky (Plus: Getting started with Bluesky)
I keep telling myself I'll blog more. Then I don't.
Bluesky usage has really boomed recently and I've spent some time trying to understand how the ATProto works. It's been really exciting to finally find a corner of the internet that's full of cool people instead of misinformation and AI slop. I don't claim to fully comprehend every aspect of it so far, but the best explanation I've read has been by @pfrazee.com.
@mozzius.dev built a really cool thing on his blog, which I found by the way of @emilyliu.me. Since the AppView is just free for anyone to query without an API key, I can just query the firehose on every pageload and get replies to a thread on Bluesky and display them on my website.
As a result, the current publish flow on this blog is now:
- Write a blogpost (the hard part)
- Publish the blogpost on Vercel
- Write and publish a Bluesky post for this blogpost
- Grab the ATProto URI for this post and set it to the YAML front matter for my BlueSky post
- Publish the blogpost on Vercel
This is a pretty involved process! I wonder if folks who are doing this have a better way of publishing.
Anyhow, for anyone who might have a website written in Next with Tailwind CSS, here are the two pieces of code I used for my comments section, adapted heavily from the code published by @mozzius.dev.
comment.tsx
import { rKey } from "@/lib/comments";
import { AppBskyFeedDefs, AppBskyFeedPost } from "@atproto/api";
import { ThreadViewPost } from "@atproto/api/dist/client/types/app/bsky/feed/defs";
export default async function Comment({
children,
}: {
children: ThreadViewPost;
}): Promise<JSX.Element> {
const record = children.post.record;
if (!AppBskyFeedPost.isRecord(record)) {
return <></>;
}
if (record.text.length < 3) {
return <></>;
}
let replies = <></>;
if (children.replies) {
replies = (
<>
{children.replies
.filter((r) => AppBskyFeedDefs.isThreadViewPost(r))
.map((r) => {
return (
<div
className="ml-4 pl-2 border-l-2 border-gray-500"
key={r.post.uri}
>
<Comment key={r.post.uri}>{r}</Comment>
</div>
);
})}
</>
);
}
return (
<div className="font-sans pb-4">
<b>
<a
href={`https://bsky.app/profile/${
children.post.author.did
}/post/${rKey(children.post.uri)}`}
>
{children.post.author.displayName ?? children.post.author.handle}
</a>
</b>
: {record.text}
{replies}
</div>
);
}
commentsection.tsx
import { AtpAgent, AppBskyFeedDefs } from "@atproto/api";
import Comment from "@/components/comment";
import { rKey } from "@/lib/comments";
export default async function CommentSection({
uri,
}: {
uri: string;
}): Promise<JSX.Element> {
if (!uri) return <p className="text-center">No comments</p>;
const agent = new AtpAgent({ service: "https://public.api.bsky.app" });
const response = await agent.getPostThread({ uri });
const thread = response.data.thread;
if (!AppBskyFeedDefs.isThreadViewPost(thread)) {
return <p className="text-center">Could not find thread</p>;
}
if (!thread.replies || thread.replies.length === 0) {
return <p className="text-center">No comments yet</p>;
}
return (
<div className="mx-auto max-w-3xl mt-8">
<h2 className="text-3xl font-sans font-extrabold">Comments</h2>
<p className="font-sans">
You can join the conversation by replying to{" "}
<a
className="hover:underline"
href={`https://bsky.app/profile/${thread.post.author.did}/post/${rKey(
thread.post.uri
)}`}
>
<b>this thread</b>
</a>{" "}
on BlueSky
</p>
<div className="mt-8 text-neutral-700">
{thread.replies.map((reply) => {
// if the reply is blocked/deleted, skip it
if (!AppBskyFeedDefs.isThreadViewPost(reply)) return null;
return <Comment key={reply.post.uri}>{reply}</Comment>;
})}
</div>
</div>
);
}
I hope this helps and I'll see you on Bluesky!