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.