Guide

How to add a custom YouTube block

Knut Melvær

Knut runs developer relations at Sanity.io.

There are times when you want to embed a video in your rich text content. Usually you would paste the HTML embed code into the editor and move along. With Portable Text, however, you want to make sure that your content is structured, and you don't embed too many assumptions about presentation in it. Maybe you want to use this video in a native app, or rather use a specialized component for it in your frontend(s).

In this tutorial we will cover how to add a custom YouTube block into your schema and the editor, how to add a custom preview component for it, and and how to serialize it in the frontend. We will also look at how you can add a custom paste handler, so that your editors can copy-paste a YouTube link right into the editor, and have it stored as the custom block.

The schema

The main thing we are interested in when doing a YouTube embed, is not the embed code in itself, but a way to identify what video it is, that is, it's ID. The ID is included in the video URL, so if we for example have this URL, https://www.youtube.com/watch?v=asENbq3s1m8&feature=youtu.be, the ID is asENbq3s1m8.

Now, we could have asked our editors to find this ID and only put that into Sanity, but that seems a bit too much to ask, and isn't always easy since YouTube URLs can contain a lot of parameters. We will assume that we can work with an URL and find the ID programatically whereever we want to show the video.

We'll make a very simple schema to begin with. An object we call youtube, with a field called url. You can extend this later if you want to for example specify a time stamp, or other things.

// youtube.js
export default {
  name: 'youtube',
  type: 'object',
  title: 'YouTube Embed',
  fields: [
    {
      name: 'url',
      type: 'url',
      title: 'YouTube video URL'
    }
  ]
}

The next step is to import the file into the schema.js to make it available as a type: 'youtube':

import createSchema from 'part:@sanity/base/schema-creator'
import schemaTypes from 'all:part:@sanity/base/schema-type'

import bodyPortableText from './objects/bodyPortableText'
import post from './documents/post'
import youtube from './youtube'
export default createSchema({ name: 'default', types: schemaTypes.concat([ post, bodyPortableText,
youtube
]) })

Now we can include this field in bodyPortableText:

// bodyPortableText.js
export default {
  name: 'bodyPortableText',
  type: 'array',
  title: 'Body',
  of: [
    {
      type: 'block'
    },
    {
      type: 'youtube'
    }
  ]
}


Now you can access the new YouTube field in the editor’s toolbar where you can paste in a YouTube URL. However, it would be nice to get an actual preview of the video that you want to embed. So let’s add that.

You’re maybe already familiar with how to configure previews with Sanity. First you select the fields you want to get content from. You can assign them to variables like title, subtitle, and media and let the Studio figure out the rest. Or you can return a prepare function if you want to more control over what goes in to the slots.

However, you can also pass a React component and gain full control over what’s rendered. It still gets the variables that you define in select as props. For the YouTube preview, we’re going to use a pre-built component called react-youtube, as well as a package called get-youtube-id to first get the ID from the URL, and return and render the embed in the editor.

We start by installing the dependencies we need by running npm install react-youtube get-youtube-id in the root folder. Then we open the youtube.js file, and add the following to it:

// youtube.js
import React from 'react'
import getYouTubeId from 'get-youtube-id'
import YouTube from 'react-youtube'
const Preview = ({value}) => {
const { url } = value
const id = getYouTubeId(url)
return (<YouTube videoId={id} />)
}
export default { name: 'youtube', type: 'object', title: 'YouTube Embed', fields: [ { name: 'url', type: 'url', title: 'YouTube video URL' } ],
preview: {
select: {
url: 'url'
},
component: Preview
}
}

This pretty much mirrors what you would do in a React frontend as well. And speaking of frontends, let's take a closer look at how this should be implemented.

Render the YouTube embed in a frontend

When you insert a YouTube embed in the rich text editor, it will be added as a custom block in the Portable Text array. If you are not familiar with how Portable Text works, you can learn more in this introduction. You can also tag along with this tutorial and follow the code examples if you just want to get a feel for it.

React

To render custom blocks, as the YouTube embed is, you have to add a serializer to the Portable Text package for React (currently called @sanity/block-content-to-react). If you don't have this in your project from before, you'll have to install it by running npm install @sanity/block-content-to-react in your command line.

Next up is installing a component to show the YouTube video, and a package to suss out the id for the YouTube video from its URL. In this case we’re going to install the same dependencies, and add pretty much the same code, as we did in the studio. So first, run npm install react-youtube get-youtube-id to add the dependencies in your frontend project.

And where you want to be able to output this YouTube embed, do the following:

import React from 'react'
import getYouTubeId from 'get-youtube-id'
import YouTube from 'react-youtube'
import PortableText from '@sanity/block-content-to-react'

const serializers = {
    types: {
      youtube: ({node}) => {
        const { url } = node
        const id = getYouTubeId(url)
        return (<YouTube videoId={id} />)
      }
    }
}

export default function Body ({blocks}) {
  return (
    <PortableText blocks={blocks} serializers={serializers} />
  )
}

Vue

To render custom blocks, as the YouTube embed is, you have to add a serializer to the Portable Text package for Vue (currently called sanity-block-vue-component). If you don't have this in your project from before, you'll have to install it by running npm installl sanity-block-vue-component in your command line. We need something for YouTube as well, so let's run npm install vue-youtube and add it to where we mount our Vue app:

import Vue from "vue";
import App from "./App.vue";

import VueYoutube from "vue-youtube";
Vue.use(VueYoutube);

new Vue({
  render: h => h(App)
}).$mount("#app");

Then we'll create a new YouTube.vue file for our component with the code we need to render the video. We'll add the same package as we used in the studio to get the ID from the URL (npm install get-youtube-id):

<template>
  <youtube :video-id="videoId"></youtube>
</template>
<script>
import getId from "get-youtube-id";

export default {
  data() {
    return {
      videoId: getId(this.url)
    };
  },
  props: {
    url: {
      type: String,
      default: () => ""
    }
  }
};
</script>

In our component for Portable Text we can now import YouTube.vue and add it to our serializers, like this:

<template>
  <PortableText :blocks="blocks" :serializers="serializers" />
</template>

<script>
import PortableText from "sanity-blocks-vue-component";
import Youtube from './YouTube'

export default {
  components: {
    PortableText
  },
  props: {
    blocks: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      serializers: {
        types: {
          youtube: YouTube
        }
      }
    };
  }
};
</script>

That's it! Now the data under node in the custom YouTube block in our Portable Text array will be passed on as props to the YouTube.vue component.