Posting to Bluesky via the API from PHP – Part Four – Link Cards

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.

Example of a Link Card

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.

Leave a Reply

Your email address will not be published. Required fields are marked *