I’m really enjoying spending time on Bluesky right now. One of the things I really enjoy about the whole experience is that the project is pretty much Open Source, people are building some really cool things with the platform, and there are some nice APIs to have fun with.
I’m familiar with the Webmentions standard and how it can be used to facilitate cross-site conversations by showing data such as likes and comments/replies to links on the internet. I worked with Webmentions a few years ago to display Webmention data from other social media platforms on my site. However, it often felt like a lot of hoops to jump through, when you can just get some data from an API.
In this post, we’re going to use the Bluesky API to fetch a collection of avatars of users who have liked a Bluesky post that you have associated with a public blog post, so you can display something that looks like this on your website.
Given this website is a static site built with Eleventy, it requires a few steps to associate a published blog post with a Bluesky post.
This website is a static site that uses client-side JavaScript sparingly. The JavaScript code for this functionality runs on my blog page templates conditionally if a blog post has a Bluesky Post ID associated with it.
Alternatives to this approach would be to (in my case) use an Edge Function to modify the static HTML response at request time, but in the past I have had performance issues with calling third-party APIs in this way, such as a slower Time to First Byte (TTFB) than desired. Read How I Fixed my Brutal TTFB for more context.
Additionally, this feature on my website is a progressive enhancement, and the function of the page is not dependent on showing Bluesky likes. Therefore, if calls to the Bluesky API fail on the client, it doesn’t matter, and we can clean up the DOM appropriately. If we were running this same code on a server, it could block the rendering of the page (without proper error handling, at least), and the post wouldn’t get read. Big shame.
With my site being a static site, technically I could fetch the Bluesky data at build time and render the data statically on each blog post. However, I wanted this feature to bring joy by being a near real-time interactive experience. And plus, it wouldn’t be ideal to re-build my website every minute or so, to keep the data in sync.
Given we are loading n third-party images (user avatars), the size of the images is important. Fortunately, the Bluesky API provides at least two image sizes for each user, and we want to use the smallest one.
Additionally, given we are loading n images and we don’t know how long they will take to load or how much of an effect they will have on the page layout, some considerations have been made to avoid Cumulative Layout Shift (CLS) as much as possible. These will be outlined alongside the code examples below.
Let’s take a look at the HTML, CSS and JavaScript that makes the magic happen.
The HTML is contained within a section element. This component contains:
For the CSS classes I’m using BEM syntax, but you can use whatever CSS system you prefer. To target the DOM elements in JavaScript I’m using data-attributes prefixed with data-bsky; you can target DOM elements using CSS classes in JavaScript, but I prefer to use data-attributes to separate concerns. You could even use IDs on the elements and target those with JavaScript if you wish.
The bskyPostId associated with a blog post is added into a data-attribute on a meta tag next to this component. This is purely unique to my set-up, given that I’m building a static site, and need access to a build-time variable on the client-side in a separate JavaScript file. You may have access to your bskyPostId in your app state, for example, if you’re using a different framework. Edit as you see fit.
<meta data-bsky-post-id="${post.bskyPostId}" /> <section> <h3> The CSS </h3> <p>The CSS you see here has been slightly modified from my implementation to avoid you having to use my custom properties and personal spacing preferences. Please add what you need to make your implementation right for you.</p> <p>I’d like to call out the magic number min-height: 400px on the parent container class, .post__likes; this is to maintain a fixed height of at least 400px for the element on page load, so that the avatars don't shift the page content around as they gradually load in (the container will expand vertically on mobile). This is to prevent a bad CLS score. In the JavaScript code below, you’ll notice that I’ve specified a limit on the number of avatars fetched, based on how many avatars will fit comfortably inside this fixed-height container.<br> </p> <pre class="brush:php;toolbar:false">.post__likes { min-height: 400px; /* to avoid CLS as much as possible! */ } .post__likesTitle { font-size: 2rem; color: #000; } .post__likesCta { color: #000; font-size: 1.25rem; font-style: italic; display: block; } .post__likesList { list-style: none; padding: 0; display: flex; flex-direction: row; flex-wrap: wrap; } .post__like { width: 4rem; aspect-ratio: 1/1; margin-right: -1rem; border-radius: 100%; filter: drop-shadow(0px 0.125rem 0.125rem rgba(0, 0, 0, 0.25)); } .post__like__avatar { border-radius: 100%; } .post__like--howManyMore { width: 4rem; aspect-ratio: 1/1; display: flex; justify-content: center; align-items: center; font-size: 1rem; font-weight: bold; font-style: italic; background-color: #208bfe; /* Bluesky blue */ color: #fff; }
Disclaimer: this code is provided in plain JavaScript; you may adapt this code to your own website framework should you wish, but the beauty of writing this in plain JavaScript is that you can use it on any front end as it is.
First, you’ll need to define a few variables. The LIMIT specifies the maximum number of avatars you want to display on your page depending on how you want to display them. My limit is set to 59 because that’s how many avatars fit nicely on four rows (with extra space to display how many more likes there are). The maximum number of avatars you can fetch with this API endpoint is 100.
The bskyPostId is grabbed from the meta tag as described in the HTML section above (you may need to do this differently depending on your framework and existing code).
In order to modify the DOM after fetching data, we need to access the container, likesContainer and likesCount elements using document.querySelector().
Replace the value of myDid with your own Bluesky DID. And everything else is good to go.
<meta data-bsky-post-id="${post.bskyPostId}" /> <section> <h3> The CSS </h3> <p>The CSS you see here has been slightly modified from my implementation to avoid you having to use my custom properties and personal spacing preferences. Please add what you need to make your implementation right for you.</p> <p>I’d like to call out the magic number min-height: 400px on the parent container class, .post__likes; this is to maintain a fixed height of at least 400px for the element on page load, so that the avatars don't shift the page content around as they gradually load in (the container will expand vertically on mobile). This is to prevent a bad CLS score. In the JavaScript code below, you’ll notice that I’ve specified a limit on the number of avatars fetched, based on how many avatars will fit comfortably inside this fixed-height container.<br> </p> <pre class="brush:php;toolbar:false">.post__likes { min-height: 400px; /* to avoid CLS as much as possible! */ } .post__likesTitle { font-size: 2rem; color: #000; } .post__likesCta { color: #000; font-size: 1.25rem; font-style: italic; display: block; } .post__likesList { list-style: none; padding: 0; display: flex; flex-direction: row; flex-wrap: wrap; } .post__like { width: 4rem; aspect-ratio: 1/1; margin-right: -1rem; border-radius: 100%; filter: drop-shadow(0px 0.125rem 0.125rem rgba(0, 0, 0, 0.25)); } .post__like__avatar { border-radius: 100%; } .post__like--howManyMore { width: 4rem; aspect-ratio: 1/1; display: flex; justify-content: center; align-items: center; font-size: 1rem; font-weight: bold; font-style: italic; background-color: #208bfe; /* Bluesky blue */ color: #fff; }
Next, we’re going to define two functions that modify the DOM using the data from the Bluesky APIs.
The drawHowManyMore function only runs if there are more likes on the post than what has been fetched by the getLikes API. Again, I’m using BEM syntax for my CSS; if you’re using something different you will need to update which classes are added to the likesMore element.
The drawLikes function loops through the likes data from the getLikes API and creates an img element for each actor. Note that we replace avatar with avatar_thumbnail in the like.actor.avatar string. This is to display an image that is 128x128px, instead of the default 1000x1000px. Don’t forget the alt text attribute on the img element.
const LIMIT = 59; const bskyPostId = document.querySelector("[data-bsky-post-id]").dataset.bskyPostId; const container = document.querySelector("[data-bsky-container]"); const likesContainer = document.querySelector("[data-bsky-likes]"); const likesCount = document.querySelector("[data-bsky-likes-count]"); const myDid = "add_your_did"; const bskyAPI = "https://public.api.bsky.app/xrpc/"; const getLikesURL = `${bskyAPI}app.bsky.feed.getLikes?limit=${LIMIT}&uri=`; const getPostURL = `${bskyAPI}app.bsky.feed.getPosts?uris=`;
View the full JavaScript file on GitHub.
It only takes a few seconds from a Bluesky user liking a post to their avatar showing up on a blog post.
The likes actors are sorted by timestamp-of-like-descending, so when someone likes your post on Bluesky, they appear at the top left of the avatar list. This, I hope, creates even more joy than intended (for left-to-right reading geographies, at least).
The Bluesky getPosts API updates quicker than the getLikes API. This means that on a page refresh, the likes number is generally up-to-date, and the avatars may take another second or two to appear on another refresh.
I hope it goes without saying that I’d love to see your implementations and how you made this code work for your on your website. When you’re ready to post about it on Bluesky, tag the handle @whitep4nth3r.com in the replies, and I’ll like it to put my face on your blog post.
The above is the detailed content of How I show Bluesky likes on my blog posts. For more information, please follow other related articles on the PHP Chinese website!