Skip to content

Save social posts one at a time from X, Bluesky and Mastodon - #68

Merged
callumalpass merged 6 commits into
mainfrom
feat/social-feed-posts
Oct 11, 2026
Merged

callumalpass merged 6 commits into
mainfrom
feat/social-feed-posts

Conversation

@callumalpass

Copy link
Copy Markdown
Contributor

Feeds are never saved as sources. On a timeline, profile or search page, Save opens a picker of the loaded posts, and a selection inside a post saves a highlight against that post. A post saved from a feed, a profile or its permalink is one record.

What changes

  • Site adapters (packages/web-capture/src/social/) for X, Bluesky and Mastodon run before article extraction. Each decides whether a page is one post or a feed (Zotero's single/multiple pattern) and lists the posts loaded on it. Mastodon is detected by page markers, so any instance works.
  • Identity is the platform's: X status ID, Bluesky at:// URI with the handle resolved to a DID, Mastodon status URI. URL variants (twitter.com, mobile., ?s=, handles) normalise to one permalink.
  • Threads: an author's self-thread becomes one source; other people's replies are left out and a quoted post is kept as context. Bluesky and Mastodon use their public, unauthenticated APIs with the page as fallback. X uses only the loaded page (no undocumented or third-party endpoints): on a conversation page the thread is the author's run of posts around the focal post. X threads are always marked partial.
  • Extension: post picker with the middle-of-viewport post preselected and multi-select; highlights record the post they were made in ([data-post-id]), and saved highlights and the page badge are redrawn per post as a feed loads and scrolls.
  • Reader: posts read with author, time, thread parts, quoted posts and media. Adding a Bluesky or Mastodon URL in Reader uses the same adapters; X explains that it needs the extension.
  • Storage: source kind and CSL type post. social_post frontmatter keeps identity and thread shape only (platform, id, url, author, published, thread root, partial flag, each part's id/url). Text and media live in the reading copy. Images are embedded (the renderer allows only data: images) after scaling to 1600px and re-encoding as WebP: at most 1.5 MiB each and 6 MiB per post; animated GIFs are kept as they are. The archive keeps every original media URL. No type-pack change is needed.

Verification

  • pnpm check passes; extension and Reader build.
  • Fixture tests cover URL normalisation and identity, each adapter's detection, listing, resolution and extraction, thread unrolling (including X conversation pages without reply markup), picker preselection, selection-to-post resolution, per-post highlight matching and the media limits.
  • Through the built extension in Chromium against public pages: Bluesky profile and post, Mastodon profile and post. Each checked canonical identity, no feed snapshot, selection anchoring, redraw after the site replaces a post's DOM, and re-anchoring after reload.

Not yet verified live: the connected save → open in Reader → highlight → reload round trip. LAB's daemon (0.1.0-beta.123) reports incompatible_version against LAB Connect, so no LAB collection is available. X has only been tested against fixtures; there was no logged-in X browser.

social_post stored every thread part's text and media next to the readable
body that already holds them. It now keeps platform, id, url, author,
published, thread root, partial flag and each part's id/url (with its quoted
post). Highlights already record their post id from both the extension and
Reader, so an unmarked quote resolves only on a single-post source instead of
by matching text across thread parts.
- X markup has no reply edges, so threads only unrolled for the fixture's
  invented attribute. On a conversation page the thread is now the author's
  contiguous run around the focal post; feeds still never join posts.
- Adapters expose entries(), pairing each post with its element in one pass.
  Feed highlight redraw ran element() per post on every mutation (quadratic)
  and now makes a single pass with quotes grouped by post.
- Images are still embedded (the renderer allows only data: images) but are
  scaled to 1600px and re-encoded as WebP first, within 1.5 MiB each and
  6 MiB per post, down from 5 and 20 MiB. Animated GIFs are kept as is.
- The archive now links every original video/audio URL, which previously
  lived only in frontmatter.
@callumalpass
callumalpass merged commit 33a3963 into main Oct 11, 2026
2 checks passed
@callumalpass
callumalpass deleted the feat/social-feed-posts branch October 11, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant