When working with rich content in Sanity, you may need to embed YouTube videos. Instead of pasting raw HTML embed codes (which mix content with presentation) it’s better to store only the video data and let your frontend handle rendering. This keeps content clean, reusable, and consistently styled.

Add a YouTube Schema Type

A YouTube schema type is a custom field definition in Sanity that tells your studio how to store YouTube video data. Instead of embedding raw HTML, this schema captures just the essentials (like the video URL or ID) so the frontend decides how to display it.

We need this schema type to:

  • Keep video content structured and reusable
  • Ensure consistent rendering across different frontends
  • Separate content from presentation for easier maintenance

Add this schema type to your Sanity schema folder (usually in `schemas/` inside your project). Once defined, it can be reused in blog posts, documentation, or any other content type where videos are needed.

To start, define a custom schema type for YouTube videos in your Sanity Studio:

code
// ./schemaTypes/youTubeType/index.ts
code
import {defineType, defineField} from 'sanity'
code
import {PlayIcon} from '@sanity/icons'
code

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

Explanation:

  • preview.select.title: Maps the video URL to the preview title, so the YouTube URL is passed into the YouTubePreview component.
  • preview.components.preview: Overrides the default Sanity preview with a custom React component (YouTubePreview).
  • <ReactPlayer url={value.url} />: Renders the YouTube video in the frontend using the video URL stored in Sanity.
  • getIdFromUrl: A helper function from vue-youtube that extracts the video ID from a full YouTube URL.
  • components.types.youtube: Defines how to render the youtube block type when using @portabletext/vue in a Vue app.
Article image

Now import and register this type in your main schema file so it’s available across your project:

code
// ./schemaTypes/index.ts
code
import youtube from './youtube'
code

code
export const schemaTypes = [
code
// other schema types
code
youtube
code
]
Article image

Add to Block Content

The YouTube schema type you just created defines how Sanity stores video data. But on its own, it won’t appear inside your rich text fields (like blog posts or documentation pages).

That’s where extending block content comes in. By adding the YouTube schema type to your block content definition, you allow editors to insert YouTube videos directly within rich text, right alongside text, images, and other content.

Here’s the code to enable the YouTube type inside Portable Text:

code
// ./schemaTypes/blockContentType.ts
code

code
import {defineType, defineArrayMember} from 'sanity'
code

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

Explanation:

  • blockContentType: The schema definition for rich text content, allowing both text blocks and YouTube embeds.
  • defineType: A Sanity function used to define a new custom schema type.
  • defineArrayMember: A helper used to declare the types of content allowed within an array field.
  • type: 'youTube': Allows YouTube embeds (defined in a custom schema) to be inserted into the rich text content.

This allows content creators to insert YouTube videos directly from the editor UI and improve usability without touching raw code or embed snippets.

Add a Block Preview Using ReactPlayer

By default, Sanity only shows the URL in the editor, which can be unclear and frustrating. This is especially true when working with multiple videos. To enhance the editorial experience, you can provide a live preview of the embedded video using ReactPlayer.

Example: Install the Required Dependencies

code
npm install react-player @sanity/ui
code

Then, create a preview component that renders the video player using the pasted URL:

code
// ./schemaTypes/youTubeType/YouTubePreview.tsx
code
import type {PreviewProps} from 'sanity'
code
import {Flex, Text} from '@sanity/ui'
code
import YouTubePlayer from 'react-player/youtube'
code

code
export function YouTubePreview({title: url}: PreviewProps) {
code
return (
code
<Flex padding={3} align="center" justify="center">
code
{typeof url === 'string'
code
? <YouTubePlayer url={url} />
code
: <Text>Add a YouTube URL</Text>}
code
</Flex>
code
)
code
}

Explanation:

  • PreviewProps: The type definition for Sanity preview component props, used to ensure correct structure for preview data.
  • YouTubePlayer: A React component from react-player/youtube used to render the YouTube video preview based on the provided URL.
  • typeof url === 'string' ? <YouTubePlayer url={url} />: <Text>Add a YouTube URL</Text>: A conditional rendering logic that shows the video if a valid URL is present, or fallback text if not.

