Featured Image with MDX and Gatsby

Matías Hernández

Banner Image for Jam

MDX, the powerful authoring format that lets you write JSX content inside of your markdown content, is an amazing tool to deliver interactive experiences to your readers.

What is MDX? You can learn more by checking the official site and/or this youtube video.

MDX is compatible with different tools, and Gatsby is one of those.

When writing in Markdown (and for extension, with MDX), you use the frontmatter section to add metadata for your content. Usually, one of those attributes is the featured image URL (or cover image, banner, etc).

There are a few problems that you can encounter when using Gatsby and MDX, however. For example, if you want to use Gatsby image preprocessing capabilities with a featured image from your frontmatter, you need to tap into the Gatsby build system through the gatsby-node.js file to retrieve the image and create the corresponding Node. But what if you want to use the filesystem routing API from Gatsby instead?

In this article we will learn:

  • How to use the Gatsby filesystem route API to automatically create pages for your MDX content file.
  • How to create a plugin by using the createResolvers API from Gatsby to create an image node from the featured image defined in the frontmatter section of the MDX file.
  • How to use the new gatsby-image-plugin to use the new image node created by the plugin in your site optimally.

What is the Gatsby filesystem route API?

A benefit of using Gatsby is the ability to create static pages based on the data that you can source. One way to accomplish this is by using the Gatsby API through the gatsby-node by querying the data programmatically and somehow imperatively creating the pages. But Gatsby offers what can be considered a simple, more declarative way to accomplish the same thing: the filesystem route API.

This is a way to create pages from your GraphQL data by using certain file naming notations that will allow you to control the page path and the queried data without touching the gatsby-node file. Since the publication of this API, it has become the recommended process for creating pages. If for some reason, your use case is not covered by this API, you can always jump into gatsby-node and use the createPages API.

How can I create pages?

Let's assume that you have a simple Gatsby site that is sourcing data from the filesystem, like a collection of MDX files. Since MDX is an extension for Markdown, you can use frontmatter to define some attributes for your files. In this case, you have the following in the header of every MDX file.

1---
2title: Some Title
3cover: some url to an image
4date: 04/30/2021
5description: Some description
6---

These files are stored inside src/content/posts.

Assume that you are already able to source the MDX files into your Gatsby schema. If that is not is the case, you need to set up the gatsby-source-filesystem plugin in your gatsby-config.js file like this or you can use this starter to set things up.

Now, we want to create pages for each of these posts. By default, Gatsby assumes that the content of the src/pages folder will become a statically generated page. We will create a file under that folder and use some naming conventions to pull the data from the GraphQL layer.

Let's create the file:

1$ touch src/pages/{mdx.slug}.js

Here, we used the curly braces {} convention in the name. This tells Gatsby that this is a dynamic URL, generated by querying the GraphQL schema, and retrieves the mdx nodes from both inside the node reading the slug attribute of the node.

This is Similar to a GraphQL query that looks like this:

1query MyQuery {
2 mdx {
3 slug
4 }
5}

For a file named 'Some Title', the corresponding route's output will be '/some-title'.

The notation means:

  • Using '.' (period), you signify that you want to access a field on a node of a type.
  • Using '__' (double underscore), you signify that you want to access a nested field on a node.

Page implementation

Now that the file we just created will become the template for the content we want to render, go ahead and open the file, and let's add a basic definition for it.

