We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
In the previous post, we learned how to add mentions to your post when posting to Bluesky.
The last part of this series will learn you how to add website cards to your posts.
You'll see that it resembles the way you can add tags and mentions, but with an extra step.
Step 1: Get the preview image for the website
As we want our website card to have a nice image, we need to fetch it from the URL. To do so, we are going to use the Open Graph image.
Don't worry, you don't need to do this manually. You can use the opengraph_parser module for this. We can install it by adding the following dependency to your mix.exs
file:
{:opengraph_parser, "~> 0.6.0"}
Once installed, you can use it fetch the Open Graph image using the website's URL. Just in case there is no image, I'm adding a fallback to the favicon of the website by using the Favicon Extractor web service.
# First, fetch the body of the URL, limiting the redirects to avoid possible endless loops
body = Req.get!(url, max_redirects: 1).body
# Then use the OpenGraph module to get the OpenGraph info
og_info = OpenGraph.parse(body)
# The fallback URL in case there is no image
hostname = URI.parse(url).host
favicon_url = "https://www.faviconextractor.com/favicon/#{hostname}?larger=true"
# Extract the image
image_url = og_info.image || favicon_url
Step 2: Uploading the image as a blob
The next step is that you need to upload the image as a blob so that you can later on use it in the website card.
We'll first get the actual image from the URL so that we have the raw image data. We also determine the image content type as we need it to create the blob.
The blob can then be created by using the com.atproto.repo.uploadBlob
function.
# Fetch the image from the URL
image = Req.get!(image_url)
# Get the content type of the image
image_content_type = image |> Req.Response.get_header("content-type")
# Create the actual blob image
blob =
Req.post!(
"https://bsky.social/xrpc/com.atproto.repo.uploadBlob",
headers: %{
"Content-Type" => image_content_type,
"Accept" => "application/json"
},
auth: {:bearer, accessJwt},
body: image.body
).body["blob"]
The blob value now contains a structure like this:
%{
"$type" => "blob",
"mimeType" => "image/jpeg",
"ref" => %{"$link" => "bafkreih4ittooqpzhubsiwchajmxjbo4uyskqyqoeojrmiixxgofcz3yay"},
"size" => 54499
}
Step 3: Creating the post record
Instead of facets (which you can still add if you want), we now add an embed
record to the post. This record contains the information of the website card to embed and looks like:
# Create the current timestamp in ISO format with a trailing Z
created_at = DateTime.utc_now() |> DateTime.to_iso8601()
# The text to post
text = "Hello from Yellow Duck with a website card"
# Create the post record
record =
%{
text: text,
createdAt: created_at,
"$type": "app.bsky.feed.post",
langs: ["en-US"],
embed: %{
"$type": "app.bsky.embed.external", # This type indicates a website card
external: %{
uri: url, # The URL to the website
title: title, # The title to the website
description: description, # The description of the website
thumb: blob # The resulting structure from step 2
}
}
}
You can also use the Open Graph info to retrieve the title and description of the website.
Step 4: Post to Bluesky
We can now use the same code from the previous posts to publish the record:
%{"commit" => %{"rev" => rev}} =
Req.post!(
"https://bsky.social/xrpc/com.atproto.repo.createRecord",
auth: {:bearer, token},
json: %{
repo: did, # The did value from step 1
collection: "app.bsky.feed.post", # We want to post to our timeline
record: record # The record we want to post to our timeline
}
).body
If all goes well, the website card should show up in the post.
Conclusion
This ends the series on posting to Bluesky from Elixir.
As you can see, once you know how all the bits and pieces fit together, it's not that hard to automate.
Since this doesn't require too much code, I'm always included to just write a little module myself to handle this rather than relying on an external library which I don't have any control over.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.