Blog Comments, Powered by Bluesky (Plus: Getting started with Bluesky)

Wed Nov 13 2024

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:

  1. Write a blogpost (the hard part)
  2. Publish the blogpost on Vercel
  3. Write and publish a Bluesky post for this blogpost
  4. Grab the ATProto URI for this post and set it to the YAML front matter for my BlueSky post
  5. 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!

Comments

You can join the conversation by replying to this thread on BlueSky

Emily Liu: amazing !!
Kevin Liao: Thank you! I'm wondering what your blog publishing flow looks like? (See the post, it's currently a five-step process and a little involved for me!)
Kevin Liao: Test Comment 2.
doooooooomiki: Mic test, eins, drei...
doooooooomiki: Yup, that's neat
Ray Berger: It looks so good! Ready for the next big blog post :)
Physics love-hater: Was there some implementation reason to prefer AT Protocol to ActivityPub, did you go with BlueSky because the community seems better than what's on Mastodon, or please say some other thing that would resolve the question I seem to be asking?
Kevin Liao: That's a good question! Honestly, I think the fact that the Relay lets me query a single location to get everyone's comments without an API key meant hacking this together took an hour of my time. I don't know how I'd approach doing this on Mastodon. Not an indictment of APub, more my skills.
Kevin Liao: Also, Starter Packs mean that in a week, I've built more of a network on Bluesky than over years of participation on Mastodon. Simply makes more sense to invest more in learning about ATProto!
Jacob Aronoff: Kevin this is so cool! You’re inspiring me
Kevin Liao: Thanks! Do you run your own PDS? I'm trying to debug why your comment is not showing up.
Kevin Liao: Hmm, it seems like a caching issue on my end.
Jacob Aronoff: i do run a PDS but my main account isn't on it
Tom Sherman: Very cool! You can make that workflow more seamless if you move your blog content onto atproto, but I know this isn't possible if you need MDX or something If you have a blog post on atproto, you can create the Bluesky post with the same rkey
Tom Sherman: Then when you render the blog post you can check for comments of the Bluesky post with the same rkey
Kevin Liao: I was thinking about this! The gears in my head are turning... The more I understand @atproto.com the more entranced I am by what it promises.