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.

Looks up handles via Bluesky's public API (public.api.bsky.app). Mentions need a DID, so they're left out until resolved.

Preview

Facets JSON


  
TypeTextBytesCharactersJS index (UTF-16)

If the last three columns differ, code that uses character or JS string positions will put this facet on the wrong text.

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