Skip to main content

Adding a YouTube embed component to tinaCMS

00:04:17:59

Before we begin delving into the code, clone and initialise the following project:

https://github.com/mashakos/tina-barebones-starter

To initialise, use yarn:

bash
yarn install

To run the local dev server:

bash
yarn dev

You should see The barebones starter homepage at http://localhost:3000

Click on the Posts menu link, then HelloWorld to reach this page:

Our aim is to set up a tina component, so that we can add youtube embeds from within the tinaCMS admin site.

Updating the schema

First, we need to update the schema for post types so that we have a definition for our youtube component. To do this, go back to the project source code and navigate to /tina/collections/post.jsx

This is our schema currently:

jsx
/**
* @type {import('tinacms').Collection}
*/
export default {
label: "Blog Posts",
name: "post",
path: "content/post",
fields: [
{
type: "string",
label: "Title",
name: "title",
},
{
type: "rich-text",
name: "body",
label: "Body",
isBody: true,
},
],
ui: {
router: ({ document }) => {
return `/posts/${document._sys.filename}`;
},
},
};
SANITY CHECK: We need to make sure that our schema is MDX for any custom components to work:
jsx
label: "Blog Posts",
name: "post",
path: "content/post",
// add this
format: 'mdx',

We have two fields, title and body, with body being a rich-text object type (which just means that you have a multi-line editor box that can accept markdown and html).

Since youtube embeds are a part of the body content, they are not really a separate object. They are more of a "feature" you can insert into your body text. This feature is called a template in the schema. We add our youtube embed template with the following code, within the body object type:

jsx
templates: [
{
name: "YoutubeEmbed",
label: "Embed Youtube",
fields: [
{
name: "url",
label: "Link URL",
type: "string",
},
],
},
],
SANITY CHECK: Make sure that your body object looks like this after adding the youtube embed template:
jsx
{
type: "rich-text",
name: "body",
label: "Body",
isBody: true,
templates: [
{
name: "YoutubeEmbed",
label: "Embed Youtube",
fields: [
{
name: "url",
label: "Link URL",
type: "string",
},
],
},
],
},

In the terminal, run the tina audit command so that our changes are registered:

bash
tinacms audit

Now that our post document type schema is updated, we need to create the component that parses any added youtube embeds in our body content.

To do this, we go back to the project source code and navigate to /pages/posts/[slug].js

SANITY CHECK: Make sure the file extension is mdx in line 63:
jsx
const { data, query, variables } = await client.queries.post({
relativePath: ctx.params.slug + ".mdx",
});

Starting at line 15 you should see the Layout code fragment:

jsx
<Layout>
<div data-tina-field={tinaField(data.post, "body")}>
<TinaMarkdown content={data.post.body} />
</div>
</Layout>

A brief overview of the code above:

  • data-tina-field: a helper attribute that indicates where editable data exists in the page to tinaCMS
  • TinaMarkdown: a helper component that renders the body content of a page or post

We need to pass a component to TinaMarkdown that targets youtube embeds and returns JSX code that renders a youtube block on the page.

I like to initialise a parent object that I can add multiple components to. This is helfpul later when you need to create new components to pass to TinaMarkDown. The structure of this parent object roughly:

txt
// This is pseudo code! DO NOT copy
const tinaComponents = {
// Object1 is a component we are initialising here
Object1: (props) => {
let variable1 = 0;
return (
<>
<p>
{variable1}
</p>
</>
);
},
// Object2 is a component imported from elsewhere with its props expanded
...Object2,
};

In the above pseudo code, multiple components are passed at once to TinaMarkDown. Each component handling a certain object type.


Before we continue with the component implementation, we need to handle a real world issue with youtube urls: that they come in many forms. To tackle this, we must first set up a helper function that brings back only the YouTube video id regardless of the url variant. First, create a js file in components titled ytparser.js, then add the following code:

jsx
export function ytParser(url){
var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
var match = url.match(regExp);
return (match&&match[7].length==11)? match[7] : false;
}

add the import at the top of /pages/posts/[slug].js

jsx
import { ytParser } from '../../components/ytparser.js';

Now that we have a general idea of what we will pass to TinaMarkDown, we can implement our parent object with the youtube embed component within:

jsx
const tinaComponents = {
// The "YoutubeEmbed" component renders YouTube urls.
YoutubeEmbed: (props) => {
// using the ytParser function to retrieve video id
let ytURL = props.url ? `https://www.youtube.com/embed/${ytParser(props.url)}` : "";
return (
<>
<iframe
width="740"
height="416"
src={ytURL}
title="YouTube video player"
allow="accelerometer;
autoplay;
clipboard-write;
encrypted-media;
gyroscope;
picture-in-picture;
web-share" allowFullScreen>
</iframe>
</>
);
},
};

… and with that, we have a YouTube embed object!

if we try to add a youtube video however, we will get this error:

To resolve this, we need to pass our parent object to TinaMarkDown:

jsx
<TinaMarkdown content={data.post.body} components={tinaComponents}/>

and now you should see a working YouTube embed

Save your edit, and enjoy your pages with YouTube content!