{
  "$type": "site.standard.document",
  "description": "feat: add a Bluesky-powered comment section to Astro blog posts",
  "path": "/posts/bluesky-comments-in-astro/",
  "publishedAt": "2026-04-17T00:00:00.000Z",
  "site": "https://read.ryancowl.es",
  "tags": [
    "Code"
  ],
  "textContent": "I've been wanting to add comments to this blog, but I didn't want to deal with a third-party comment system or manage a database. Then I realized I already have a place where people can discuss posts. Bluesky.\n\nBluesky's API is public and requires no auth for reading. So the idea is simple. Post about a blog entry on Bluesky, drop the post's AT URI into the frontmatter, and let a Vue component fetch the reply thread and render it as comments. No accounts to manage, no moderation tools to build, and the conversation lives where people are already hanging out.\n\n  \n\nHow it works\n\nWhen a blog post has a  in its frontmatter, a Vue component loads on the page and calls the public Bluesky API to fetch the reply thread. Replies get sorted by likes and rendered with the author's avatar, handle, and timestamp. Each comment links back to the original post on Bluesky so people can jump in and reply directly.\n\nThe component uses Astro's [](https://docs.astro.build/en/reference/directives-reference/#clientidle) directive, so it loads after the page is interactive and doesn't block the initial render.\n\n  \n\nSetting it up\nAdd the field to the content schema\n\nI added an optional  string to the blog collection schema in .\nBuild the component\n\nThe  component takes a single prop (the AT URI) and does everything client-side.\n\nThe public API endpoint is [](https://docs.bsky.app/docs/api/app-bsky-feed-get-post-thread). It accepts a  parameter (the AT URI of the post) and a  parameter (how many levels of replies to fetch). I'm fetching up to six levels deep so nested conversations can show up without needing another request.\nWire it into the post layout\n\nIn , the component conditionally renders when the frontmatter includes a .\n\nNo env vars, no API keys, no server-side code.\nAutomate the Bluesky post\n\nI didn't want to manually copy AT URIs every time I publish, so I put together a small Node script that handles the round trip. It scans for blog posts missing a , creates a Bluesky post with the title and link, and writes the URI back into the frontmatter.\n\nThe script uses a Bluesky App Password for auth, passed in via environment variables ( and ). After running it, the frontmatter gets updated with the AT URI and I just commit and deploy. This post was published that way.\n\n  \n\nDomain as a Bluesky handle\n\nWhile I was at it, I also set up  as my Bluesky handle. Bluesky lets you verify a domain by adding a DNS TXT record.\n\nA nice bit of identity verification that ties the blog and the Bluesky account together.\n\n  \n\nWhat I like about this approach\n\nThe conversation stays on Bluesky, which means I don't need to build moderation, authentication, or spam filtering. People comment using their existing accounts. And since the API is public, there's no server-side component to maintain.\n\nIf I forget to create a Bluesky post for something, the comment section just doesn't appear. Graceful degradation by default.\n\nIf you scroll down, you should see it in action :)\nResources\nBluesky API Documentation\nAT Protocol Specification\nAstro Client Directives",
  "title": "Adding Bluesky Comments to an Astro Blog"
}