Bluesky facet checker
Links, @mentions and #tags in a Bluesky post are "facets", and their positions are counted in UTF-8 bytes, not characters. This page builds correct facets, or checks a post's facets and tells you what's off. Runs in your browser; nothing you type is sent anywhere unless you press a button that says it fetches.
Preview
Facets JSON
| Type | Text | Bytes | Characters | JS index (UTF-16) |
|---|
Result
| # | Type | Bytes | Covers | Verdict |
|---|
Why byte offsets?
A facet looks like {"index": {"byteStart": 5, "byteEnd": 18}, "features": [...]}. The numbers point into the
post text encoded as UTF-8. ASCII letters are 1 byte, but é is 2, most CJK characters are 3, and an emoji like 🙂 is 4
(family emoji can be 18+). So one emoji before a link shifts every later offset by 3 bytes compared with counting characters, and
the link lands on the wrong text, or the post is rejected.
JavaScript makes it worse: "🙂".length is 2 (UTF-16 code units), Python's len("🙂") is 1, and neither is 4.
The fix: encode the text to UTF-8 first (new TextEncoder().encode(text), text.encode("utf-8")), then measure.
The 300-character post limit is different again: it counts graphemes (what a person sees as one character), which this page shows above the preview.
Longer version, with Python and JavaScript snippets and why readme.md turns into a link:
Why is my Bluesky link on the wrong text?
Limits, honestly
- Build mode uses the same detection rules as the official
@atproto/apilibrary (v0.30.3): links, bare domains, @mentions, #tags and $cashtags. It was checked against that library on 18,000+ random texts with zero differences. That includes the quirks:readme.mdbecomes a link (.mdis a real TLD),notes.txtdoesn't, and neither doesExample.COM. - Mentions only become facets once they're resolved to a DID (press Resolve @mentions).
- The "likely cause" hint only spots the two classic mistakes (character offsets, UTF-16 offsets).
- bsky-tiny, a one-file Python client, uses the same rules; both are tested against each other.