Gallery in Gatsby and MDX


Martin Koparanov

September 18, 2019

I needed to implement a gallery in my portfolio section of the website (the one you are reading this on hopefully), which is built with Gatsby and MDX. I tried a bunch of workarounds and disgusting hacks to get it working with gatsby-remark-images, but none of them really worked the way I wanted to. I got really frustrated because it seemed like no one else has ever thought of this possibility and no relevant results came up when searching for it. I couldn't let it slide, but got too frustrated to keep working on it, so I went to sleep. The next day, my fresh mind was seeing things more clearly, so I managed to come up with a somewhat satisfactory solution.

Disclaimer #1: In this post I assume you are familiar with Gatsby and MDX, and you know how to make a blog with it.

Disclaimer #2: I want to find better solutions to this problem, so I am hoping to spawn a discussion from this post, which you can join too!

If you just want to see example code, you can take a look at the example repository for this post.

What it does

It generates small thumbnail for the grid view of the gallery, and a big version to display when opening the image in a modal. Also allows you to put your any gallery anywhere you want in the document.

Demonstration

Here is the result you will achieve by the end of this tutorial.

Demo gallery

This is the story of the war between cats and dogs. Pick your side.

The dog army

The cat army

The code

This is how the MDX document looks like.

The frontmatter part:

You first define what galleries you want and list the images you'd like to display...

...
galleries:
- id: dogs
images:
- src: ./images/doggos/comrade.png
title: Soviet Dog
description: This dog comes from Russia. He has the nuclear power
to destroy half the world and all the cats with it.
...
- src: ./images/doggos/boxer.jpg
title: 4 Fists Phil
description: Phil is the dog army's secret weapon.
He is the one who shall take down Rambo Cat.
- id: cats
images:
- src: ./images/cades/cabbage cade.jpg
title: Cabbage Cade
description: Cabbage Cade is well known among the cat's community.
He is the leader of the cat's army and Lieutenant Lettuce's personal enemy.
...
- src: ./images/cades/chungus.jpg
title: Slightly Smaller Chungus
description: Big Chungus's smaller cousin, who also happens to be big,
but is slightly smaller than Big Chungus.
Still, do not underestimate him.

Usage in the document

Then you place your gallery anywhere you want in the document

<h3>The dog army</h3>
<Gallery id="dogs" />
<h3>The cat army</h3>
<Gallery id="cats" />

How to achieve this

Now let's get to the interesting part. The part where I tell you how I made this.

The GraphQL query

This is basically your standard GraphQL query for a blog post, except now we have the "galleries" field in the frontmatter. We generate a small and big version of the images. One for previewing in the grid, and one for displaying in the modal.

query($slug: String!) {
mdx(fields: { slug: { eq: $slug } }) {
id
excerpt(pruneLength: 160)
frontmatter {
title
date(formatString: "MMMM DD, YYYY")
galleries {
id
images {
title
description
src {
childImageSharp {
preview: fluid(
maxWidth: 200
maxHeight: 200
cropFocus: ATTENTION
) {
...GatsbyImageSharpFluid
}
big: fluid(maxWidth: 1200) {
...GatsbyImageSharpFluid
}
}
}
}
}
}
body
}
}

The gallery component

This is the component responsible for displaying the images in a grid.

import React, {useState, useCallback} from 'react';
import {Row, Col} from 'reactstrap';
import Img from 'gatsby-image';
import GalleryModal from './GalleryModal';
const MdxGallery = ({images}) => {
const [index, setIndex] = useState(0);
const [modalOpen, setModalOpen] = useState(false);
const handleClick = useCallback((i) => {
setIndex(i);
setModalOpen(true);
}, [setIndex, setModalOpen]);
const prev = useCallback(() => {
setIndex(index === 0 ? images.length - 1 : index-1);
}, [index, setIndex, images]);
const next = useCallback(() => {
setIndex(index === images.length - 1 ? 0 : index+1);
}, [index, setIndex, images]);
return (
<Row>
<GalleryModal isOpen={modalOpen} setModalOpen={setModalOpen} image={images[index]} prev={prev} next={next} />
{images.map((img, i) => {
if(process.env.NODE_ENV !== 'production' && !img.src) {
// this gets removed in production, so don't worry
console.error(`This image is missing "src", you probably messed up the path to it: ${img}`);
return <>MISSING IMAGE</>
}
return (
<Col key={i} xs="6" md="3" className="p-2">
<button style={{border: 'none', background: 'transparent'}} className="w-100 h-100" onClick={() => handleClick(i)}>
<Img fluid={img.src.childImageSharp.preview} />
</button>
</Col>
);
})}
</Row>
);
};
export default MdxGallery;

