Show the author's own chain on Bluesky and Threads
CI / Typecheck, test, build (pull_request) Successful in 46s

People write in chains on both, and a link into one arrives pointing at a
single post out of several. Showing only that post loses the thing that was
being said. Other people's replies are a different matter: they are a
conversation rather than the thing that was shared, and on a busy post there
are hundreds of them.

A Post is now a list of Segments instead of one body. Most platforms produce
exactly one and say so through oneSegment(); the two that thread produce the
whole chain, with isAnchor marking the post that was actually linked, which
need not be the first.

Bluesky walks parent upward and the author's own replies downward, stopping at
the first post by anyone else. That needs depth and parentHeight on
getPostThread, which drags the entire reply tree along -- a few hundred KB on a
popular post -- because there is no way to ask the API for one author's branch.

Threads is harder to read. The page ships the linked post, the author's
follow-ups, other people's replies and a pile of unrelated recommendations, all
as flat thread_items containers with no nesting to go on. What separates a
follow-up from a stranger's reply is that a follow-up is the author replying to
themselves; a reply from someone else carries the same reply_to_author with a
different name on it. The first post of a chain replies to nothing at all, so
it is reachable only by walking backwards from the post that answers it -- a
test caught that, when linking the second post of a thread returned just the
one post.

Fixtures for both are real captures. The Bluesky one keeps two of every level's
outside replies rather than pruning them away, because a filter is only worth
testing against the thing it is supposed to exclude.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BGkRmLfiWuJHx6tQ12EELY
This commit is contained in:
2026-08-26 17:16:02 -03:00
co-authored by Claude Opus 5
parent 2a8f71adc7
commit 60a9468875
16 changed files with 1184 additions and 115 deletions
+13 -2
View File
@@ -62,6 +62,10 @@ Request → `src/routes/post.ts` → `src/platforms/index.ts` maps prefix to ada
Adding a platform is one file in `src/platforms/` plus one row in the table in
`index.ts`. Everything downstream already handles a `Post`.
A `Post` is a list of `Segment`s, not a single body. Most platforms produce one
(`oneSegment` in `types.ts`); Bluesky and Threads produce the author's whole chain,
with `isAnchor` marking the post that was linked — which need not be the first.
Things worth knowing before editing:
- **One Chromium, one context, persistent.** Cookies and dismissed banners accumulate on
@@ -88,7 +92,9 @@ Things worth knowing before editing:
- **Bluesky** — asks the public API directly (still through the browser context), so it
is the most reliable. The web app calls `getPostThreadV2` now; the page fallback
deliberately matches only V1.
deliberately matches only V1. Threads are built by walking `parent` up and the
author's own `replies` down; `depth`/`parentHeight` are what make that possible, at
the cost of dragging the whole reply tree along (a few hundred KB on a busy post).
- **X** — the `platform.twitter.com` embed calls the syndication endpoint; we catch that
response. A quote post carries no media of its own, so the quoted post's media is used.
- **Instagram** — the least reliable. It ships the structured payload only some of the
@@ -102,7 +108,12 @@ Things worth knowing before editing:
segment and are rebuilt in `buildOriginalUrl`.
- **Threads** — same media schema as Instagram (`src/platforms/meta-media.ts`). Its
payloads are full of empty stub nodes, so the finder only accepts a node with actual
candidates in it.
candidates in it. The page ships the linked post, the author's follow-ups, other
people's replies and unrelated recommendations all as flat `thread_items`
containers. A follow-up is the author replying to *themselves*, which is what
separates it from a stranger's reply carrying the same `reply_to_author`. The first
post of a chain replies to nothing, so it is only reachable by walking backwards
from the one that answers it.
## Verification puzzles