···11+# <bsky-conversation>
22+33+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.
44+55+## Quick start
66+77+```html
88+<script type="module" src="https://unpkg.com/bsky-conversation"></script>
99+1010+<bsky-conversation uri="https://bsky.app/profile/did:plc:.../post/..."></bsky-conversation>
1111+```
1212+1313+That's it. No build step, no dependencies.
1414+1515+## Install via npm
1616+1717+```bash
1818+npm install bsky-conversation
1919+```
2020+2121+```js
2222+// Auto-registers the <bsky-conversation> element
2323+import 'bsky-conversation'
2424+2525+// Or import the class for manual registration
2626+import { BskyConversation } from 'bsky-conversation'
2727+customElements.define('my-conversation', BskyConversation)
2828+```
2929+3030+## Attributes
3131+3232+| Attribute | Default | Description |
3333+|-----------|---------|-------------|
3434+| `uri` | (required) | The bsky.app post URL. Use DID-based URLs for reliability. |
3535+| `max-depth` | `3` | How many levels of nested replies to show. At the cutoff, a "More of the conversation on Bluesky" link appears. |
3636+| `show-original-post` | `false` | Set to `"true"` to include the root post in the timeline. |
3737+| `engage-text` | `"Add your thoughts on Bluesky"` | CTA link text shown in the header and at the bottom. Set to `""` to hide. |
3838+| `header-template` | (none) | Custom header template string. Overrides the default header format. |
3939+4040+## Template syntax
4141+4242+The `header-template` attribute supports a mini template language.
4343+4444+**Simple tokens** — replaced with their value:
4545+4646+| Token | Value |
4747+|-------|-------|
4848+| `{replies}` | Raw reply count |
4949+| `{quotes}` | Raw quote count |
5050+| `{reposts}` | Raw repost count |
5151+| `{repostedBy}` | Linked names, e.g. `@alice, @bob, and 3 others` |
5252+| `{postUrl}` | The bsky.app post URL |
5353+5454+**Pluralization** — `{name|singular|plural}` outputs nothing when 0, `"1 singular"` when 1, `"N plural"` when 2+:
5555+5656+```
5757+{replies|reply|replies} → "" or "1 reply" or "17 replies"
5858+{quotes|quote|quotes} → "" or "1 quote" or "5 quotes"
5959+```
6060+6161+**Conditional blocks** — `{name?content}` renders content only if the value is truthy:
6262+6363+```
6464+{repostedBy?Reposted by {repostedBy}.} → "" or "Reposted by @alice, @bob."
6565+{replies?{replies|reply|replies} so far} → "" or "17 replies so far"
6666+```
6767+6868+**Full example:**
6969+7070+```html
7171+<bsky-conversation
7272+ uri="https://bsky.app/profile/did:plc:.../post/..."
7373+ header-template="This post has {replies?{replies|reply|replies}}{quotes?, {quotes|quote|quotes}}{repostedBy?, and has been reposted by {repostedBy}}."
7474+/>
7575+```
7676+7777+## CSS custom properties
7878+7979+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.
8080+8181+| Property | Light default | Dark default | Controls |
8282+|----------|--------------|-------------|----------|
8383+| `--bsky-border-color` | `#e5e7eb` | `#374151` | Separators, thread lines |
8484+| `--bsky-muted-color` | `#6b7280` | `#9ca3af` | Handles, timestamps, secondary text |
8585+| `--bsky-link-color` | `black` | `#60a5fa` | Link text color |
8686+| `--bsky-link-hover` | `#2563eb` | `#3b82f6` | Link hover color |
8787+| `--bsky-link-underline` | `rgba(82,82,91,0.5)` | `rgba(59,130,246,0.3)` | Link underline color |
8888+| `--bsky-link-underline-hover` | `rgba(59,130,246,0.3)` | `rgba(59,130,246,0.3)` | Link underline hover color |
8989+9090+```css
9191+bsky-conversation {
9292+ --bsky-link-color: #333;
9393+ --bsky-muted-color: #888;
9494+}
9595+```
9696+9797+Dark mode activates when a parent element has the `dark` class.
9898+9999+## Behavior
100100+101101+- Fetches from the public Bluesky API (no authentication needed)
102102+- Rich text rendering with proper UTF-8 byte-offset facet handling (links, @mentions, #hashtags)
103103+- Root post author's direct replies are filtered out (they're extensions of the original post, not conversation)
104104+- Hidden replies (via threadgate) and detached quotes (via postgate) are filtered out
105105+- Reply threads stay grouped — nested replies are not flattened into the timeline
106106+- Quote posts are interleaved chronologically with top-level reply threads
107107+- Reposts appear only in the header summary, not as timeline items
108108+- API failures degrade gracefully — the rest of the conversation still renders
109109+- All user content is XSS-hardened through `escapeHtml()`
110110+111111+## License
112112+113113+MIT