Learn how to programmatically fetch and embed your liked Bluesky posts using authentication, API calls, and oEmbed endpoints.
I've recently made the move over to Bluesky. I can already confirm that there is a vibrant tech community there with tons of interesting, useful, and inspiring content. I'm a happy new user! As a result, I've been wanting to embed my top liked Bluesky posts in my "Dev roundup" monthly newsletter posts. My aim is to provide a curated list of Bluesky posts that is specifically tailored to Software Developers.
Luckily, Bluesky's API is completely free to use, allowing programmatic access to all of the content within. This tutorial will walk you through the process of retrieving and embedding liked Bluesky posts using their API, perfect for personal blogs, portfolios, or content aggregation projects.
I have built a script that allows me to automatically embed my Bluesky posts in a markdown blog post. I think that any or all of the steps used in this script are valuable for many use-cases.
To summarize my workflow for embedding liked posts, we follow these key steps:
Let's break down each function and its purpose:
export const createSession = async (): Promise<string | null> => {
try {
const response = await fetch(
"https://bsky.social/xrpc/com.atproto.server.createSession",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
identifier: "your-handle",
password: "your-password",
}),
}
);
const responseJson = await response.json();
return responseJson.accessJwt;
} catch (e) {
console.error("Failed to create session: ", e);
return null;
}
};
Key Insights:
accessJwt
JWT (JSON Web Token) for subsequent API callscreateSession
endpoint from Bluesky's ATP (Authenticated Transfer Protocol)export const getBlueskyLikeUris = async (actor: string, limit: number = 40) => {
const token = await createSession();
if (!token) {
console.error("Failed to get token");
return;
}
const response = await fetch(
"https://bsky.social/xrpc/app.bsky.feed.getActorLikes?actor=${actor}&limit=${limit}",
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const responseJson = await response.json();
const uris = responseJson.feed.map((entry: any) => entry.post.uri);
return uris;
};
Key Insights:
getActorLikes
endpoint to fetch liked postsexport const getBlueskyPostEmbedMarkup = async (uri: string) => {
try {
const response = await fetch(`https://embed.bsky.app/oembed?url=${uri}`);
const responseJson = await response.json();
const formattedHTML = prettier.format(responseJson.html, {
parser: "html",
plugins: [require("prettier/parser-html")],
htmlWhitespaceSensitivity: "ignore",
printWidth: 1000,
});
return formattedHTML.replace(/<script[\s\S]*?<\/script>/g, "");
} catch (e) {
console.error("Failed to get Bluesky post embed markup");
return null;
}
};
Key Insights:
prettier
to format the HTML consistently<script>
tags for security and clean embedding
async function embedLikedPosts() {
try {
// Get liked post URIs
const likedPostUris = await getBlueskyLikeUris();
if (!likedPostUris) {
console.error("No liked posts found");
return;
}
// Convert URIs to embed HTML
const embedPromises = likedPostUris.map(getBlueskyPostEmbedMarkup);
const embedHtmlArray = await Promise.all(embedPromises);
// Filter out any failed embeds
const validEmbeds = embedHtmlArray.filter(embed => embed !== null);
// Return the markup for all liked posts
return `
## Some Fave Posts 🦋
${validEmbeds.join(`\n\n`)}
`
} catch (error) {
console.error("Error embedding Bluesky posts:", error);
}
}
This solution works for me because all that I require is a statically generated monthly blog post.
Some improvements could include:
accessJwt
token if used in long running processesEmbedding Bluesky posts provides a dynamic way to showcase your social media interactions. By understanding the API workflow and implementing robust error handling, you can create engaging, personalized, and curated content integrations.
Enjoy and happy tinkering! 🚀