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.
The workflow
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.
- Publish a blog post, which triggers a static site build
- Publish a Bluesky post which links to the published blog post
- Associate the ID of the Bluesky post with that published blog post (in a CMS, for example)
- Re-build the site
- Profit
Technical choices
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.
Optimising for performance
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.
Prerequisites to show Bluesky likes on your blog posts
- A Bluesky account
- A website
- Some blog posts
- A way to store a Bluesky post ID with your blog post data (e.g. if you write your blogs in markdown, store the post ID in your front matter; if you’re using a CMS, add a field to your blog post content model, etc)
The code
Let’s take a look at the HTML, CSS and JavaScript that makes the magic happen.
The HTML
The HTML is contained within a section element. This component contains:
- an h3 element, which will be populated with the total number of likes (your heading level element may vary),
- a link to the Bluesky post to encourage people to like it, and
- an empty ul element, ready to be filled with Bluesky avatars.
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; }
The JavaScript
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.
Some cool observations
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.
Share your results with me on Bluesky
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!

The React ecosystem offers us a lot of libraries that all are focused on the interaction of drag and drop. We have react-dnd, react-beautiful-dnd,

There have been some wonderfully interconnected things about fast software lately.

I can't say I use background-clip all that often. I'd wager it's hardly ever used in day-to-day CSS work. But I was reminded of it in a post by Stefan Judis,

Animating with requestAnimationFrame should be easy, but if you haven’t read React’s documentation thoroughly then you will probably run into a few things

Perhaps the easiest way to offer that to the user is a link that targets an ID on the element. So like...

Listen, I am no GraphQL expert but I do enjoy working with it. The way it exposes data to me as a front-end developer is pretty cool. It's like a menu of

In this week's roundup, a handy bookmarklet for inspecting typography, using await to tinker with how JavaScript modules import one another, plus Facebook's

I've recently noticed an interesting change on CodePen: on hovering the pens on the homepage, there's a rectangle with rounded corners expanding in the back.


Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

AI Hentai Generator
Generate AI Hentai for free.

Hot Article

Hot Tools

Zend Studio 13.0.1
Powerful PHP integrated development environment

SublimeText3 English version
Recommended: Win version, supports code prompts!

Dreamweaver CS6
Visual web development tools

MantisBT
Mantis is an easy-to-deploy web-based defect tracking tool designed to aid in product defect tracking. It requires PHP, MySQL and a web server. Check out our demo and hosting services.

VSCode Windows 64-bit Download
A free and powerful IDE editor launched by Microsoft