Table of contents
Open Table of contents
What this setup does
I wanted comments on a static Astro blog without adding a separate database or a traditional comment service. Bluesky turned out to be a workable middle ground. Every new post creates a discussion thread on Bluesky, and the blog page fetches that thread on the client side and renders it as comments.
The publishing flow
The annoying detail is timing. If you want Bluesky to generate a proper preview card, the article page has to be live first, with valid Open Graph tags in place. That requirement shapes the whole deployment flow, so I handled it with GitHub Actions.
Core workflow
- Code lands on
main, GitHub Actions builds it, and Cloudflare Pages deploys the site. - A script polls the new post URL until the page returns HTTP 200 and exposes valid
og:titleandog:imagemetadata. - After that check passes, the script calls the Bluesky API and creates a post with a manually assembled
externalembed so the link card looks right. - The script saves the returned post
uriandurlintosrc/data/bluesky-comments.json. - GitHub Actions commits that JSON change, which triggers a second deployment. On that second pass, the post step is skipped because the mapping already exists.
Guardrails
Two small checks keep this from doing the wrong thing.
- Bot commits include a specific
[bot]tag, so the workflow can tell when it should deploy only and skip the Bluesky posting step. - The script checks the mapping JSON before posting. If the article is already there, it does nothing instead of creating a duplicate thread.
How comments are rendered
Data fetching
I did not use SSR for comments. The rest of the blog is static, and I wanted to keep it that way. Comments load in the browser only when someone opens the page.
- The component reads the shared mapping JSON and finds the Bluesky post
urifor the current article. - It calls the public Bluesky
getPostThreadendpoint with a normal browserfetch. - The returned thread is flattened into the fields the UI needs, such as avatar, author name, content, likes, and reposts.
Rendering choices
- The page only renders two reply levels. Beyond that, the thread becomes hard to scan inside a blog layout.
- The comment block uses the same typography and spacing as the rest of the site, so it does not feel bolted on.
Why I picked this approach
This approach is plain, but that is the point. I do not need to run a database, store comment records myself, or keep another service alive just for a small personal blog. Bluesky’s public API is enough for this use case, and unlike X, it is still realistic to build on.
The other reason is architectural laziness in the good sense. The blog stays a static site on Cloudflare Pages, and the only dynamic part is loaded when someone actually needs it. That keeps the deployment model simple and the comment system separate from the rest of the content.
Comments
Read replies from Bluesky here.