NOTE: This post is mainly about how I developed the php2Bluesky library. If you are looking for information on using the library in your own code please see here: https://github.com/williamsdb/php2Bluesky/
In this we post we are continuing to look at accessing Bluesky from the API via PHP. Previously we looked at making a connection to Bluesky, posting text, images and handling links and in this post we take a look at Link Cards.
What Even IS a Link Card?
Link Cards are the snippets of a website that is linked to a post that you will no doubt be familiar with seeing elsewhere. Typically, elsewhere these are handled for you but not in Bluesky – here you do all the heavy lifting.
Fetching Details of a Link
In order to attach a link card to a Bluesky post you have to provide:
- the url of the link
- the subject and description to display
- an image to show
To do this we retrieve the page at the given URL and examine the DOM to find the information that we require. This does rely on a the page being well-formed and having the elements that we require. WordPress sites seem to work well, for example.
The following code snippet takes in a Bluesky connection and a url granbs the page details, uploads any associated image and returns the formatted array that you need to insert into the parameters to pass on to Bluesky.
function fetch_link_card($connection, $url) {
// The required fields for every embed card
$card = [
"uri" => $url,
"title" => "",
"description" => "",
];
// Create a new DOMDocument
$doc = new DOMDocument();
// Suppress errors for invalid HTML, if needed
libxml_use_internal_errors(true);
// Load the HTML from the URL
$doc->loadHTMLFile($url);
// Restore error handling
libxml_use_internal_errors(false);
// Create a new DOMXPath object for querying the document
$xpath = new DOMXPath($doc);
// Query for "og:title" and "og:description" meta tags
$title_tag = $xpath->query('//meta[@property="og:title"]/@content');
if ($title_tag->length > 0) {
$card["title"] = $title_tag[0]->nodeValue;
}
$description_tag = $xpath->query('//meta[@property="og:description"]/@content');
if ($description_tag->length > 0) {
$card["description"] = $description_tag[0]->nodeValue;
}
// If there is an "og:image" meta tag, fetch and upload that image
$image_tag = $xpath->query('//meta[@property="og:image"]/@content');
if ($image_tag->length > 0) {
$img_url = $image_tag[0]->nodeValue;
// Naively turn a "relative" URL (just a path) into a full URL, if needed
if (!parse_url($img_url, PHP_URL_SCHEME)) {
$img_url = $url . $img_url;
}
$image = $this->upload_media_to_bluesky($connection, $img_url);
}
$embed = '';
$embed = [
'embed' => [
'$type' => 'app.bsky.embed.external',
'external' => [
'uri' => $card['uri'],
'title' => $card['title'],
'description' => $card['description'],
'thumb' => $image,
],
],
];
return $embed;
}
Because the image now lives on some remote server we need to make a change to our existing upload_media_to_bluesky
function. This is to take account of the fact that mime_content_type
only works on local not remote files. Therefore, we need to grab the file and check the headers for the the mime type information.
function upload_media_to_bluesky($connection, $filename)
{
// upload the file
$body = file_get_contents($filename);
if(filter_var($filename, FILTER_VALIDATE_URL)){
$headers = get_headers($filename, 1);
if (isset($headers['Content-Type'])) {
$mime = $headers['Content-Type'];
} else {
// this is a problem so needs to be handled!
}
}else{
$mime = mime_content_type($filename);
}
$response = $connection->request('POST', 'com.atproto.repo.uploadBlob', [], $body, $mime);
$image = $response->blob;
return $image;
}
Building the Parameters
Like before we next have to create the parameter array in the correct format expected by Bluesky. This requires a separate “embed” block for the link card. This is done as follows and is already included in the code above:
$embed = [
'embed' => [
'$type' => 'app.bsky.embed.external',
'external' => [
'uri' => $card['uri'],
'title' => $card['title'],
'description' => $card['description'],
'thumb' => $image,
],
],
];
Again this then needs to be merged with the parameters that we created for sending text to Bluesky using the array_merge function to slot the image parameters into the correct place.
$args['record'] = array_merge($args['record'], $embed);
// send to bluesky
$connection->request('POST', 'com.atproto.repo.createRecord', $args);
Putting it all Together
I deliberately haven’t included a full worked example in this post as that is covered in the next and final post.
Very very helpful for me. I wanted to add those cards via my automation tool (n8n). I found your code and was really able to update my JS code and it worked more or less directly out of the box.
Now we can federate our link previews very easy also to bluesky 💛💚
Thank you very much for writing this down!
You’re welcome – glad that it was useful.