Dynamic Video blogs with Gatsby.js and MDX

William Imoh

If a picture is worth a thousand words, how about a video? Mixed content presentations currently have a higher conversion rate on desired outcomes when sharing information. Hence the combined use of video, image, and text content in blogs, tutorials, and even content marketing pages.

This post will explore adding interactive video content in a blog created with Gatsby.js and Markdown. We’ll utilize MDX, which helps us render React.js components in Markdown documents.

Markdown is a widely accepted choice for creating content, mainly blog posts, due to its simplicity in creating rich text and its closeness to writing plain text.

At the end of this tutorial, we’ll be able to display a video player playing a specific video for each of our blog posts in a Gatsby-powered blog, with text content written in Markdown.

Sandbox

We completed this project in CodeSandbox. You can fork it to run the code.

Prerequisites

For this project, you require knowledge of JavaScript and React.js. The comprehension of Gatsby.js and Markdown content creation would be nice to have but isn’t needed.

As this is an enhancement to an existing Gatsby blog, you can utilize your current Gatsby blog or create a new one using the Gatsby blog starter. We bootstrapped this project using a blog starter built with Gatsby and styled with Chakra-ui components. You can find the starter CodeSandbox below to get started quickly.

https://codesandbox.io/s/goofy-lalande-m1sh0?file=/src/pages/index.js

In the above CodeSandbox, we have the following:

  • Installed dependencies and plugins including Chakra-ui and gatsby-transformer-remark
  • Plugin configuration in the gatsby-config.js file
  • Gatsby blog setup with pages automatically created from Markdown documents
  • Site layout and pages styled with Chakra-ui
  • Blog posts fetched and listed on the homepage
  • Single pages rendered and styled for each blog post

We’ll make several modifications to each of the above entities along the way and explain the reason for each change.

The current home page looks like this:

Each blog post page looks like this:

Our goal is to display a video at the top of each blog post content while maintaining the markdown content.

Dependencies installation and plugin configuration

We require the MDX plugin for Gatsby and its dependencies. We’ll install those using yarn with:

1yarn add gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react

Alternatively, you can install them with NPM using:

1npm install --save gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react

Next, we need to configure the plugin in the gatsby-config.js file in the project’s root directory. First, we updated the siteMetadata object to reflect the new site title and description.

1module.exports = {
2 siteMetadata: {
3 title: `Gatsby x MDX Video Blog`,
4 description: `A Gatsby video blog using MDX`,
5 author: `@gatsbyjs`,
6 },
7 plugins: [
8 // plugins go in here
9 ]
10 }

Next, we add the plugin configurations for gatsby-plugin-mdx.

1module.exports = {
2 siteMetadata: {
3 // site metadata goes in here
4 },
5 plugins: [
6 {
7 resolve: `gatsby-source-filesystem`,
8 options: {
9 name: `blogPosts`,
10 path: `${__dirname}/src/blog-posts`
11 }
12 },
13 {
14 resolve: `gatsby-plugin-mdx`,
15 options: {
16 defaultLayouts: {
17 blogPosts: require.resolve("./src/components/layout.js"),
18 default: require.resolve("./src/components/layout.js"),
19 },
20 extensions: [`.mdx`, `.md`],
21 }
22 },
23 // other plugins go in here
24 ]
25 }

In the above snippet, we sourced all blog posts using gatsby-source-filesystem, which creates file nodes for all the documents in the blog-posts directory specified. We then configured gatsby-plugin-mdx and added options for a layout and default layout. This layout specified is a default exported React component applied to all MDX documents (we use the current page layout created in src/components). We added default layouts for files sourced by gatsby-source-filesystem with the name of blogPosts. We set a global default layout as a fallback.

Lastly, we added both .mdx and .md extensions so that the MDX plugin will handle both markdown and MDX files.

With the .md and .mdx extension specified in the plugin configuration, you can discard the gatsby-transformer-remark plugin.

Page creation for MDX documents

With the complete MDX plugin setup in gatsby-config.js, file nodes for the MDX data are created in Gatsby’s data layer and queried using GraphQL. We’ll update the logic that automatically makes pages for markdown files to use MDX files instead.

We update the automatic slug and page creation process in gatsby-node.js. First, in the onCreateNode function, we specify the slug field’s creation only for MDX node types.

1const { createFilePath } = require(`gatsby-source-filesystem`)
2 const path = require("path")
3
4 exports.onCreateNode = ({ node, getNode, actions }) => {
5 const { createNodeField } = actions
6 if (node.internal.type === "Mdx") {
7 const slug = createFilePath({ node, getNode, basePath: `blog` })
8 createNodeField({
9 node,
10 name: `slug`,
11 value: slug
12 })
13 }
14 }