Next, update your schema to use this preview:

code
// ./schemaTypes/youTubeType/index.ts
code
import {defineType, defineField} from 'sanity'
code
import {PlayIcon} from '@sanity/icons'
code
import {YouTubePreview} from './YouTubePreview'
code

code
export const youtube = defineType({
code
name: 'youtube',
code
type: 'object',
code
title: 'YouTube Embed',
code
icon: PlayIcon,
code
fields: [
code
defineField({
code
name: 'url',
code
type: 'url',
code
title: 'YouTube video URL',
code
}),
code
],
code
preview: {
code
select: {title: 'url'},
code
},
code
components: {
code
preview: YouTubePreview,
code
},
code
})

Explanation:

  • fields: [...]: Defines the set of fields included in the YouTube object schema.
  • name: 'url': The internal identifier for the field storing the full YouTube video URL.
  • title: 'YouTube video URL': The label shown to content editors when entering the video URL.
  • preview.select.title: Maps the url field to the title property used in the preview component.
  • components.preview: Overrides the default preview with the custom YouTubePreview component.
Article image

Render the YouTube Embed in a Frontend

Sanity stores YouTube embeds as custom blocks inside the Portable Text array. To display them correctly on your website or app, you’ll need to define a custom serializer that knows how to render the YouTube block.

React Example

If you don’t have it yet, install the Portable Text React package and React Player:

code
npm install @portabletext/react react-player
code

Then, create a reusable component that renders Portable Text with the YouTube serializer:

code
// src/components/Body.tsx
code
import React from 'react'
code
import {PortableText} from '@portabletext/react'
code
import ReactPlayer from 'react-player'
code

code
const serializers = {
code
types: {
code
youtube: ({value}) => <ReactPlayer url={value.url} />
code
}
code
}
code

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

Explanation:

  • serializers.types.youtube: A custom serializer that tells PortableText how to render youtube block types using ReactPlayer.
  • value.url: The YouTube video URL stored in the Sanity document, passed to ReactPlayer for playback.
  • Body({blocks}): A React component that receives blocks (Portable Text content) as a prop and renders it using PortableText.
  • components={serializers}: Passes the custom serializers to PortableText, enabling support for embedded YouTube videos.

Using ReactPlayer here ensures consistent video behavior across devices and browsers, and allows you to maintain a single rendering strategy for all your rich content blocks.

Vue Example

To integrate YouTube embeds in Vue, Install the packages:

code
npm install @portabletext/vue vue-youtube
code

Create a Vue component for the video:

code
<template>
code
<youtube :video-id="videoId"></youtube>
code
</template>
code

code
<script>
code
import { getIdFromUrl } from 'vue-youtube'
code

code
export default {
code
props: {
code
url: String
code
},
code
computed: {
code
videoId() {
code
return getIdFromUrl(this.url)
code
}
code
}
code
}
code
</script>

Explanation:

  • getIdFromUrl: A utility function from vue-youtube that extracts the video ID from a full YouTube URL.
  • videoId(): A computed property that returns the extracted YouTube video ID using getIdFromUrl.
  • <youtube :video-id="videoId"></youtube>: A Vue component that renders the embedded YouTube video using the extracted video ID.

Then, configure Portable Text rendering:

code
<template>
code
<PortableText :value="value" :components="components" />
code
</template>
code

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

code
export default {
code
props: {
code
value: Array
code
},
code
data() {
code
return {
code
components: {
code
types: { youtube: YouTube }
code
}
code
}
code
}
code
}
code
</script>

Explanation:

  • components.types.youtube: A custom block renderer that tells PortableText to use the YouTube component for youtube block types.
  • <PortableText :value="value" :components="components" />: Renders the provided Portable Text content using the custom YouTube renderer.

The same pattern works for other frameworks like Svelte or Astro; just adapt the serializer to match the framework’s conventions.