# <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 `
```
That's it. No build step, no dependencies.
## 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 to let you customize how you introduce the conversation on your site.
**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:**
```html
```
## Styling timeline items
The component assigns distinct CSS classes to each type of timeline item, so you can style replies, threaded replies, and quote posts independently:
| Selector | What it matches |
|----------|----------------|
| `.bsky-conversation .reply` | A direct reply to the original post |
| `.bsky-conversation .thread .reply` | A nested reply within a thread (child of another reply) |
| `.bsky-conversation .quote` | A quote post |
| `.bsky-conversation .original` | The original post (when `show-original-post="true"`) |
| `.bsky-conversation .thread` | The `
` wrapper around nested replies — has a left border by default |
For example, to visually distinguish quote posts from replies:
```css
bsky-conversation .quote {
background: #f8f8fa;
border-radius: 8px;
padding: 1em;
}
```
## 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:
```css
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:
```html
```
If your site uses a different convention (e.g., `data-theme="dark"` or `prefers-color-scheme`), you can set the properties yourself:
```css
@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 directly 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
- If quote or repost API calls fail, the thread still renders without that data. If the thread itself cannot be fetched, the component renders nothing.
- All user content is XSS-hardened through `escapeHtml()`
## Moderation
Sometimes people will reply or repost in a manner you don't want to appear on your site. You can use Bluesky's built in tools to help you manage this.
To hide a reply, go to the post on bsky.app, select "Hide reply from everyone" from the ellipsis menu, and it will no longer appear on the page.
If someone quotes your post and you detach that quote, it will no longer appear in the conversation.
Additionally, this package includes an optional script that takes the full URL of a post that you wish to hide. In order for this to work, you will need to create a `.env` file and include your bsky handle and an [app password](https://bsky.app/settings/app-passwords).
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
```bash
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:
```bash
npm install
```
### Usage
```bash
# 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