Pragmatism in the real world

Embedding Notist slides

This site uses WordPress under the hood as I find the flexibility that a good CMS provides quite useful. For the talks section, I use a custom post type so that I can set additional properties on the post and customise the display. With my usual lack of imagination, my custom post type is called talk.

When Notist was released, I’ve been uploading the PDFs for my presentations there so that I have a nicely rendered online version. If I remember, I also add relevant tweets to the Notes presentation too. However, as I like to have a page on this site for each talk, I then embed the Notist slides back here.

I used to have a rather hard-coded solution that only supported 16:9 aspect ratio presentations, but as a friend suggested that I should write it down, I reached out to Drew McLellan to see how I could improve it. Rather helpfully, he pointed me at the right way to do it!

oEmbed

Drew informed me that Notist uses oEmbed. The oEmbed website describes it as:

oEmbed is a format for allowing an embedded representation of a URL on third party sites. The simple API allows a website to display embedded content (such as photos or videos) when a user posts a link to that resource, without having to parse the resource directly.

This is exactly what we need & for Notist, the API call is:

https://noti.st/api/oembed?url=https://noti.st/{username}/{presentation code}

This will then return a payload along these lines:

$ curl https://noti.st/api/oembed?url=https://noti.st/akrabat/3fdyeY | jq
{
  "type": "rich",
  "version": "1.0",
  "title": "Capturing Light",
  "author_name": "Rob Allen",
  "author_url": "https://noti.st/akrabat",
  "provider_name": "Notist",
  "provider_url": "https://noti.st",
  "cache_age": 604800,
  "thumbnail_url": "https://on.notist.cloud/slides/deck3929/large-0.png",
  "thumbnail_width": 1600,
  "thumbnail_height": 900,
  "html": "<p data-notist=\"akrabat/3fdyeY\">View <a href=\"https://noti.st/akrabat/3fdyeY\">Capturing Light</a> on Notist.</p><script async src=\"https://on.notist.cloud/embed/002.js\"></script>",
  "width": 960,
  "height": 540
}

There’s three pieces of information we need: html, cache_age & the aspect ratio (width/height).

Implementation in WordPress

To implement this within my custom post type, I registered these post meta data fields:

  • talk_notist_code
  • talk_notist_embed_html
  • talk_notist_embed_expires
  • talk_notist_embed_aspect

In my talk post type’s admin page, I provide an input field for talk_notist_code as this is the unique code from Notist and I need to enter it on my page as the link between the talk page on my site and the presentaiton on Notist.

The others (talk_notist_embed_html, talk_notist_embed_expires & talk_notist_embed_aspect) hold the embedded HTML to render, the time when we need to refetch that HTML and the aspect ratio of the presentation respectively. I hide them in the admin UI and never think about them.

Fetching the HTML

To fetch the HTML code to embed, I wrote this function:

/**
 * Retrieve the HTML embed code, the time when it expires & its aspect ratio for a Notist presentation
 *
 * @param string $username       Notist username
 * @param string $notist_code    Notist code for this presentation
 * @return [string, int, float]  Array containing embed code and expiry (unix time) & aspect ratio
 */
function get_notist_embed_html(string $username, string $notist_code): array
{
    $url = "https://noti.st/api/oembed?url=https://noti.st/$username/$notist_code";
    $string_data = file_get_contents($url);
    if ($string_data === false) {
        return ['', 0, 0];
    }

    $data = json_decode($string_data, true);
    if (!is_array($data)) {
        return ['', 0, 0];
    }

    if (isset($data['html'])) {
        $cache_age = $date['cache_age'] ?? 604800; // 7 days
        $aspect_ratio = ($data['thumbnail_width'] ?? 16) / ($data['thumbnail_height'] ?? 9);
        return [
            $data['html'],
            time() + $cache_age,
            $aspect_ratio,
        ];
    }
}

This function grabs the JSON from Notist’s oEmbed API and then extracts the HTML and cache_age. We add the cache_age to the output of time() so that we know when to refetch. The aspect ratio can be calculated by dividing the thumbnail’s width and height.

One nice thing about PHP is that file_get_contents() is a really easy way to do a GET request on an API. I’ve put in some minimal error checking to hopefully ensure that it won’t fall over if the Notist API is down or returning something expected. I’ll add error logs and alerting at some point too. Note that as we need to return multiple pieces of information, we return an array with the information.

Store the HTML and timeout

We don’t want to have to fetch this HTML every time the page is loaded on our site, so we store it into WordPress’s post_meta and only fetch it if we don’t have it. As I’m a little lazy, I do this in content-talk.php:

$notist_username = 'akrabat';
$notist_code = get_post_meta($postId, 'talk_notist_code', true);
$notist_embed_html = get_post_meta($postId, 'talk_notist_embed_html', true);
$notist_embed_expires = get_post_meta($postId, 'talk_notist_embed_expires', true);
$notist_embed_aspect = get_post_meta($postId, 'talk_notist_embed_aspect', true);

if (empty($notist_embed_html) || time() > $notist_embed_expires) {
    // No embed HTML or it's expired: get new embed HTML.
    $info = get_notist_embed_html($notist_username, $notist_code);

    list($notist_embed_html, $notist_embed_expires, $notist_embed_aspect) = $info;
    if ($notist_embed_html) {
        update_post_meta($postId, 'talk_notist_embed_html', $notist_embed_html);
        update_post_meta($postId, 'talk_notist_embed_expires', $notist_embed_expires);
        update_post_meta($postId, 'talk_notist_embed_aspect', $notist_embed_aspect);
    }
}

We retrieve notist_embed_html, notist_embed_expires & notist_embed_aspect from the WordPress post_meta and if there is no HTML or it has expired, we fetch them using our shiny new get_notist_embed_html(). Assuming we actually found some HTML, we then update the post_meta fields with the new HTML code, the updated expiry time and the aspect ratio, ready for the next page load.

Render the HTML

We can now render the HTML that will embed the presentation in our page. As we’re not totally trusting, we put the HTML that we retrieved in a sandboxed iframe for security. Who knows what is in the JS file that Notist gives us?!

if ($notist_embed_html) {
    $notist_embed_html = htmlentities($notist_embed_html);
    $percentage        = 100.0 / $notist_embed_aspect;

    echo <<<HTML
<h4>Slides</h4>
<div style="position: relative; overflow: hidden; padding-top: $percentage%;">
    <iframe sandbox="allow-scripts" scrolling="no" frameborder="0"
        style="position:absolute; top:0; left:0; width:100%; height:100%; border:0;"
        srcdoc="$notist_embed_html"></iframe>
</div>
HTML;
}

We need a div and an iframe in order to make our embedded presentation responsive. The containing div needs a padding-top that is related to the aspect ratio (75% for 4:3, 56.25% for 16:9), which is why we calculated and stored it earlier. My HTML/CSS isn’t the strongest, so maybe there’s an easier way?

You can see it in action on the Capturing Light talk page.

That’s it

With this small bit of code, not only can I embed Notist slides on my talk pages, but as and when Notist improves their slide player, I’ll pick up the improvements automatically. With the use of a cache, I also avoid too many API calls too.

There’s some WordPress specific code here, but this approach can be generalised to any website. It’d be nice if Notist supplied a WordPress plugin, but until then, this works for me.