Skip to main content

Adding CodePen oEmbeds to your Drupal site

Published on

 

I feel that whenever I attempt something with code, nothing ever just works. There's some bug or some roadblock that has to be overcome. I manage to find something that goes awry. Apparently, that is the case with embedding CodePen pens via oEmbed today!

Drupal 8.6.0 added oEmbed support for media! Out of the box, only remote video providers Vimeo and YouTube are supported. But a quick hook implementation exposes more providers. The providers are based on a repository provided by the oEmbed website. The repository is here: https://oembed.com/providers.json

So I tried to add CodePen. Here's my snippet of code that I added to my module so that it would be available as a source for my media type:

/**
 * Implements hook_media_source_info_alter().
 */
function mglaman_dev_media_source_info_alter(array &$sources) {
  $sources['oembed:codepen'] = [
    'id' => 'codepen',
    'label' => new TranslatableMarkup('CodePen'),
    'description' => new TranslatableMarkup('Embed a CodePen.'),
    'allowed_field_types' => ['string'],
    'default_thumbnail_filename' => 'no-thumbnail.png',
    'providers' => ['Codepen'],
    'class' => 'Drupal\media\Plugin\media\Source\OEmbed',
    'forms' => [
      'media_library_add' => OEmbedForm::class
    ],
    'provider' => 'mglaman_dev',
  ];
}

Great, off to the races! Naaaaaah. Not so much. I kept getting errors that the URL was invalid and a resource couldn't be fetched. So, I popped on Xdebug to see what was going on.

This is the entry for CodePen from the oEmbed repository:

  {
    "provider_name": "Codepen",
    "provider_url": "https://codepen.io",
    "endpoints": [
      {
        "schemes": [
          "http://codepen.io/*",
          "https://codepen.io/*"
        ],
        "url": "https://codepen.io/api/oembed"
      }
    ]
  },

Here's the matching blog post where CodePen announced oEmbed support: https://blog.codepen.io/documentation/oembed/. Their embed format is the following:

https://codepen.io/api/oembed?format=json&url=URL-TO-PEN-HERE

The oEmbed repository is missing one feature: defining additional query parameters. The CodePen oEmbed requires the format query parameter. Even though no other value is supported (like XML or YAML).

Given the CodePen https://codepen.io/mglaman/pen/oNBRMeW, Drupal ends up generating the following URL: 

https://codepen.io/api/oembed/?url=https://codepen.io/mglaman/pen/oNBRMeW

And that returns a 404 because there is no format query parameter 👎🏻. Luckily, there is a hook to alter an oEmbed resource URL to make modifications just like these: hook_oembed_resource_url_alter().

Here's my hook, and we're off to the races! A valid URL! Woohoo!

/**
 * Implements hook_oembed_resource_url_alter().
 */
function mglaman_dev_oembed_resource_url_alter(array &$parsed_url, \Drupal\media\OEmbed\Provider $provider) {
  if ($provider->getName() === 'Codepen') {
    $parsed_url['query']['format'] = 'json';
  }
}

And, once again. I'm smacked with a brick wall and an error. The provider cannot be found.

But. Wait? CodePen is in the provider repository. I was able to assemble the URL. What in the world? Well, let's look at the results of the full oEmbed URL: https://codepen.io/api/oembed/?url=https://codepen.io/mglaman/pen/oNBRM…

{
  "success": true,
  "type": "rich",
  "version": "1.0",
  "provider_name": "CodePen",
  "provider_url": "https://codepen.io",
  "title": "Locale formatting of time elements",
  "author_name": "Matt Glaman",
  "author_url": "https://codepen.io/mglaman",
  "height": "300",
  "width": "800",
  "thumbnail_width": "384",
  "thumbnail_height": "225",
  "thumbnail_url": "https://assets.codepen.io/42103/internal/screenshots/pens/oNBRMeW.default.png?fit=cover&format=auto&ha=false&height=360&quality=75&v=2&version=1619715151&width=640",
  "html": "<iframe id=\"cp_embed_oNBRMeW\" src=\"https://codepen.io/mglaman/embed/preview/oNBRMeW?default-tabs=html%2Cresult&amp;height=300&amp;host=https%3A%2F%2Fcodepen.io&amp;slug-hash=oNBRMeW\" title=\"Locale formatting of time elements\" scrolling=\"no\" frameborder=\"0\" height=\"300\" allowtransparency=\"true\" class=\"cp_embed_iframe\" style=\"width: 100%; overflow: hidden;\"></iframe>"
}

Notice the provider_name value? It's CodePen. The oEmbed resource repository has it as Codepen. Drupal cannot find the appropriate provider because the names mismatch. This is a good thing. It's a security measure. oEmbed is actually pretty dangerous as you're allowing a third-party website to embed random HTML.

To fix this, I have opened an issue and a pull request against the oEmbed repository

This will fix the oEmbed provider repository. However, I don't want to wait for this to be merged to have the ability to embed a CodePen item into my site. There must be a way!

There isn't. There are no hooks to handle modifying this response. 

But. You can decorate the media.oembed.provider_repository service to provide a workaround!


UPDATE! dpi has made me aware of the oEmbed Providers module which allows you to customize the providers repository via a user interface. This way you do not need to write a decorated service. And maybe it would fix the format query parameter.


Here's my service definition:

services:
  mglaman_dev.oembed.provider_repository:
    class: Drupal\mglaman_dev\DecoratedProviderRepository
    decorates: media.oembed.provider_repository
    parent: media.oembed.provider_repository

Here's my decorated class, with built-in backward compatibility in case my pull request gets merged.

<?php declare(strict_types=1);

namespace Drupal\mglaman_dev;

use Drupal\media\OEmbed\ProviderRepository;

final class DecoratedProviderRepository extends ProviderRepository {

  public function get($provider_name) {
    if ($provider_name === 'CodePen') {
      try {
        return parent::get($provider_name);
      }
      catch (\InvalidArgumentException $e) {
        return parent::get('Codepen');
      }
    }
    return parent::get($provider_name);
  }

}

AND IT WORKS!

If you think I should contribute this to a media_codepen module, let me know and I will.

#