Serving Remote Optimized Images w/ gatsby-image

Banner for a MediaJam post

William Imoh

Even though a picture is worth a thousand words, it costs a pretty penny to store and deliver images online. Because of this, many modern web-development tools aim to minimize the impact of images, and other media assets, on site performance.

This post shows you how to build an optimized webpage by leveraging the gatsby-transformer-cloudinary plugin and gatsby-image. We’ll upload local images to a remote content delivery network (Cloudinary), then subsequently source, and transform, the responsive remote images in a GatsbyJS project.


We completed this project on Codesandbox, and you can find it here. You need to provide your Cloudinary credentials as environment variables in the sandbox by creating a .env file in the root directory. Specify the environment variables as shown in .env.example.

Here are the steps we’ll follow to use remote images in a gatsby project with gatsby-image:

  1. Install Gatsby.js and its dependencies.
  2. Set up the project configuration and layout.
  3. Handle single- or multiple-image queries from Cloudinary through gatsby-transformer-cloudinary.
  4. Optimize the sourced images with Cloudinary and lazy-load them with gatsby-image.
  5. Transform the images using Cloudinary right in the graphql image query.
  6. Design a responsive layout and typography with Chakra-ui.

Typically, with gatsby-image, images are stored locally in the project. To utilize external images, a hosted image is required and used with a normal HTML <img/> element. With gatsby-transformer-cloudinary, we can utilize remote images in gatsby-image.

Prerequisite and Installation

This project requires knowledge of JavaScript, React, and the basics of Gatsby.js. You install Gatsby.js and other packages with Node.js and its package manager npm, or Yarn, a viable alternative for it.

We install Gatsby.js globally with npm, and create a new project using the command

1npm install -g gatsby-cli && gatsby new gtc-demo

To give you a jump-start, Gatsby scaffolds new projects with a starter, which comprises several pages and modules, and we’ll remove them when they’re obsolete.

We proceed to install the required dependencies using the command:

1cd gtc-demo && npm i --save gatsby-transformer-cloudinary dotenv @chakra-ui/gatsby-plugin @chakra-ui/react @emotion/react @emotion/styled framer-motion

Project configuration setup

Sign up for a Cloudinary account. Cloudinary offers a free tier, which is more than adequate for small to medium projects. Once signed up, note your cloud name, API key, and API secret for later use.

Gatsby touts two types of plugins:

  • Source plugins that fetch data from many sources into Gatsby projects.
  • Transformer plugins that convert sourced data to usable formats.

gatsby-transformer-cloudinary uploads images from a local directory to Cloudinary, and transforms the returned image to a format usable by gatsby-image. Cloudinary serves as a drop-in replacement for gatsby-plugin-sharp, and harnesses gatsby-image’s native image-processing capabilities.

Gatsby ships with a configuration file named gatsby-config.js in the root of the project. We’ll set up gatsby-transformer-cloudinary, and @chakra-ui/gatsby-plugin, by updating the array of plugins in gatsby-config.js, to include this:

2 module.exports = {
3 siteMetadata: {
4 title: `Gatsby x Cloudinary`,
5 description: `Serve remote images in your Gatsby app using gatsby-image`,
6 author: `@gatsbyjs`
7 },
8 plugins: [
9 `gatsby-plugin-react-helmet`,
10 {
11 resolve: `gatsby-source-filesystem`,
12 options: {
13 name: `cloudinary-images`,
14 path: `${__dirname}/src/remote-images`
15 }
16 },
17 {
18 resolve: "@chakra-ui/gatsby-plugin",
19 options: {
20 isUsingColorMode: true
21 }
22 },
23 {
24 resolve: "gatsby-transformer-cloudinary",
25 options: {
26 cloudName: process.env.CLOUDINARY_CLOUD_NAME,
27 apiKey: process.env.CLOUDINARY_API_KEY,
28 apiSecret: process.env.CLOUDINARY_API_SECRET,
29 uploadFolder: "gtc-art-gallery"
30 }
31 }
32 ]
33 }

In the configuration file, gatsby-source-filesystem, a source plugin sources file nodes into the Gatsby data layer. Here, we’ve sourced all the images in a folder, which are uploaded once to Cloudinary on build.

We created the remote-images folder in the src directory of the project, and uploaded all the images into the folder.

We proceed to create a .env file in the project’s root directory, and add our Cloudinary credentials as specified below:

