Man holding smartphone with YouTube logo

Adding embedded video to my Gatsby blog

Let's add some video

As part of #100DaysOfGatsby I'm adding capabilities to my new Gatsby/Sanity blog. Today I'm going to add the ability to add embedded YouTube video to a Portable Text block. It would be easy enough to create a video field that could hold the YouTube ID and parameters and then use that on the page, but this would limit flexability as it would need to be put on the page at the same place or need complex formatting rules. However, if we can add a video embed as part of our main Portable Text block, then we can put a video wherever we like in the body of the blog post.

I'd like to give a big shout out to Knut @kmelve who's writings and videos seem to be my main source of learning and inspirtation. For this feature, I leaned hevily on this guide https://www.sanity.io/guides/portable-text-how-to-add-a-custom-youtube-embed-block

Step 1 - Extend our Sanity Studio code to have a youtube embed

We will define a new object by creating a new schema file called /studio/schemas/objects/youtube.js

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

Next we will need to add a reference to this new object in our main schema object /studio/schemas/schema.js so that we can reference this type elsewere.

import createSchema from 'part:@sanity/base/schema-creator'
import schemaTypes from 'all:part:@sanity/base/schema-type'
import author from './documents/author'
import category from './documents/category'
import post from './documents/post'
import siteSettings from './documents/siteSettings'
import bodyPortableText from './objects/bodyPortableText'
import bioPortableText from './objects/bioPortableText'
import excerptPortableText from './objects/excerptPortableText'
import mainImage from './objects/mainImage'
import authorReference from './objects/authorReference'
import youtube from './objects/youtube'

export default createSchema({
  name: 'blog',
  types: schemaTypes.concat([
    siteSettings,
    post,
    category,
    author,
    mainImage,
    authorReference,
    bodyPortableText,
    bioPortableText,
    excerptPortableText,
    youtube
  ])
})

Now that we have defined the type in the schemas we can reference it in other schemas. Let's add this to our the main body portable text object which is at /studio/schemas/objects/bodyPortableText.js

export default {
  name: 'bodyPortableText',
  type: 'array',
  title: 'Post body',
  of: [
    {
      type: 'block',
      title: 'Block',
      // Styles let you set what your user can mark up blocks with. These
      // corrensponds 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' },
        { title: 'Number', value: 'number' }
      ],
      // 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' },
          { title: 'Underline', value: 'underline' },
          { title: 'Strike', value: 'strike-through' },
          { title: 'Code', value: 'code' }
        ],
        // Annotations can be any object structure – e.g. a link or a footnote.
        annotations: [
          {
            name: 'link',
            type: 'object',
            title: 'URL',
            fields: [
              {
                title: 'URL',
                name: 'href',
                type: 'url'
              }
            ]
          }
        ]
      },
      of: [{ type: 'authorReference' }]
    },
    // 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.
    {
      type: 'mainImage',
      options: { hotspot: true }
    },
    {
      type: 'code',
      title: 'Code block',
      options: { theme: 'github' }
    },
    {
      type: 'youtube'
    }
  ]
}

Now we have the ability to embed a video. But it's not very friendly to the content editor as they don't see a preview of the video in the Studio. There's also some nice things we can do to extract the YouTube ID from the URL to make things a little cleaner. Lastly - we should add an ALT descriptor for accessibility.

This is where we can see the true power behind having the Studio as a react app - we can add react components to extend it to our needs. In this case we are going to install react-youtube and get-youtube-id. The react-youtube componet will display the component in the Studio for us using Sanity's preview method. (I won't get into the details of preview as that's more than I want to take on right now, but know that it can be quite powerful)

Change to the /studio folder then install these components

npm install react-youtube get-youtube-id

Now let's update our 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'
    },
    {
      name: 'alt',
      type: 'string',
      title: 'Alternative text',
      description: 'Important for SEO and accessiblity.',
      validation: Rule => Rule.error('You have to fill out the alternative text.').required()
    }
  ],
  preview: {
  	select: {
      url: 'url'
  	},
  	component: Preview
  }
}

Now we have the ability to embed a youtube video into the body block, and see the video previewed there. Here's a screenshot to show you the Block Editor in the Studio:

Step 2 - Update our Gatsby front end code to be able to use the youtube embed

To render the custom blocks in our portable text, we need to have Sanity's Portable Text Serializer package called @sanity/block-content-to-react installed. Since this part of the blog starter we started with, we already have this, so we are good to go. (I mention this in case someone is following along and is having trouble).

We also will need the same react components we used in the studio. Change to the /web directory and install these

npm install react-youtube get-youtube-id

Now we will need to update the gatsby portable text component to add the serializers for this new type. In our case, that is the file /web/src/components/serializers.js. We will add imports for the two youtube helper components, then define how the youtube type should be handled by the serializer.

import React from 'react'
import Figure from './Figure'
import Code from './Code'
import getYouTubeId from 'get-youtube-id'
import YouTube from 'react-youtube'

const serializers = {
  types: {
    authorReference: ({node}) => <span>{node.author.name}</span>,
    mainImage: Figure,
    code: Code,
    youtube: ({node}) => {
      const { url } = node
      const id = getYouTubeId(url)
      return (<YouTube videoId={id} />)
    }
  }
}

export default serializers

Now our gatsby code knows how to handle the youtube type and we will see an embedded video.

Here's an embedded video to show it in action!

That's all for today. See you next time