1import React from "react"
2import { graphql } from "gatsby" // We need to explicitly import GraphQL here.
3import { MDXRenderer } from "gatsby-plugin-mdx"; //Since we want to render MDX, we need this plugin.
4
5export default function PostPage(props) {
6 return (
7 <div>
8 <h1>{props.data.frontmatter.title}</h1>
9 <img src={props.data.cover} alt={props.data.frontmatter.title} />
10 <span>{props.data.frontmatter.date}</span>
11 <MDXRenderer>{props.data.body</MDXRenderer>
12 </div>
13 )
14}
15
16export const query = graphql`
17 query($id: String) {
18 mdx(id: { eq: $id }) {
19 frontmatter {
20 cover
21 date
22 description
23 }
24 body
25 }
26 }
27`

This defines the component that will be rendered, and also the data that will be used. The filesystem route API naming will automatically create a route for your page for each node, and will automatically pass the queried field slug via props.params to your component. Gatsby will also automatically give you the id for the data you are querying.

So, you are able to access the queried data through the data prop. This data is the result of the graphql query that filter the data by id. In this example, we queried the whole data of the post and rendered that in the component.

For more information about how the filesystem route API is designed and works, please check the Gatsby documentation site.

How to manage the image of the post

One important thing to notice in the previous example is that we are querying the cover attribute from the frontmatter as a string attribute and rendering that as a simple img tag. Additionally, if you are using a media service, such as Cloudinary, your image can be optimized by the service and even cached, but What if you want more control over the image?

Gatsby allows powerful image processing features using the Sharp library to automatically process images to be performant, using features like lazy-loading. That said, this only works if the image is a File node in the GraphQL layer.

Gatsby v3.0 was released along with a new amazing plugin to handle and optimize images. gatsby-plugin-image is now the default way to manage your media files.

This plugin handles the hard parts of producing images in multiple sizes to accomplish your responsiveness needs while maintaining high-performance scores without any hassle.

To use it, we just need to install it, and add it to the gatsby-config file:

1npm install gatsby-plugin-image gatsby-plugin-sharp gatsby-source-filesystem gatsby-transformer-sharp

Then update your config file:

1module.exports = {
2 plugins: [
3 `gatsby-plugin-image`,
4 `gatsby-plugin-sharp`,
5 `gatsby-transformer-sharp`,
6 ],
7}

Unfortunately, our query now just retrieves the cover image as a string. The mdx node is not actually doing anything special with the image data, so we can't use the power of this plugin. What can we do?

Use createResolvers to create an image node

We will need to tap into the Gatsby build system in the gatsby-node API to add a simple function that will read the image attribute that we have and to create the required node to allow us to use the new image plugin.

This is already available as a plugin. You can install it directly from npm.

The createResolvers function is part of the gatsby-node API. It allows us to add additional customizations on top of a pre-made data schema. As mentioned by the Gatsby documentation:

This is an “escape hatch” API, as it allows us to modify any fields or types in the Schema, including Query type.

You can learn more by reading the Gatsby documentation about createResolvers here.

We will use the API to read the mdx node that we are using, retrieve the cover attribute from the frontmatter, and create a new field by using createRemoteNodeField. This allows us to read the image as an external resource, and then add it back to the schema. Since it is an image, it will go through the sharp transformation and create a queryable node that we can use with gatsby-plugin-image. To do this, let's add some code to the gatsby-node.js file:

1exports.createResolvers = ({
2 actions,
3 cache,
4 createNodeId,
5 createResolvers,
6 store,
7 reporter,
8}, pluginOptions) => {
9 const { createNode } = actions
10
11 createResolvers({
12 Mdx: {
13 cover: {
14 type: `File`,
15 resolve(source, args, context, info) {
16 return createRemoteFileNode({
17 url: source.frontmatter.cover,
18 parentNodeId: source.id,
19 cache,
20 createNode,
21 createNodeId,
22 reporter,
23 })
24
25 }
26 },
27 },
28 })
29}

And that's it! Here, we are telling Gatsby to create a new node for the mdx nodes. Essentially, every time you read an mdx node, you add a new attribute called cover, which will be a File. This file will be created by createRemoteFileNode, which reads the source.frontmatter.cover string.

Read more about 'createRemoteFileNode` in the Gatsby official documentation.

Now, we can use the powerful Gatsby image processing features in our post component.

How to use gatsby-plugin-image

The gatsby-plugin-image that we already installed and configured offers a way to tap into the Gatsby image processing features by exposing two components: StaticImage and GatsbyImage.

The StaticImage component is supposed to be used with images that are not meant to change, like a logo on your site. On the other hand, GatsbyImage is meant to be used with dynamic data, like the images that come from a CMS, or in our case, from the file system.

To use this plugin, we need to update our PostPage component to retrieve the correct information in the query. Let's do it!.

1export const query = graphql`
2 query($id: String) {
3 mdx(id: { eq: $id }) {
4 frontmatter {
5 date
6 description
7 }
8 cover {
9 childImageSharp {
10 gatsbyImageData(width: 600)
11 }
12 }
13 body
14 }
15 }
16`

Here, we remove the cover attribute from the frontmatter query. It was a new attribute that reads the childImageSharp information and retrieves a "function" named gatsbyImageData that returns a data structure with all the information and processing on the image. You can configure even further by, for example, adding a placeholder.

1cover {
2 childImageSharp {
3 gatsbyImageData(width: 600, placeholder: BLURRED)
4 }
5 }

This will add a very low-resolution version of the source image and display it as a blurred background.

Find more information about the different configurations for 'gatsbyImageData` in Gatsby's documentation.

Our last step is to update the actual component code to use the new gatsbyImageData. For that, we will use a helper that is provided by the plugin called getImage. This helper takes a File node as our coverattribute and returns the file.childImageSharp.gatsbyImageData information, so our component will look like this:

1... //the previous imports
2import { GatsbyImage, getImage } from "gatsby-plugin-image"
3
4export default function PostPage({ data: { mdx} }) {
5 const image = getImage(mdx.cover)
6 return (
7 <div>
8 <h1>{mdx.frontmatter.title}</h1>
9 <GatsbyImage image={image} alt={mdx.frontmatter.title} />
10 <br />
11 <span>{mdx.frontmatter.date}</span>
12 <MDXRenderer>{mdx.body}</MDXRenderer>
13 </div>
14 )
15}

Now, you have access to the processing power of Gatsby in your MDX files while using the Filesystem route API.

You can check this example in the following repository, visit the deployed example here or fork this codesandbox

Conclusion

We just learned how to use the Filesystem Route API from Gatsby, a powerful convention for declaratively creating pages. But we found an issue. We can't use the image processing features of Gatsby with the frontmatter data that we have by using this approach. Because of this, we tapped into the Gatsby build system by adding a resolver for our Mdx files that creates a new File node from the data of the image URL. This new File field allows us to use the gatsby-image-plugin capabilities to get responsive images in our PostPage component.

It is worth mentioning that this will slow the build process since it means that for each MDX file, Gatsby needs to download the cover image and process it to store it in the filesystem and make it available to your GraphQL query.

If you are using an image storage service such as Cloudinary that pre-processes your image, and performs transformations on the fly, this process isn't necessarily required. You will need to evaluate your performance requirements, however.

Resources

Matías Hernández

Senior Frontend Engineer

Matías is a Chilean Software Engineer, father, host of two podcasts and egghead, and Escuela Frontend, instructor.

He focuses on front-end development and shares what he knows and learns with the community through articles and video lessons tailored to the Spanish community.

Matías host two podcasts: Café con Tech and Control Remoto, and write for different tech publications.

You can always reach him on twitter