Skip to content

feat: Add support for Bluesky card previews#161

Open
pdubroy wants to merge 1 commit intohumanwhocodes:mainfrom
pdubroy:main
Open

feat: Add support for Bluesky card previews#161
pdubroy wants to merge 1 commit intohumanwhocodes:mainfrom
pdubroy:main

Conversation

@pdubroy
Copy link

@pdubroy pdubroy commented Feb 12, 2026

I noticed that when my post has a link, Bluesky doesn't automatically add an embed card. For example, compare these two posts:

This PR adds support for Bluesky embed cards, via embed with $type: "app.bsky.embed.external" as described here.

Here's a test post that was generated via the code in this PR: https://bsky.app/profile/ohmjs.org/post/3meog7agbhw25

@nzakas
Copy link
Contributor

nzakas commented Feb 12, 2026

Thanks for the pull request. I need to take some time to think about this because cardPreview is Bluesky-specific and this utility was meant to be more or less universal across all strategies. Adding options that only apply to one strategy isn't really the direction I want to go as that will clutter the API. I think ideally the Bluesky strategy would being able to generate a card preview automatically but not sure how feasible that is.

@pdubroy
Copy link
Contributor Author

pdubroy commented Feb 13, 2026

Ok, makes sense. FYI I have a little script that uses crosspost which was largely vibecoded; here's is how it's fetching the info for the embed card:

async function fetchOgData(url) {
  const res = await fetch(url, {
    headers: {'User-Agent': 'crosspost-bot/1.0'},
    redirect: 'follow',
    signal: AbortSignal.timeout(10000),
  });
  const html = await res.text();

  const og = name => {
    const m =
      html.match(
        new RegExp(`<meta[^>]+property=["']og:${name}["'][^>]+content=["']([^"']+)["']`, 'i')
      ) ??
      html.match(
        new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:${name}["']`, 'i')
      );
    return m?.[1] ?? null;
  };

  return {
    uri: url,
    title: og('title') ?? '',
    description: og('description') ?? '',
    thumb_url: og('image') ?? null,
  };
}

Obviously doing it with a RegEx is bit nasty. Here's what it would look like using metascraper:

import metascraper from 'metascraper';
import metascraperDescription from 'metascraper-description';
import metascraperImage from 'metascraper-image';
import metascraperTitle from 'metascraper-title';

const scraper = metascraper([
  metascraperDescription(),
  metascraperImage(),
  metascraperTitle(),
]);

export async function fetchOgData(url) {
  const res = await fetch(url, {
    headers: {'User-Agent': 'og-fetch/1.0'},
    redirect: 'follow',
  });
  const html = await res.text();
  const metadata = await scraper({html, url});
  return {
    uri: url,
    title: metadata.title ?? '',
    description: metadata.description ?? '',
    thumb_url: metadata.image ?? null,
  };
}

Seems pretty clean. (And ~72KB of dependencies in total, in case it matters.)

If you'd prefer to do it that way, I'm happy to submit another PR that adds this to lookup the embed info for the first link in the post. That should cover 90% of use cases.

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.

2 participants