This snippet creates a new slug field for all MDX file nodes using the blog post’s file path. Next, we’ll update the createPages function to create pages using each blog post’s newly generated slug.

You can create blog posts pages based on any existing field in the node. It doesn’t have to be from the slug. It could be a unique ID.

1const path = require("path")
2 exports.createPages = async ({ graphql, actions }) => {
3 const { createPage } = actions
4 const result = await graphql(`
5 query {
6 allMdx {
7 edges {
8 node{
9 fields {
10 slug
11 }
12 frontmatter {
13 title
14 }
15 }
16 }
17 }
18 }
19 `)

In the snippet above, we made a GraphQL query to fetch all MDX documents with their slug and title. With these, we loop through the returned data and create pages using the createPage function. We use the slug as the path, added a template for the page in the component key, and added data sent to the page as context.

1result.data.allMdx.edges.forEach(({node}) => {
2 createPage({
3 path: node.fields.slug,
4 component: path.resolve("./src/templates/blog-template-mdx.js"),
5 context: {
6 slug: node.fields.slug,
7 title: node.frontmatter.title
8 }
9 })
10 })
11 }

We added a template for the MDX blog posts so we need to create this template in src/templates. We make a new file called blog-template-mdx.js, similar to the blog-template.js file, in the same directory.

In blog-template-mdx.js, we import all required dependencies and make a GraphQL query for MDX data using its slug. This slug is already passed as context data on page creation.

1import React from "react"
2 import { graphql } from "gatsby"
3 import Layout from "../components/layout"
4 import SEO from "../components/seo"
5 import { Box, Center, HStack, Text } from "@chakra-ui/react"
6 import { MDXRenderer } from "gatsby-plugin-mdx"
7 import "./blog-template.css"
8
9 export const query = graphql`
10 query($slug: String!) {
11 mdx(fields: {slug: {eq: $slug}}) {
12 frontmatter {
13 publishBy
14 title
15 }
16 timeToRead
17 wordCount {
18 words
19 }
20 body
21 }
22 }
23 `

Here we imported the MDXRenderer component, which we’ll use to render the page’s MDX content. We also imported an existing CSS file to style the blog content. Also, we queried all relevant data required to display the blog content.

Next, we create the blog post component and render all relevant data.

1// Imports go here
2
3 export const query = graphql`
4 // graphql query goes in here
5 `
6 const BlogPostMdx = ({ data }) => {
7 const post = data.mdx
8 return (
9 <Layout>
10 <SEO title="Home" />
11 <Text my={5} fontSize={"3xl"} fontWeight={"bold"}>
12 {post.frontmatter.title}
13 </Text>
14 <Box mb={10}>
15 <Center>
16 <HStack spacing={5} mt={3}>
17 <Text color={"gray.400"} fontSize={"xs"}>
18 {post.timeToRead} {post.timeToRead > 1 ? "mins read" : "min read"}
19 </Text>
20 <Text color={"gray.400"} fontSize={"xs"}>
21 {post.frontmatter.publishBy}
22 </Text>
23 </HStack>
24 </Center>
25 </Box>
26 {/*handle MDX content*/}
27 <Box className={'blog-content'}>
28 <MDXRenderer>{post.body}</MDXRenderer>
29 </Box>
30 </Layout>
31 )
32 }
33 export default BlogPostMdx

We use Chakra-ui components and props to style the page template. Be sure to delete the old blog-template.js file as it will be compiled on app build and throw errors if kept.

At this point, the hot-reloaded app should throw multiple errors as we haven’t created MDX content or rebuilt the app. We update all markdown content in src/blog-posts to MDX files by changing their file extension to .mdx.

Next, we’ll update the home page component in src/pages/index.js to use a new GraphQL query with MDX data.

First, we update the imports and GraphQL query for MDX data to:

1import React from "react"
2 import { graphql, Link } from "gatsby"
3 import Layout from "../components/layout"
4 import SEO from "../components/seo"
5 import { Box, Text, SimpleGrid, HStack } from "@chakra-ui/react"
6
7 export const query = graphql`
8 query {
9 allMdx(sort: { order: DESC, fields: frontmatter___publishBy }) {
10 edges {
11 node {
12 fields {
13 slug
14 }
15 frontmatter {
16 title
17 publishBy
18 }
19 timeToRead
20 }
21 }
22 }
23 }
24 `

Lastly, we update the IndexPage component to render the queried MDX data:

1const IndexPage = ({ data }) => {
2 const blogPosts = data.allMdx.edges
3 return (
4 <Layout>
5 <SEO title="Home" />
6 <Text my={5} fontSize={"3xl"} fontWeight={"bold"}>
7 Blog Posts
8 </Text>
9 <SimpleGrid columns={3} spacing={8}>
10 {blogPosts &&
11 blogPosts.map(({ node }, index) => (
12 <Box as={Link} shadow="md" borderWidth="1px" rounded={"lg"} p={2}
13 key={index}
14 to={node.fields.slug}
15 >
16 <Text fontSize={"sm"} mt={3} fontWeight={"500"}>
17 {node.frontmatter.title}
18 </Text>
19 <HStack spcacing={5} mt={1}>
20 <Text color={"gray.400"} fontSize={"xs"}>
21 {node.timeToRead} {node.timeToRead > 1 ? "mins" : "min"}
22 </Text>
23 <Text color={"gray.400"} fontSize={"xs"}>
24 {node.frontmatter.publishBy}
25 </Text>
26 </HStack>
27 </Box>
28 ))}
29 </SimpleGrid>
30 </Layout>
31 )
32 }
33 export default IndexPage

With these, we’ll restart the development server to see the blog with MDX content.

Video player component creation

We’ll utilize the Cloudinary-React SDK to display a video player with a remote video on Cloudinary. To use media assets on Cloudinary for this project, you need to have a Cloudinary account with videos uploaded to Cloudinary. You create one here.

We proceed to install the cloudinary-react SDK using yarn with

1yarn add cloudinary-react

The cloudinary-react package exports Image, Video, and Transformation React components required to present media assets and optimized transformations from Cloudinary.

While we use Cloudinary here, you can use any video library or even the native. <video> element in HTML to render your choice video component.

We proceed to create a component in src/components called video-player.js.

1// src/components/video-player.js
2 import React from "react"
3 import { Box, Center } from "@chakra-ui/react"
4 import { CloudinaryContext, Video } from "cloudinary-react"
5
6 const VideoPlayer = ({ publicId }) => {
7 return (
8 <Box mb={"4%"}>
9 <Center>
10 <CloudinaryContext cloudName={"chuloo"}>
11 <Video publicId={publicId} autoPlay controls />
12 </CloudinaryContext>
13 </Center>
14 </Box>
15 )
16 }
17
18 export { VideoPlayer }

This functional component receives a prop of publicId of the Cloudinary video. It renders a video player with autoplay content and playback controls.

CloudinaryContext specifies data required and available to all child Cloudinary components, i.e., cloudName. Like all other components of this project, we use Chakra-ui for styling.

We can further enhance this video component to use video transformations and optimizations available on Cloudinary.

We can now import this component into any MDX document and render the video on runtime. To skip this VideoPlayer component’s importation into every MDX document, we will utilize shortcodes to import the component into each MDX document automatically.

We handle the shortcode in the MDX document template. First, we import the VideoPlayer and MDXProvider components.

1// src/components/layout.js
2 import { VideoPlayer } from "./video-player"
3 import { MDXProvider } from "@mdx-js/react"
4
5 const shortcodeComponents = { VideoPlayer }
6 const Layout = ({ children }) => {
7 const data = useStaticQuery(graphql`
8 // GraphQL query goes in here
9 `)
10
11 return (
12 <>
13 <Header siteTitle={data.site.siteMetadata?.title || `Title`} />
14 <Box margin={"0 auto"} maxWidth={960} padding={`0 1.0875rem 1.45rem`}>
15 <Box>
16 {/*Furnish the provider with shortcode */}
17 <MDXProvider components={shortcodeComponents}>{children}</MDXProvider>
18 </Box>
19 </Box>
20 </>
21 )
22 }

Any component specified in the shortcodeComponents object is available to all MDX documents with the above layout.

Finally, we’ll add the VideoPlayer component with a publicId of an existing video on Cloudinary to each MDX document.

We restart the development server to rebuild the site. Here’s an example blog post with a video of chickens clucking away:

Conclusion

In this post, we discussed how to create an interactive blog using MDX and Gatsby.js. We also introduced a video player with which we added remote videos from Cloudinary. You can add other interactive components to the blog using MDX. Also, add video transformations to each video or customize the video player even further to have subtitles and a caption.

You may find the following resources useful:

William Imoh

Creating tech solutions and talking about them

William is a developer, developer advocate, and product manager. When he's not working on technology, he's organizing game nights, making drinks, or playing basketball.