Pretty straightforward. It receives an array of Gatsby-built images and displays them. It also controls the modal and switching between images.

The modal

import React, { useCallback } from 'react'
import {
Modal,
Carousel,
CarouselItem,
CarouselControl,
ModalHeader,
ModalBody,
ModalFooter,
Button,
} from 'reactstrap'
import Img from 'gatsby-image'
const GalleryModal = ({ image, setModalOpen, isOpen, prev, next }) => {
const toggle = useCallback(() => {
setModalOpen(!isOpen)
}, [isOpen, setModalOpen])
return (
<Modal isOpen={isOpen} toggle={toggle}>
<ModalHeader tag="h3" toggle={toggle}>
{image.title}
</ModalHeader>
<ModalBody>
<Carousel activeIndex={0} previous={prev} next={next} interval={false}>
{[
<CarouselItem key={0}>
<Img
fluid={image.src.childImageSharp.big}
alt={image.description || image.title}
/>
</CarouselItem>,
]}
<CarouselControl
onClickHandler={prev}
direction="prev"
directionText="Previous"
/>
<CarouselControl
onClickHandler={next}
direction="next"
directionText="Next"
/>
</Carousel>
<p className="my-3">{image.description}</p>
</ModalBody>
<ModalFooter>
<Button onClick={toggle}>Close</Button>
</ModalFooter>
</Modal>
)
}
export default GalleryModal

Also pretty straightforward. I used a carousel for displaying the image to make use of Bootstrap's accessibility features. You can do it however you want.

The page template

This is the part that connects the query with the gallery components. In the blog template, we have our usual MDXProvider with our MDX Components. This time, we enrich the components passed to MDXProvider with a component which we create for every blog post.

I removed the unimportant parts

import React, { useMemo } from 'react'
import { Link, graphql } from 'gatsby'
import { MDXRenderer } from 'gatsby-plugin-mdx'
import { MDXProvider } from '@mdx-js/react'
import Layout from '../components/Layout'
import MdxGallery from '../components/MdxGallery'
// any other components you want to have
const components = {}
const BlogPostTemplate = ({ data, pageContext }) => {
const allComponents = useMemo(() => {
const GalleryComponent = ({ id, ...props }) => {
const galleries = data.mdx.frontmatter.galleries.reduce(
(acc, gallery) => {
acc[gallery.id] = gallery.images
return acc
},
{}
)
return <MdxGallery images={galleries[id]} {...props} />
}
return {
...components,
Gallery: GalleryComponent,
}
}, [data])
const post = data.mdx
const { previous, next } = pageContext
return (
<Layout>
<MDXProvider components={allComponents}>
<h1>{post.frontmatter.title}</h1>
<MDXRenderer>{post.body}</MDXRenderer>
</MDXProvider>
</Layout>
)
}
export default BlogPostTemplate
export const pageQuery = ... //already shown in the tutorial

This creates a new Gallery component, which has an "id" prop and wraps our original Gallery component. Basically, it makes a map of the existing galleries and the "id" prop picks the correct gallery which then gets passed on to our original Gallery component.

That's it! Now all you have to do is upload your images, define your galleries and go use them!

Conclusion

I hope you got an idea of how I achieved this. Obviously, you can implement it however you like - have different meta fields (such as title, description), implement your modal however you want, display them in a list, not a grid - the choice is yours!

As I said, this is not a perfect solution, it is just the best I could come up with. If you have any better ideas, I would be thrilled to hear them! Join the discussion!


Categories: Discussion, Frontend, Tutorial,

Tags: gatsby, mdx, react,