1# Find this at
4 # Generate an API key pair at
5 CLOUDINARY_API_KEY=xxxxxxxxxxxxxx
6 CLOUDINARY_API_SECRET=xxxxxxxxxxxxxxxxxxx

Next, we start the development server on localhost:8000 by running the command:

1gatsby develop

All the images in the remote-images folder are uploaded to Cloudinary, and added to file nodes for the returned URLs are created in Gatsby’s graphql layer.

Pages layout design

The layout.js file in the src/components directory specifies the pages’ design, which persists across the site, and can contain UI elements like headers and footers.

We edit the layout to add a header, which contains the navigation bar and body of the website. First, we import all required dependencies with:

1import React from "react"
2 import PropTypes from "prop-types"
3 import { graphql, useStaticQuery } from "gatsby"
4 import Header from "./header"
5 import { Box, Text, Link } from "@chakra-ui/react"

Next, we define the layout component, and fetch the site title defined in gatsby-config.js using useStaticQuery, a hook that utilizes Gatsby.js's StaticQuery API to make GraphQL queries in Gatsby.js components.

1const Layout = ({ children }) => {
2 const data = useStaticQuery(graphql`
3 query SiteTitleQuery {
4 site {
5 siteMetadata {
6 title
7 }
8 }
9 }
10 `)
11 return (
12 <Box>
13 <Header siteTitle={} />
14 <Box width={["90%", "90%", "80%"]} mx={"auto"}>
15 <main>{children}</main>
16 <Text mt={10}>
17 For this demo, the amazing images here by great artists were all
18 sourced from{" "}
19 <Link
20 href={""}
21 target={"_blank"}
22 color="teal.500"
23 >
24 Unsplash
25 </Link>
26 </Text>
27 </Box>
28 </Box>
29 )
30 }
31 Layout.propTypes = {
32 children: PropTypes.node.isRequired
33 }
34 export default Layout

In the snippet above

  • The <Header/> custom component represents the navigation bar. Chakra-ui styles the layout with different responsive widths and multiple breakpoints: 90 percent, 90 percent, and 80 percent for mobile, tablet, and desktop devices, respectively.

  • The mx property sets the page’s horizontal-margin property to auto, centering the page content regardless of the width.

Next, we add the site title and the navigation bar’s dimensions by updating the Header functional component in src/components/header.js, as follows.

We import the required dependencies, and UI components.

1import { Link as GatsbyLink } from "gatsby"
2 import PropTypes from "prop-types"
3 import React from "react"
4 import { Box, Flex, Heading, Text, Button } from "@chakra-ui/react"

Next, we create a Header component to render a minimal navigation bar with a menu link.

1const Header = ({ siteTitle }) => {
2 return (
3 <Flex
4 as="nav"
5 align="center"
6 justify="space-between"
7 wrap="wrap"
8 px={["0.5em", "0.5em", "1.5em"]}
9 py={["1em", "1em", "1.5em"]}
10 bg="blue.900"
11 color="white"
12 >
13 <Flex align="flex-start">
14 <Heading as="h1">
15 <GatsbyLink to="/">
16 <Box color={"white.800"}>
17 <Text fontSize={["md", "md", "lg"]}>{siteTitle}</Text>
18 </Box>
19 </GatsbyLink>
20 </Heading>
21 </Flex>
22 <Flex align="flex-end">
23 <Button colorScheme={"blue"} mr={2} size={"sm"}>
24 <GatsbyLink to="/">Home</GatsbyLink>
25 </Button>
26 </Flex>
27 </Flex>
28 )
29 }

Lastly, we define the proptypes, the component's default prop values, and export the Header component.

1Header.propTypes = {
2 siteTitle: PropTypes.string
3 }
4 Header.defaultProps = {
5 siteTitle: ``
6 }
7 export default Header

We use Chakra-ui components and styles to build a navigation bar, accompanied by the site title and a button to return to the homepage with a click.

Pages creation

We have two pages: a page with a responsive banner image, and a page with a responsive image gallery.

Responsive banner image In src/pages/index.js, we’ll import the required dependencies and make a GraphQL query for a Cloudinary Image.

First, we import the required dependencies and make a GraphQL query for the fluid images:

