Add Bluesky replies, quotes, and reposts to any web page. Handy for adding a comments section anywhere.
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

JavaScript 92.7%
HTML 7.3%
8 1 0

Clone this repository

https://tangled.org/jimray.locket.computer/bsky-conversation https://tangled.org/did:plc:lysqukqdu6hsrhet5v2brjgo/bsky-conversation
git@tangled.org:jimray.locket.computer/bsky-conversation git@tangled.org:did:plc:lysqukqdu6hsrhet5v2brjgo/bsky-conversation

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

<bsky-conversation>#

A zero-dependency web component that displays a Bluesky conversation thread — replies, quote posts, and reposts — for any public Bluesky post. Drop it into any page with a single <script> tag.

Quick start#

<script type="module" src="https://unpkg.com/bsky-conversation"></script>

<bsky-conversation uri="https://bsky.app/profile/did:plc:.../post/..."></bsky-conversation>

That's it. No build step, no dependencies.

Install via npm#

npm install bsky-conversation
// Auto-registers the <bsky-conversation> element
import 'bsky-conversation'

// Or import the class for manual registration
import { BskyConversation } from 'bsky-conversation'
customElements.define('my-conversation', BskyConversation)

Attributes#

Attribute Default Description
uri (required) The bsky.app post URL. Use DID-based URLs for reliability.
max-depth 3 How many levels of nested replies to show. At the cutoff, a "More of the conversation on Bluesky" link appears.
show-original-post false Set to "true" to include the root post in the timeline.
engage-text "Add your thoughts on Bluesky" CTA link text shown in the header and at the bottom. Set to "" to hide.
header-template (none) Custom header template string. Overrides the default header format.

Template syntax#

The header-template attribute supports a mini template language.

Simple tokens — replaced with their value:

Token Value
{replies} Raw reply count
{quotes} Raw quote count
{reposts} Raw repost count
{repostedBy} Linked names, e.g. @alice, @bob, and 3 others
{postUrl} The bsky.app post URL

Pluralization{name|singular|plural} outputs nothing when 0, "1 singular" when 1, "N plural" when 2+:

{replies|reply|replies}        → "" or "1 reply" or "17 replies"
{quotes|quote|quotes}          → "" or "1 quote" or "5 quotes"

Conditional blocks{name?content} renders content only if the value is truthy:

{repostedBy?Reposted by {repostedBy}.}    → "" or "Reposted by @alice, @bob."
{replies?{replies|reply|replies} so far}   → "" or "17 replies so far"

Full example:

<bsky-conversation
  uri="https://bsky.app/profile/did:plc:.../post/..."
  header-template="This post has {replies?{replies|reply|replies}}{quotes?, {quotes|quote|quotes}}{repostedBy?, and has been reposted by {repostedBy}}."
/>

CSS custom properties#

The component defines design tokens with sensible defaults, overridable from the host page. All internal sizing uses em units, so it scales with inherited font size.

Property Light default Dark default Controls
--bsky-border-color #e5e7eb #374151 Separators, thread lines
--bsky-muted-color #6b7280 #9ca3af Handles, timestamps, secondary text
--bsky-link-color black #60a5fa Link text color
--bsky-link-hover #2563eb #3b82f6 Link hover color
--bsky-link-underline rgba(82,82,91,0.5) rgba(59,130,246,0.3) Link underline color
--bsky-link-underline-hover rgba(59,130,246,0.3) rgba(59,130,246,0.3) Link underline hover color

Override example:

bsky-conversation {
  --bsky-link-color: #333;
  --bsky-muted-color: #888;
}

Dark mode#

The component ships with a built-in dark palette that activates when any ancestor has the dark class. This works with common dark mode patterns:

<!-- Toggle dark class on <html> or <body> -->
<html class="dark">
  <body>
    <bsky-conversation uri="..."></bsky-conversation>
  </body>
</html>

If your site uses a different convention (e.g., data-theme="dark" or prefers-color-scheme), you can set the properties yourself:

@media (prefers-color-scheme: dark) {
  bsky-conversation {
    --bsky-border-color: #374151;
    --bsky-muted-color: #9ca3af;
    --bsky-link-color: #60a5fa;
    --bsky-link-underline: rgba(59, 130, 246, 0.3);
    --bsky-link-hover: #3b82f6;
    --bsky-link-underline-hover: rgba(59, 130, 246, 0.3);
  }
}

Behavior#

  • Fetches from the public Bluesky API (no authentication needed)
  • Rich text rendering with proper UTF-8 byte-offset facet handling (links, @mentions, #hashtags)
  • Root post author's direct replies are filtered out (they're extensions of the original post, not conversation)
  • Hidden replies (via threadgate) and detached quotes (via postgate) are filtered out
  • Reply threads stay grouped — nested replies are not flattened into the timeline
  • Quote posts are interleaved chronologically with top-level reply threads
  • Reposts appear only in the header summary, not as timeline items
  • API failures degrade gracefully — the rest of the conversation still renders
  • All user content is XSS-hardened through escapeHtml()

Moderation#

The hide-reply script lets you hide replies or detach quote posts from conversations via the command line. It auto-detects the post type.

Setup#

cp .env.example .env

Fill in your ATPROTO_HANDLE and ATPROTO_APP_PASSWORD (create an app password in Bluesky Settings > App Passwords). Then install the dev dependencies:

npm install

Usage#

# Hide a reply (adds to threadgate hiddenReplies)
npm run hide-reply https://bsky.app/profile/did:plc:.../post/...

# Detach a quote post (adds to postgate detachedEmbeddingUris)
npm run hide-reply https://bsky.app/profile/did:plc:.../post/...

The script auto-detects whether the URL is a reply or a quote post:

  • Replies: Walks up the thread to find the root post and adds the reply URI to the root post's app.bsky.feed.threadgate record. Equivalent to "Hide reply for everyone" on bsky.app.
  • Quote posts: Detects the embedded post and adds the quote URI to the root post's app.bsky.feed.postgate record. Equivalent to "Detach quote" on bsky.app.

The authenticated user must own the root post being replied to or quoted.

License#

MIT