Using Remote Image specified in a Blog Post’s Markdown Frontmatter

Banner for a MediaJam post

William Imoh

Markdown is widely preferred as a means to create content. It provides a flexible system to make rich content while maintaining the simplicity of entering plain text.

An additional benefit of writing in Markdown is the ability to specify Markdown document properties, and data, in the Markdown frontmatter.

Frontmatter is a set of key-value pairs in a block, stated at the top of a Markdown document to contain the document’s metadata.

Blog posts often contain a banner or cover image, specified in some way in the blog content. In Gatsby.js powered blogs, this image can be queried with Graphql from any image source.

Having this image specified in the Markdown frontmatter would provide a better experience. A better solution, however, is to have this image optimized and transformed to a gatsby-image compatible image object with fluid and fixed types.

In this article, we will discuss how to utilize the gatsby-transformer-cloudinary plugin to source images on Cloudinary with an ID specified in the Markdown frontmatter of a Gatsby.js blog post. This image will be transformed into a gatsby-image compatible image object with fluid and fixed properties, right in the Markdown frontmatter.


We completed this project in a Codesandbox. You can fork it to see the project live. Once you fork it, you are required to specify your Cloudinary account credentials. These include your Cloud name, API key, and API secret as environment variables in a .env file (which you’ll create) in the root directory of the project. See .env.example in the root directory of the project for the required environment variables.

You can create an account on Cloudinary to obtain the credentials.

Cloudinary account information is required to upload local images on Cloudinary. But, if you don’t need to upload images to Cloudinary, you don’t need to specify the Cloudinary credentials nor specify an upload folder in the plugin configuration.

Prerequisites and Setup

For this jam, we are implementing a feature in an existing Gatsby.js blog. You can use your blog or create a new blog using the Gatsby blog starter. We created a bare-bones Gatsby.js blog in a Codesandbox. You can fork it to get started. The starter contains:

  • Installed dependencies which include Chakra-ui for styling, and gatsby-transformer-remark to create file nodes for Markdown files.
  • Gatsby.js plugins configuration in gatsby-config.js .
  • Gatsby-node code to automatically generate pages for Markdown files.
  • Minimalistic layout setup in src/components/layout.js.
  • Templates for blog post pages in src/templates/blog-template.js.
  • Example blog posts in Markdown located in src/blog-posts.

Here’s the starter Codesandbox below

You need to specify your Cloudinary account information as environment variables in a .env file to use this blog starter. See .env.example for a sample setup.

The current bare blog starter looks like this:

Installing Gatsby-transformer-cloudinary

Gatsby-transformer cloudinary is a transformer plugin for Gatsby.js that creates Cloudinary asset nodes for images in a Gatsby project. Local image files are uploaded to Cloudinary, and an optimized image data, and an image object fragment compatible with gatsby-image, is created by the plugin, using the returned image URLs. This way, remote images delivered via a content delivery network are used in the Gatsby.js website. Cloudinary transformations can also be applied directly in the GraphQL query for the image data.

Another feature of the plugin is to create asset nodes for images already on Cloudinary. The plugin converts an object or data node with a key of CloudinaryAssetData set to true, (and other required keys), into a CloudinaryAsset node in GraphQL. This is true for both JSON data structures, and frontmatter in Markdown.

We’ll use remote images on Cloudinary, specified in each blog post’s frontmatter. The frontmatter data must contain:

  • cloudinaryAssetData: We set to true.
  • originalWidth: This is the original width of the remote image.
  • originalHeight: This is the original height of the remote image.
  • publicId: This is the public ID of the image on Cloudinary.
  • cloudName: This is the cloud name of the Cloudinary account the image resides.

The originalWidth and originalHeight values are required to compute the aspect ratio of the image. This is necessary to display the image in the right dimensions.

We’ll install the plugin using yarn with

1yarn add gatsby-transformer-cloudinary

Once its installation is complete, we add the plugin configuration to gatsby-config.js.

3 module.exports = {
4 siteMetadata: {},
5 plugins: [
6 {"other plugins go in here"},
7 {
8 resolve: `gatsby-transformer-cloudinary`,
9 options: {
10 cloudName: process.env.CLOUDINARY_CLOUD_NAME,
11 apiKey: process.env.CLOUDINARY_API_KEY,
12 apiSecret: process.env.CLOUDINARY_API_SECRET,
13 useCloudinaryBreakpoints: false
14 }
15 }
16 ]
17 }

We’ll restart the development server to load the plugins, source, and transform file nodes.

Updating the frontmatter of blog posts

Our goal is to specify a blog cover image in the frontmatter of a blog post, and we query an image object fragment which we, in turn, use in gatsby-image.