1import React from "react"
2 import { graphql, Link, useStaticQuery } from "gatsby"
3 import Layout from "../components/layout"
4 import SEO from "../components/seo"
5 import Image from "gatsby-image"
6 import { Box, Button, Heading, Text } from "@chakra-ui/react"
8 const IndexPage = () => {
9 // fetch images
10 const data = useStaticQuery(graphql`
11 query BannerImage {
12 bannerImage: file(name: { eq: "7" }) {
13 cloudinary: childCloudinaryAsset {
14 fluid(transformations: ["e_grayscale"], maxWidth: 1500) {
15 ...CloudinaryAssetFluid
16 }
17 }
18 }
19 }
20 `)
21 // Assign the returned images to variables.
22 const bannerImage = data.bannerImage.cloudinary.fluid
24 return (
25 // component JSX to go in here
26 )

The useStaticQuery hook is for querying the images in this component. The ...CloudinaryAssetFluid fragment returns a fluid image data that contains aspect ratio, the base64 image, src, sizes, and srcSet. Where we must specify those data fields in a query, fragments take the place of the GraphQL fields.

Just as we added e_grayscale transformation in the query, multiple Cloudinary transformations including format and quality types, can be utilized. You can find transformation options here.

Next, we return the JSX elements to render the page. The elements are Chakra-ui components.

1return (
2 <Layout>
3 <SEO title="Home" />
4 <Box mb={[10, 20, 100]}>
5 <Heading size={"xl"} m={3} textAlign={"center"}>
6 Responsive Banner Image
7 </Heading>
8 <Box>
9 <Image fluid={bannerImage} />
10 </Box>
11 </Box>
12 <Text my={5}>
13 Click any of the buttons below to see the gallery or single Image with
14 the <i>getFluidImageObject</i> API
15 </Text>
16 <Box>
17 <Button colorScheme={"teal"} mr={10} mb={[2, 0, 0]}>
18 <Link to="/gallery"> Gallery Images</Link>
19 </Button>
20 </Box>
21 </Layout>
22 )
23 }
24 export default IndexPage

We pass the fluid image data to the gatsby-image <Image/> component to render the image.

To see the lazy loading in action, we refresh the page or look in the browser’s network tab. Since subsequent refreshes returned cached image versions, we have to do a hard refresh of the page or throttle the network to spot the lazy loading.

Gallery Images Like the single responsive image, we’ll create a page by creating a file in the src/pages directory called gallery.js. Gatsby.js handles the automatic page creation of any file added to the src/pages directory.

We proceed to import the required dependencies and query all the uploaded images.

1import React from "react"
2 import Image from "gatsby-image"
3 import { Box, Heading, SimpleGrid } from "@chakra-ui/react"
4 import { graphql, useStaticQuery } from "gatsby"
5 import Layout from "../components/layout"
6 import SEO from "../components/seo"
8 const SinglePage = () => {
9 const data = useStaticQuery(graphql`
10 query GalleryImages {
11 listImages: allCloudinaryAsset(limit: 9) {
12 images: edges {
13 node {
14 fluid {
15 ...CloudinaryAssetFluid
16 }
17 }
18 }
19 }
20 }
21 `)
22 const galleryImages = data.listImages.images

Next, we render the image gallery component using SimpleGrid - a Chakra-ui components to create grids using CSS-grid.

1return (
2 <Layout>
3 <SEO title={"single"} />
4 <Box mx={"auto"} my={10}>
5 <Heading textAlign={"center"} size={"xl"} mb={10}>
6 Optimized Gallery Images
7 </Heading>
8 <SimpleGrid columns={[1, 2, 3]} spacing={2}>
9 {, index) => (
10 <Box
11 key={index}
12 p={3}
13 m={2}
14 my={"auto"}
15 shadow="md"
16 borderWidth="1px"
17 rounded={"lg"}
18 >
19 <Image fluid={val.node.fluid} />
20 </Box>
21 ))}
22 </SimpleGrid>
23 </Box>
24 </Layout>
25 )
26 }
27 export default SinglePage

Using Chakra-ui components, we loop through the returned list of fluid images and render each in a gatsby-image component.

You can see the final result here, and the pages look like this:

How about we look at how to use remote Images in gatsby-image without the GraphQL query overhead? We’ll discuss that in a subsequent post.


In this post, we saw how to utilize remote optimized images from Cloudinary in a Gatsby site. We render the images using Gatsby.js's gatsby-image component to provide performance optimizations during the site's build process.

You may find these 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.