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. Understanding my Bluesky API Workflow 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: Create an authenticated session Retrieve liked post URIs for an "actor" Use these URIs to fetch oEmbed embed HTML Clean and format the embed code The Complete Implementation Let's break down each function and its purpose: 1. Creating a Bluesky Session 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: This function authenticates your Bluesky account. Note: this example hardcodes strings for credentials, but this should be avoided in production use cases. It returns an accessJwt JWT (JSON Web Token) for subsequent API calls Uses the createSession endpoint from Bluesky's ATP (Authenticated Transfer Protocol) Error handling ensures graceful failure if authentication fails 2. Retrieving Liked Post URIs 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: Requires an authenticated session token Uses the getActorLikes endpoint to fetch liked posts Important -- the endpoint domain should be https://bsky.social, as this is an authenticated request. Extracts unique URIs for each liked post Limits to 40 posts (configurable) 3. Converting URIs to Embeddable HTML export 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: Uses Bluesky's oEmbed endpoint with post URIs to access a post's embeddable HTML Optional: Utilizes prettier to format the HTML consistently Optional: Removes <script> tags for security and clean embedding The reason for this is that I embed a single Bluesky script for each post containing Bluesky content. Flexible error handling Putting It All Together: A Complete Example 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); } } Potential Enhancements This solution works for me because all that I require is a statically generated monthly blog post. Some improvements could include: Add pagination support for fetching more than 40 liked posts Implement caching to reduce unnecessary API calls Create a more robust error handling mechanism Creating mechanism for refreshing accessJwt token if used in long running processes Sorting liked posts by popularity (likes) Troubleshooting Tips Verify your Bluesky credentials are correct Check that the Bearer token is being set correctly on your authenticated requests. Verify that the endpoint domains you are using are all valid. Conclusion Embedding 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. Next Steps Experiment with the code Customize the embed styling Explore additional Bluesky API endpoints Enjoy and happy tinkering! 🚀 Learn how to programmatically fetch and embed your liked Bluesky posts using authentication, API calls, and oEmbed endpoints. 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. Bluesky 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. Bluesky's API Understanding my Bluesky API Workflow 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: Create an authenticated session Retrieve liked post URIs for an "actor" Use these URIs to fetch oEmbed embed HTML Clean and format the embed code Create an authenticated session Retrieve liked post URIs for an "actor" Use these URIs to fetch oEmbed embed HTML oEmbed embed HTML Clean and format the embed code The Complete Implementation Let's break down each function and its purpose: 1. Creating a Bluesky Session 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; } }; 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: Key Insights: This function authenticates your Bluesky account. Note: this example hardcodes strings for credentials, but this should be avoided in production use cases. It returns an accessJwt JWT (JSON Web Token) for subsequent API calls Uses the createSession endpoint from Bluesky's ATP (Authenticated Transfer Protocol) Error handling ensures graceful failure if authentication fails This function authenticates your Bluesky account. Note: this example hardcodes strings for credentials, but this should be avoided in production use cases. Note: this example hardcodes strings for credentials, but this should be avoided in production use cases. Note: this example hardcodes strings for credentials, but this should be avoided in production use cases. It returns an accessJwt JWT (JSON Web Token) for subsequent API calls accessJwt Uses the createSession endpoint from Bluesky's ATP (Authenticated Transfer Protocol) createSession Error handling ensures graceful failure if authentication fails 2. Retrieving Liked Post URIs 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; }; 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: Key Insights: Requires an authenticated session token Uses the getActorLikes endpoint to fetch liked posts Important -- the endpoint domain should be https://bsky.social, as this is an authenticated request. Extracts unique URIs for each liked post Limits to 40 posts (configurable) Requires an authenticated session token Uses the getActorLikes endpoint to fetch liked posts the getActorLikes Important -- the endpoint domain should be https://bsky.social, as this is an authenticated request. an authenticated request. Extracts unique URIs for each liked post Limits to 40 posts (configurable) 3. Converting URIs to Embeddable HTML export 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; } }; export 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: Key Insights: Uses Bluesky's oEmbed endpoint with post URIs to access a post's embeddable HTML Optional: Utilizes prettier to format the HTML consistently Optional: Removes <script> tags for security and clean embedding The reason for this is that I embed a single Bluesky script for each post containing Bluesky content. Flexible error handling Uses Bluesky's oEmbed endpoint with post URIs to access a post's embeddable HTML Bluesky's oEmbed endpoint Optional: Utilizes prettier to format the HTML consistently prettier Optional: Removes <script> tags for security and clean embedding The reason for this is that I embed a single Bluesky script for each post containing Bluesky content. <script> The reason for this is that I embed a single Bluesky script for each post containing Bluesky content. The reason for this is that I embed a single Bluesky script for each post containing Bluesky content. Flexible error handling Putting It All Together: A Complete Example 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); } } 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); } } Potential Enhancements This solution works for me because all that I require is a statically generated monthly blog post. Some improvements could include: Add pagination support for fetching more than 40 liked posts Implement caching to reduce unnecessary API calls Create a more robust error handling mechanism Creating mechanism for refreshing accessJwt token if used in long running processes Sorting liked posts by popularity (likes) Add pagination support for fetching more than 40 liked posts Implement caching to reduce unnecessary API calls Create a more robust error handling mechanism Creating mechanism for refreshing accessJwt token if used in long running processes accessJwt Sorting liked posts by popularity (likes) Troubleshooting Tips Verify your Bluesky credentials are correct Check that the Bearer token is being set correctly on your authenticated requests. Verify that the endpoint domains you are using are all valid. Verify your Bluesky credentials are correct Check that the Bearer token is being set correctly on your authenticated requests. Verify that the endpoint domains you are using are all valid. Conclusion Embedding 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. Next Steps Experiment with the code Customize the embed styling Explore additional Bluesky API endpoints Experiment with the code Customize the embed styling Explore additional Bluesky API endpoints Enjoy and happy tinkering! 🚀