We update each blog post’s frontmatter to include a coverImage object key with key-value pairs, describing the required image. The frontmatter of a blog post looks like this.

2 ---
3 title: "First blog post using Gatsby x Cloudinary"
4 path: "/first-post-2302202"
5 publishBy: "2021-02-21"
6 coverImage:
7 cloudinaryAssetData: true
8 originalWidth: 1000
9 originalHeight: 800
10 publicId: "remote-images/7"
11 cloudName: "chuloo"
12 ---
13 # Introduction
14 Lorem ipsum dolor sit amet, epicuri \[sensibus\]( oportere est ut. No ubique oporteat est, laudem quaerendum quo ut. Feugiat adversarium est ne, ei pri illud definitiones. Mea ea apeirian sensibus signiferumque? Mei an graeci civibus, usu ad eirmod labitur.

We specified an image on Cloudinary with a publicID of remote-images/7. We replicate the same update for every blog post, each with its image.

We restart the development server to rebuild the GraphQL data nodes. Looking in the Graphiql interface, we can see that the frontmatter data for allMarkdownRemark node in the explorer is updated. The data initially in coverImage is replaced with a cloudinaryAsset node having fluid and fixed properties.

With this setup, we can query data in coverImage along with data for the blog post content in each blog post.

Update pages

Home page We proceed to update the home page’s query and UI in src/pages/index.js to include the blog image.

We update the GraphQL query 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 Image from "gatsby-image"
6 import { Box, Text, SimpleGrid, HStack } from "@chakra-ui/react"
8 export const query = graphql`
9 query {
10 allMarkdownRemark(sort: { order: DESC, fields: frontmatter___publishBy }) {
11 edges {
12 node {
13 fields {
14 slug
15 }
16 frontmatter {
17 title
18 publishBy
19 coverImage {
20 fluid(transformations: "f_auto,q_auto") {
21 ...CloudinaryAssetFluid
22 }
23 }
24 }
25 timeToRead
26 }
27 }
28 }
29 }
30 `

This query includes transformations for quality and format optimizations, and you can find other transformations on Cloudinary. The …CloudinaryAssetFluid fragment returns the fluid image required by gatsby-image.

Next, we update the page component to

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

The updated page looks like this:

Blog post page We’ll update the blog post template in src/templates/blog-template.js. First, we update the GraphQL query to include the query for the cover image.

1import React from "react"
2 import { graphql } from "gatsby"
3 import Image from "gatsby-image"
4 import Layout from "../components/layout"
5 import SEO from "../components/seo"
6 import { Box, Center, HStack, Text } from "@chakra-ui/react"
7 import "./blog-template.css"
8 export const query = graphql`
9 query($slug: String!) {
10 markdownRemark(fields: { slug: { eq: $slug } }) {
11 html
12 frontmatter {
13 publishBy
14 title
15 coverImage {
16 fluid(transformations: "f_auto,q_auto") {
17 ...CloudinaryAssetFluid
18 }
19 }
20 }
21 timeToRead
22 wordCount {
23 words
24 }
25 }
26 }
27 `

The query is similar to that made on the home page. Next, we update the blog post component with

1const BlogPost = ({ data }) => {
2 const post = data.markdownRemark
3 return (
4 <Layout>
5 <SEO title="Home" />
6 <Text my={5} fontSize={"3xl"} fontWeight={"bold"}>
7 {post.frontmatter.title}
8 </Text>
9 <Box mb={10}>
10 <Box mx={"auto"} shadow="md">
11 <Image fluid={post.frontmatter.coverImage.fluid} />
12 </Box>
13 <Center>
14 <HStack spacing={5} mt={3}>
15 <Text color={"gray.400"} fontSize={"xs"}>
16 {post.timeToRead} {post.timeToRead > 1 ? "mins read" : "min read"}
17 </Text>
18 <Text color={"gray.400"} fontSize={"xs"}>
19 {post.frontmatter.publishBy}
20 </Text>
21 </HStack>
22 </Center>
23 </Box>
24 <Box
25 dangerouslySetInnerHTML={{ __html: post.html }}
26 className={"blog-content"}
27 />
28 </Layout>
29 )
30 }
31 export default BlogPost

The component renders a fluid image with Chakra-ui components. The fluid image takes the Box component’s full width (a Chakra-ui extension of the HTML div).

The HTML content is also rendered using the dangerouslySetInnerHTML prop.


In this jam, we discussed using the gatsby-transformer-cloudinary plugin to create image nodes specified in a blog post frontmatter from existing images on Cloudinary. This image node, queried using GraphQL, is then utilized in a blog post page. Take this for a spin, add multiple transformations to the image, and try the fixed image property of gatsby-image.

You may find the links below 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.