Join live – Get insights, tips, + Q&A from Sanity developers on our latest releases
June 06, 2019 (Updated September 25, 2023)

How to add custom YouTube blocks to Portable Text

Official(made by Sanity team)

By Knut Melvær

This guide will take you through adding a custom YouTube block to the Portable Text Editor and show you how to render it in some common front end frameworks like React and Vue.

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

In this tutorial, you will learn how to add a custom YouTube block into your schema and the Portable Text Editor, add a custom preview component, and serialize it in the front end.

Add a YouTube schema type

The main thing you are interested in for a YouTube embed is its ID. The ID is included in the video URL, so if you have this URL: https://www.youtube.com/watch?v=asENbq3s1m8&feature=youtu.be, the ID is asENbq3s1m8.

Now, you could ask our content creators to find this ID and only put that into Sanity, but that seems a bit too much to ask since it isn't always easy since YouTube URLs can contain a lot of parameters. You can assume you can work with a URL and find the ID programmatically wherever you want to show the video.

You will start by adding a simple schema type. An object type called youtube, with a field called url. You can extend this later if you want to specify a time stamp, a title, or other things.

// schema/youtube.ts
import {defineType, defineField} from 'sanity'

export const youtube = defineType({
  name: 'youtube',
  type: 'object',
  title: 'YouTube Embed',
  fields: [
    defineField({
      name: 'url',
      type: 'url',
      title: 'YouTube video URL'
    })
  ]
})

The next step is to import the file into the schema/index.ts to make it available as a type: 'youtube'. Your setup might be slightly different, so the point here is that it should end up in the array of schema types that you specify in sanity.config.ts.

// schema/index.ts
import {blockContent} from './blockContent'
import {category} from './category'
import {post} from './post'
import {author} from './author'
import {youtube} from './youtube'
export const schemaTypes = [post, author, category, blockContent, youtube]

Now, you can add this field as a type in blockContent:

// schema/blockContent.ts
import {defineType, defineArrayMember} from 'sanity'

export const blockContent = defineType({
  name: 'blockContent',
  type: 'array',
  title: 'Body',
  of: [
    defineArrayMember({
      type: 'block'
    }),
    defineArrayMember({
      type: 'youtube'
    })
  ]
})

The new YouTube field should appear in the Portable Text Editor’s toolbar, where you can paste in a YouTube URL. However, getting an actual preview of the video you want to embed would be nice. So, let’s add that.

Add a block preview

You may be 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 more control over what goes into 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 the select object as props. You will use a pre-built component called React Player for the YouTube preview. It can take a YouTube URL (and other video streaming services) and automatically render the embedded video player. We will also install Sanity UI and use its components to make sure the UI is nice.

Start by installing the dependencies in your Sanity project folder:

npm install react-player @sanity/ui

Then, make a new file called YouTubePreview.tsx:

import type {PreviewProps} from 'sanity'
import {Flex, Text} from '@sanity/ui'
import YouTubePlayer from 'react-player/youtube'

interface PreviewYouTubeProps extends PreviewProps {
  selection?: {
    url: string
  }
}

export function YouTubePreview(props: PreviewYouTubeProps) {
  const {selection} = props
  const url = selection?.url
  return (
    <Flex padding={4} justify={'center'}>
      {url 
        ? <YouTubePlayer url={url} /> 
        : <Text>Add a YouTube URL</Text>
      }
    </Flex>
  )
}

Go back to the youtube.ts file with your YouTube schema type, and add the preview configuration, as well as the custom component:

import {defineField, defineType} from 'sanity'
import {YouTubePreview} from '../components/YouTubePreview'

export const youtube = defineType({
  name: 'youtube',
  title: 'Youtube',
  type: 'object',
  fields: [
    defineField({
      name: 'url',
      title: 'URL',
      type: 'url',
    }),
  ],
  preview: {
    select: {
      url: 'url',
    },
  },
  components: {
    preview: YouTubePreview,
  },
})

This also mirrors what you would do in a React front end. And speaking of front ends, let's take a closer look at how this data should be implemented.

Render the YouTube embed in a front end

When you insert a YouTube embed in the Portable Text Editor, it will be added as a custom block in the Portable Text array. If you are unfamiliar with Portable Text, 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 @portabletext/react). If you don't have this in your project from before, you'll have to install it by running npm install @portabletext/react in your command line.

Next up is installing a component to show the YouTube video. In this case, you can install the same React Player dependency we used for the preview:

npm install react-player

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

// src/components/Body.tsx

import React from 'react'
import {PortableText} from '@portabletext/react'

const serializers = {
    types: {
      youtube: ({node}) => {
        const { url } = node
        return (<ReactPlayer url={url} />)
      }
    }
}

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

This is a minimal example, so you might need to make some adjustments to make it render nicely within your design system. You should probably also extract it into its own component instead of having it inline as here.

Vue

To render custom blocks, as the YouTube embed is, you have to add a serializer to the Portable Text package for Vue. If you don't have this in your project from before, you'll have to install it first. We need something for YouTube as well, like vue-youtube:

npm install @portabletext/vue vue-youtube

Follow the installation instructions in vue-youtube's README. Then create a new YouTube.vue file for our component with the code we need to render the video. We also need to add some code to extract the video ID that the component takes:

<template>
  <youtube :video-id="videoId"></youtube>
</template>
<script>
import { getIdFromUrl } from 'vue-youtube'

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

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

<!-- YouTube.vue -->
<template>
  <PortableText :value="value" :components="components" />
</template>

<script>
import {PortableText} from "@portabletext/vue";
import YouTube from './YouTube'

export default {
  components: {
    PortableText
  },
  props: {
    value: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      components: {
        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.

Not using React or Vue? No problem!

Head over to Portable Text on GitHub for libraries for other popular frameworks like Svelte, Astro, and so on. They all work on the same principles as above while following the conventions of the framework.

Sanity – build remarkable experiences at scale

Sanity Composable Content Cloud is the headless CMS that gives you (and your team) a content backend to drive websites and applications with modern tooling. It offers a real-time editing environment for content creators that’s easy to configure but designed to be customized with JavaScript and React when needed. With the hosted document store, you query content freely and easily integrate with any framework or data source to distribute and enrich content.

Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.

Other guides by author