Custom YouTube preview component not working in Sanity v3

3 replies
Last updated: Mar 30, 2023
Hello everyone! πŸ™‚
Im trying to make a custom preview of a embed youtube video but it doesnt work. What am i doing wrong? Its version 3.


import { defineField, defineType } from "sanity";
import getYouTubeId from "get-youtube-id";

interface Youtube {
  _type: "youtube";
  url: string;
}

interface PreviewProps {
  media: React.ReactNode;
  title: string;
}

const YoutubePreview: React.FC<PreviewProps> = ({ media, title }) => {
  const id = getYouTubeId(media as string);
  const embedUrl = `<https://www.youtube.com/embed/${id}>`;
  return (
    <div>
      <h2>{title}</h2>
      <iframe
        width="560"
        height="315"
        src={embedUrl}
        title="YouTube video player"
        frameBorder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowFullScreen
      ></iframe>
    </div>
  );
};

export default defineType({
  name: "youtube",
  type: "object",
  fields: [
    {
      name: "url",
      title: "URL",
      type: "url",
      validation: (Rule) => Rule.required(),
    },
  ],
  preview: {
    select: {
      url: "url",
    },
    prepare(selection) {
      const { url } = selection;
      if (!url) {
        return {
          title: "Missing YouTube URL",
          media: <div>Missing YouTube URL</div>,
        };
      }
      const id = getYouTubeId(url);
      const media = (
        <img
          src={`<https://i.ytimg.com/vi/${id}/hqdefault.jpg>`}
          alt="Video thumbnail"
        />
      );
      return {
        title: "YouTube Video",
        media,
      };
    },
    component: YoutubePreview,
  },
}) as unknown as Youtube;
AI Update

I can see the issue! In Sanity v3, the way you define custom preview components has changed. You're mixing the old component property with the preview.prepare() method, which doesn't work together properly.

The Problem:

In v3, when you want a custom preview component, you need to use components.preview (not preview.component). Also, when you define a custom preview component, it receives specific props from the Form Components API, not the props you return from prepare().

The Solution:

You have two options depending on what you want to achieve:

Option 1: Simple thumbnail preview (recommended for most cases)

If you just want to show the YouTube thumbnail in lists and references, stick with the standard preview configuration without a custom component:

import { defineType } from "sanity";
import getYouTubeId from "get-youtube-id";

export default defineType({
  name: "youtube",
  type: "object",
  fields: [
    {
      name: "url",
      title: "URL",
      type: "url",
      validation: (Rule) => Rule.required(),
    },
  ],
  preview: {
    select: {
      url: "url",
    },
    prepare(selection) {
      const { url } = selection;
      if (!url) {
        return {
          title: "Missing YouTube URL",
        };
      }
      const id = getYouTubeId(url);
      return {
        title: "YouTube Video",
        subtitle: url,
        media: id ? (
          <img
            src={`https://i.ytimg.com/vi/${id}/hqdefault.jpg`}
            alt="Video thumbnail"
          />
        ) : undefined,
      };
    },
  },
});

Option 2: Full custom preview component with embedded video

If you want the actual embedded video player to show in the preview, use components.preview. Note that the custom component receives different props than what prepare() returns - it gets the entire document value:

import { defineType } from "sanity";
import getYouTubeId from "get-youtube-id";
import { PreviewProps } from "sanity";

const YoutubePreview = (props: PreviewProps) => {
  const { title } = props;
  const url = (props as any).value?.url; // Access the actual field value
  
  if (!url) {
    return <div>Missing YouTube URL</div>;
  }
  
  const id = getYouTubeId(url);
  const embedUrl = `https://www.youtube.com/embed/${id}`;
  
  return (
    <div style={{ padding: '1em' }}>
      <h3>{title || 'YouTube Video'}</h3>
      <iframe
        width="100%"
        height="315"
        src={embedUrl}
        title="YouTube video player"
        frameBorder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowFullScreen
      />
    </div>
  );
};

export default defineType({
  name: "youtube",
  type: "object",
  fields: [
    {
      name: "url",
      title: "URL",
      type: "url",
      validation: (Rule) => Rule.required(),
    },
  ],
  components: {
    preview: YoutubePreview  // Use components.preview, not preview.component
  },
  preview: {
    select: {
      url: "url",
    },
  },
});

Key differences in v3:

  1. Use components.preview instead of preview.component (the old v2 way)
  2. Custom preview components receive PreviewProps which includes the document value, not the result of prepare()
  3. The prepare() function is still useful for standard previews (Option 1), but isn't used when you have a custom component

Option 1 is usually better for performance since it just shows a thumbnail, while Option 2 embeds the full video player which might slow down your Studio if you have many videos in a list.

Hope this helps! πŸŽ₯

Show original thread
3 replies
Ah you dont want to just define the preview.media (but as you can see it does work in general πŸ˜‰ ), but define the preview component for the whole type. At the moment you’re only defining a custom component for media.you need to define this on the type bot in the preview, but I would still select and prepare values as do
πŸ™‚
components:{
  preview: YoutubePreview,
}
HI, is there a full example of this ? All examples i found are on v2 😞
ok got it`Μ€`import {defineType, defineArrayMember} from 'sanity'

/**
* This is the schema definition for the rich text fields used for
* for this blog studio. When you import it in schemas.js it can be
* reused in other parts of the studio with:
* {
* name: 'someName',
* title: 'Some title',
* type: 'blockContent'
* }
*/
export default defineType({
title: 'Block Content',
name: 'blockContent',
type: 'array',
of: [
defineArrayMember({
title: 'Block',
type: 'block',
// Styles let you set what your user can mark up blocks with. These
// correspond with HTML tags, but you can set any title or value
// you want and decide how you want to deal with it where you want to
// use your content.
styles: [
{title: 'Normal', value: 'normal'},
{title: 'H1', value: 'h1'},
{title: 'H2', value: 'h2'},
{title: 'H3', value: 'h3'},
{title: 'H4', value: 'h4'},
{title: 'Quote', value: 'blockquote'},
],
lists: [{title: 'Bullet', value: 'bullet'}],
// Marks let you mark up inline text in the block editor.
marks: {
// Decorators usually describe a single property – e.g. a typographic
// preference or highlighting by editors.
decorators: [
{title: 'Strong', value: 'strong'},
{title: 'Emphasis', value: 'em'},
],

// Annotations can be any object structure – e.g. a link or a footnote.
annotations: [
{
name: 'link',
type: 'object',
title: 'External link',
fields: [
{
name: 'href',
type: 'url',
title: 'URL',
},
{
title: 'Ouvrir dans un nouvel onglet',
name: 'blank',

type: 'boolean',
},
{
title: 'Nofollow',
name: 'nofollow',

type: 'boolean',
},
{
title: 'Sponsored',
name: 'sponsored',

type: 'boolean',
},
{
title: 'User generated content',
name: 'ugc',
type: 'boolean',
},
{
title: 'Cloaked',
name: 'cloaked',
type: 'boolean',
},
],
},
{
name: 'internalLink',
type: 'object',
title: 'Internal link',
fields: [
{
name: 'reference',
type: 'reference',
title: 'Reference',
to: [
{type: 'post'},
// other types you may want to link to
],
},
{
name: 'obfuscation',
title: 'obfuscation',
type: 'boolean',
},
],
},
],
},
}),
// You can add additional types here. Note that you can't use
// primitive types such as 'string' and 'number' in the same array
// as a block type.
defineArrayMember({
type: 'image',
options: {hotspot: true},
}),
defineArrayMember({
type: 'youtube',
}),
],
})

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?