How to add custom YouTube blocks to Portable Text
How to add a YouTube embed in the Studio, and render it on front ends
This developer guide was contributed by Knut Melvær (Head of Developer Community and Education).
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 guide, 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.
Prerequisites
To follow this guide, you need:
- A Sanity Studio v3 or later project.
react-player3.x for the Studio preview and the React example. Version 3 takes the video URL in thesrcprop. Version 2 usedurl, so these examples don't render a player with version 2.- For the front-end examples: a React app, or a Vue 3.3 or later app, that already fetches your Portable Text content.
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 your content creators to find this ID and only put that into Sanity, but YouTube URLs can contain many parameters, so finding the ID by hand is error-prone. Store the full URL and extract the ID wherever you show the video.
You will start by adding a schema type. An object type called youTube, with a field called url. You can extend this later if you want to specify a timestamp, a title, or other things.
import {defineType, defineField} from 'sanity'
import {PlayIcon} from '@sanity/icons'
export const youTubeType = defineType({
name: 'youTube',
type: 'object',
title: 'YouTube Embed',
icon: PlayIcon,
fields: [
defineField({
name: 'url',
type: 'url',
title: 'YouTube video URL',
}),
]
})The next step is to import the file into the schemaTypes/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.
// ...all other imports
import {youTubeType} from './youTubeType'
export const schemaTypes = [
// ...all other schema types
youTubeType
]Now, you can add this field as a type in any array field which also includes a block type to render the Portable Text Editor, like the example below.
import {defineType, defineArrayMember} from 'sanity'
export const blockContentType = defineType({
name: 'blockContent',
type: 'array',
title: 'Body',
of: [
defineArrayMember({
type: 'block'
}),
defineArrayMember({
type: 'youTube'
})
]
})The new YouTube field appears in the Portable Text Editor’s toolbar, where you can paste in a YouTube URL. To see the video you want to embed, add a block preview.
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. You 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 @sanity/iconspnpm add react-player @sanity/ui @sanity/iconsyarn add react-player @sanity/ui @sanity/iconsbun add react-player @sanity/ui @sanity/iconsThen, make a new file called YouTubePreview.tsx:
import type {PreviewProps} from 'sanity'
import {Flex, Text} from '@sanity/ui'
import ReactPlayer from 'react-player'
export function YouTubePreview(props: PreviewProps) {
const {title: url} = props
return (
<Flex padding={3} align="center" justify="center">
{typeof url === 'string'
? <ReactPlayer src={url} />
: <Text>Add a YouTube URL</Text>}
</Flex>
)
}Go back to the youTubeType schema file with your YouTube schema type, and add the preview configuration, as well as the custom component:
import {defineType, defineField} from 'sanity'
import {PlayIcon} from '@sanity/icons'
import {YouTubePreview} from './YouTubePreview'
export const youTubeType = defineType({
name: 'youTube',
type: 'object',
title: 'YouTube Embed',
icon: PlayIcon,
fields: [
defineField({
name: 'url',
type: 'url',
title: 'YouTube video URL',
}),
],
preview: {
select: {title: 'url'},
},
components: {
preview: YouTubePreview,
},
})You should now see a preview embed of the YouTube video blocks when they are previewed in the Portable Text Editor.
This also mirrors what you do in a React front end.
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 guide and follow the code examples if you 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 you used for the preview:
npm install react-playerpnpm add react-playeryarn add react-playerbun add react-playerAnd where you want to be able to output this YouTube embed, do the following:
import {PortableText, type PortableTextBlock, type PortableTextComponents} from '@portabletext/react'
import ReactPlayer from 'react-player'
const components: PortableTextComponents = {
types: {
youTube: ({value}) => {
const {url} = value
return <ReactPlayer src={url} />
}
}
}
export default function Body({blocks}: {blocks: PortableTextBlock[]}) {
return (
<PortableText value={blocks} components={components} />
)
}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. You also need a performant YouTube embed component like vue-lite-youtube-embed:
npm install @portabletext/vue vue-lite-youtube-embedpnpm add @portabletext/vue vue-lite-youtube-embedyarn add @portabletext/vue vue-lite-youtube-embedbun add @portabletext/vue vue-lite-youtube-embedThen create a new YouTube.vue file for the component. This component uses vue-lite-youtube-embed, which provides a fast YouTube embed. It receives the YouTube block as the value prop and extracts the video ID from its url field:
<script setup>
import {computed} from 'vue'
import LiteYouTubeEmbed from 'vue-lite-youtube-embed'
import 'vue-lite-youtube-embed/style.css'
defineOptions({inheritAttrs: false})
const props = defineProps({
value: {
type: Object,
required: true
}
})
const videoId = computed(() => {
try {
const url = new URL(props.value.url)
if (url.hostname === 'youtu.be') return url.pathname.slice(1)
return url.searchParams.get('v') ?? ''
} catch {
return ''
}
})
</script>
<template>
<LiteYouTubeEmbed v-if="videoId" :id="videoId" title="YouTube video" />
</template>In your Portable Text component, you can now import YouTube.vue and add it to the components object, like this:
<script setup>
import {PortableText} from '@portabletext/vue'
import YouTube from './YouTube.vue'
defineProps({
value: {
type: Array,
default: () => []
}
})
const components = {
types: {
youTube: YouTube
}
}
</script>
<template>
<PortableText :value="value" :components="components" />
</template>The custom YouTube block in your Portable Text array is now passed as the value prop to the YouTube.vue component.
Not using React or Vue?
Head over to Portable Text on GitHub for libraries for other popular frameworks, such as Svelte and Astro. They all work on the same principles as above while following the conventions of the framework.
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?