Add a New Embed Type

Space Madness works hard to make the content writing and editing process easy. One way of doing this is providing a simple "Embed" component in the Sanity Studio block editor.

Copy your URL, paste it into the Embed URL box, and click "Publish". That's all there is to it.

Underneath that great UX is a flexible system that helps us craft an optimal embed experience. We control what embeds are included and how they will render. This guide will help you dig into the inner workings of this system as we'll add a new embed renderer for CodePen.

Inspecting the User Experience

Let's start by taking a closer look at the user experience for adding an embed to a Sanity document. We'll try with two URLs: one that is supported, and one that isn't supported yet.

Copy this to your clipboard:

https://www.youtube.com/watch?v=O5nskjZ_GoI&list=PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo&index=2

Head over to your Sanity Studio and select a document for testing.

Click into the block editor and add an Embed block.

Paste in the URL from your clipboard. You should see no errors.

Click Publish, then go visit your page. The embed works. Dead Simple.

Now let's try with an unsupported service.

Copy this to your clipboard:

https://codepen.io/bramus/pen/JjvEExW

Now repeat the process.

You should see something like this:

Sanity Studio with invalid CodePen URL

This is a good experience! The content author is immediately alerted to the issue and the document cannot be published.

Space Madness has a clever system for building new Embeds that also includes validation of URLs within the Sanity Studio. Let's start building our new CodePen embed to learn out how.

Update the Embed Registry

All of our code for embeds lives in the sanity-astro-embeds package. This package is pulled into our Astro site to render the embeds on our page. Some of its functions are also pulled into the Sanity site to validate the embed URLs. We won't have to worry much about the structure since we only need to make edits in one place.

Let's explore the file packages/sanity-astro-embeds/src/embedRegistry.tsx.

This file might look a bit scary. It mixes Regular Expressions, React, and some gnarly Typescript, but it's relatively easy to make sense of it if we break it down. Let's start by taking a look at the Twitter entry in the registry object.

packages/sanity-astro-embeds/src/embedRegistry.tsx
twitter: {
  title: "Twitter",
  regexp: /^https?:\/\/twitter\.com/,
  getRenderProps: (urlOrId: string) => {
    const renderProps = {
      wrapperClass: "aspect-video",
    } as ComponentPropsWithoutRef<typeof YouTube> & {
      wrapperClass: string;
    };
    const tweetIdRegex = /^https?:\/\/twitter\.com\/([\w-]+\/status\/\d+)/;
    const match = urlOrId.match(tweetIdRegex);
    renderProps.tweetLink = match ? match[1] : "";
    return renderProps;
  },
  render: (props: ComponentPropsWithoutRef<typeof Tweet>) =>
    props.tweetLink ? <Tweet {...props} /> : null,
},

Let's break down each attribute:

title sets a human readable title for the embed service.

regexp is a Regular Expression that we use to check if a URL is supported. We're using a simple URL match to twitter.com. It's not perfect, but it'll probably work for most use cases.

getRenderProps is a function that will take the ID or URL from an input component and churn out data that matches the props of an embed component. We do this separately in case there is extra build time work we want to do. We want our output in the render function to be static.

render is the function that renders the React embed component. It takes the props from the getRenderProps function and passes them to an MDX Embed component.

We're making use of the fantastic library. It has an extensive list of components for popular services. We won't have to reinvent the wheel here. We just need to wrap the wheel in bubble wrap so we don't hurt ourselves.

Let's start building the scaffold of our own CodePen Registry service.

Scaffold a new Service

Let's get down to business and add our new service.

First up, is supported by MDX Embed, so let's import that at the top of our registry file.

packages/sanity-astro-embeds/src/embedRegistry.tsx
import { Spotify, Tweet, Vimeo, YouTube, CodePen } from "mdx-embed";

Scroll down to the bottom of the embedRegistry object and add a new codePen attribute with the following structure:

packages/sanity-astro-embeds/src/embedRegistry.tsx
codePen: {
  title: "CodePen",
  regexp: /^https?:\/\/codepen\.io\/[\w-]+\/(pen|full|details)\/[\w-]+$/,
  getRenderProps: (urlOrId: string) => {
    const renderProps = {
      wrapperClass: "aspect-video",
    } as ComponentPropsWithoutRef<typeof CodePen> & {
      wrapperClass: string;
    };
    // TODO

    return renderProps;
  },
  render: (props: ComponentPropsWithoutRef<typeof CodePen>) =>
    <div>TODO</div>
},

This is the basic starting point for any new embed. We provide a title and a RegExp that matches our "share" URL.

For the RegExp, Chat GPT can help you out with the details. Feel free to test out your RegExp in a tool like RegExr to help make sense of what URLs will work for a given pattern.

For the getRenderProps function, you'll notice this line:

const renderProps = {
  wrapperClass: "aspect-video",
} as ComponentPropsWithoutRef<typeof CodePen> & {
  wrapperClass: string;
};

MDX Embed does not export Types, so we use some React magic to squeeze those props out from the exported CodePen component. We re-use this pattern again with the render function's props:

render: (props: ComponentPropsWithoutRef<typeof CodePen>) =>

This is a bit wonky, but it gives us some guidance on what data each MDX Embed component expects.

The wrapper class will be applied to the div surrounding the client component. Here we use an aspect-video utility class to preserve space in the document for our future iframe.

Let's explore how to build our getRenderProps function by trying to fill in the renderProps object. Try typing out renderProps followed by a period:

VSCode with Intellisense showing required and optional properties for renderProps

Now we can see that codePenId is required and there are several optional parameters. The MDX Embed docs provide a bit more context on what we need from the URL:

https://codepen.io/team/codepen/pen/PNaGbb

Chat GPT can help us out again to extract the ID from the URL. Here's a version of RegExp that should work:

getRenderProps: (urlOrId: string) => {
  const renderProps = {} as ComponentPropsWithoutRef<typeof CodePen>;
  const codePenRegex =
    /^https?:\/\/codepen\.io\/[\w-]+\/(?:pen|full|details)\/([\w-]+)$/;
  const match = urlOrId.match(codePenRegex);

  if (match && match.length > 1) {
    renderProps.codePenId = match[1];
  }
  return renderProps;
},

We won't worry about any of the optional parameters for now. We have the required value and we can render our component.

Go ahead and update the render function to check if codePenId exists in the props and render the CodePen component.

render: (props: ComponentPropsWithoutRef<typeof CodePen>) =>
  props.codePenId ? <CodePen {...props} /> : null,

Our component is functional at this point. 🎉

Head back to Sanity Studio and try to add your CodePen embed. Sanity's validation engine should now recognize the CodePen URL as valid. You can publish the document and inspect your work on your Astro site.

With Great Power...

We'll wrap up the guide here. You have all the information necessary to tweak your embeds to your hearts content.

Please make note of the following:

  1. sanity-astro-embeds will load all embeds using the client:visible directive to try to keep page loads fast. This may result in some scroll performance issues as users scroll down the page.
  2. Each Embed service you add will cause more JavaScript to load for your users. Exercise caution and check that your pages aren't getting bogged down by Embeds.
  3. You are not limited to the components provided by MDX Embed. Dig into how they build their components, and you can build your own.