We're live-coding on Twitch! Join us!
Serving Remote Optimized Images w/ gatsby-image

Serving Remote Optimized Images w/ gatsby-image

Even though a picture is worth a thousand words, as the English adage goes, it costs a pretty penny to store and deliver images online. Hence the common goal of many modern web-development tools to minimize the impact of images and other media assets on site performance.

This post, part 1 of a three-part series, shows you how to build an optimized webpage by leveraging gatsby-transformer-cloudinary plugin and gatsby-image, ultimately sourcing and transforming responsive remote images in a GatsbyJS project.

Here are the steps:

  1. Install GatsbyJS 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 with Cloudinary.
  6. Design a responsive layout and typography with Chakra UI.

Normally with gatsby-image, images are stored locally in the project and to utilize external images it has to come from a source, hence we use gatsby-transformer-cloudinary to fetch remote images.

At the end of this project, you will be able to:

  1. Source and lazy-load remote images with gatsby-image
  2. Transform images using gatsby-transformer-cloudinary
  3. Design a responsive app with Chakra UI
  4. Add progressive web app (PWA) and offline support for the webpages
  5. Add Dark mode to your site with Chakra UI

The final app looks like this:

Prerequisites

This project requires knowledge of JavaScript, React, and basics of GatsbyJS. You install GatsbyJS and other packages with Node.js and its package manager npm, a viable alternative for which is Yarn.

Installation

Follow these steps:

  1. Verify that Node.js and npm have been installed in your system by typing this command:```bashnode -v && npm -v```
The output prints the version numbers of Node.js and npm. Otherwise, download and install both at [nodejs.org](https://nodejs.org/).
  1. Install GatsbyJS globally with npm. Type:```bashnpm install -g gatsby-cli```

  2. Create a Gatsby project in a directory of your choice. Type:```bashgatsby new gtc-demo```The default GatsbyJS starter then creates the project.

Note: To give you a jump-start, Gatsby scaffolds new projects with a starter, which comprises several pages and modules, which Gatsby then removes as the building process proceeds.

  1. Install these required packages:

  2. gatsby-transformer-cloudinary, a plugin that enables Cloudinary’s superb optimization and transformation capabilities in gatsby-image.

  3. dotenv, a module that handles environment variables.

  4. chakra-ui, a UI library that efficiently builds React.js interfaces. Install gatsby-plugin-chakra-ui and its peer dependencies.

  5. Gatsby-plugin-chakra-ui, a plugin that leverages Chakra UI in Gatsby projects.

Type the following npm command line in the project directory:

cd gtc-demo && npm i --save gatsby-transformer-cloudinary dotenv gatsby-plugin-chakra-ui @chakra-ui/core @emotion/core @emotion/styled emotion-theming

Setup of Project Configurations

Perform the three steps below to configure the project.

1. Create a Cloudinary account

Sign up for a Cloudinary account. Cloudinary offers a free tier, which is more than adequate for small to medium projects.

Afterwards, jot down your cloud name, API key, and API secret for later use. They are displayed on your Cloudinary dashboard, as in this example.

2. Set up gatsby-config.js

Gatsby touts two types of plugins:

  • Source plugins, which fetch data from many sources into Gatsby projects.

  • Transformer plugins, which convert sourced data to usable formats.

As a start, gatsby-transformer-cloudinary uploads images from a local directory to Cloudinary, transforms them to a format usable by gatsby-image, and then returns them. Cloudinary serves as a drop-in replacement for gatsby-plugin-sharp for harnessing gatsby-image’s native image-processing capabilities.

Ultimately, Gatsby ships with a configuration file named gatsby-config.js in the root of the project, after which you can configure the installed plugins in that file.

To set up gatsby-transformer-cloudinary and gatsby-plugin-chakra-ui, update the array of plugins in gatsby-config.js, as follows:

require('dotenv').config();

module.exports = {
  siteMetadata: {...},
  plugins: [
    `gatsby-plugin-react-helmet`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `cloudinary-images`,
        path: `${__dirname}/src/cloudinary-images`,
      }
    },
    {
      resolve: 'gatsby-transformer-cloudinary',
      options: {
        cloudName: process.env.CLOUDINARY_CLOUD_NAME,
        apiKey: process.env.CLOUDINARY_API_KEY,
        apiSecret: process.env.CLOUDINARY_API_SECRET,
        uploadFolder: 'gtc-art-gallery',
      },
    },
    {
      resolve:`gatsby-plugin-chakra-ui`,
      options: {
        isUsingColorMode: true,
      }
    },
    [...]
  ],
}

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

To start, create a folder in the src directory of the project with this command:

mkdir cloudinary-images

Afterwards, upload all your media assets to that folder.

gatsby-plugin-chakra-ui contains the configuration option for color mode with two choices: true and false.

3. Add environment variables

In the gatsby-config.js file, gatsby-transformer-cloudinary contains options for adding your Cloudinary credentials as environment variables along with an upload folder. That folder could be an existing one on Cloudinary and, in case it doesn’t exist, the plugin creates it on Cloudinary during the app build.

To add environment variables:

  1. Create an environment file called .env in the root of your project for storing all the environment variables for security purposes. Since .env is specified in the .gitignore file, it’s not pushed to a remote repository using git.

  2. Add your Cloudinary credentials to .env:

# Find this at https://cloudinary.com/console/settings/account
CLOUDINARY_CLOUD_NAME=xxxxx

# Generate an API key pair at https://cloudinary.com/console/settings/security
CLOUDINARY_API_KEY=xxxxxxxxxxxxxx
CLOUDINARY_API_SECRET=xxxxxxxxxxxxxxxxxxx

For the other options for gatsby-transformer-cloudinary, see its readme.

gatsby-plugin-chakra-ui is included with the config option to use color mode. This config option is either true or false.

Next, create a development environment for leveraging Gatsby's development tools, including hot reloading and the GraphQL query builder, and for viewing the project in the browser at runtime. Type this command:

gatsby develop

Once created, the development server runs on http://localhost:8000/.

Here's what the project looks like for now:

Design of Page Layout

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

Currently, the layout contains a GatsbyJS useStaticQuery hook, which makes GraphQL queries in Gatsby components. You’ll fetch the site title from siteMetadata defined in gatsby-config.js with useStaticQuery.

Edit the layout to add a header, which is the navigation bar and body of the website, as follows:

import React from "react"
import PropTypes from "prop-types"
import {graphql, useStaticQuery} from "gatsby"
import Header from "./header"
import {Box, Text, Link} from "@chakra-ui/core/dist";

const Layout = ({children}) => {

    const data = useStaticQuery(graphql` `query SiteTitleQuery {
      site {
        siteMetadata {
          title
        }
      }
    }``);

    return (
        <Box>
            <Header siteTitle={data.site.siteMetadata.title}/>
            <Box width={['90%', '90%', '80%']} mx={'auto'}>
                <main>{children}</main>
                <Text mt={10}>For this demo, the amazing images here by great artists were all sourced from <Link href={"https://unsplash.com/"} target={"_blank"} color="teal.500">Unsplash</Link></Text>
            </Box>
        </Box>
    )
};

Layout.propTypes = {
    children: PropTypes.node.isRequired,
};

export default Layout

In the above code:

  • 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. Most properties of Chakra UI’s components follow that pattern in assigning values for various screen sizes.

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

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

import {Box, Flex, Heading} from "@chakra-ui/core/dist";

const Header = ({siteTitle}) => {
    return (
        <Flex
            as="nav"
            align="center"
            justify="space-between"
            wrap="wrap"
            px={["0.5em", "0.5em", "1.5em"]}
            py={["1em", "1em", "1.5em"]}
            bg="blue.900"
            color="white"
        >
            <Flex align="flex-start">
                <Heading as="h1">
                    <GatsbyLink to="/">
                        <Box color={'white.800'}>
                            <Text fontSize={["md", "md", "lg"]}>{siteTitle}</Text>
                        </Box>
                    </GatsbyLink>
                </Heading>
            </Flex>
            <Flex align="flex-end">
                <Button variantColor={'blue'} mr={2} size={"xs"}>
                    <GatsbyLink to="/">Home</GatsbyLink>
                </Button>
            </Flex>
        </Flex>
    )
};

As demonstrated above, Chakra UI components and styles are handy for building a simple navigation bar, accompanied by the site title and a button to return to the homepage on a click.

Creation of Pages

Now create two pages, one to display a responsive banner image; and the other, multiple images in a responsive grid.

Responsive Banner Image

Build the first page, which is the homepage, with these two steps:

  1. Query a single fluid banner image and apply a Cloudinary transformation.
  2. Render the banner.

Do the following:

1. Edit the file src/pages/index.js with the code below to import the required modules:

import React from "react"
import {graphql, Link, useStaticQuery} from "gatsby";
import Layout from "../components/layout"
import SEO from "../components/seo"
import Image from "gatsby-image"
import {Box, Button, Heading, Text} from "@chakra-ui/core/dist";

The useStaticQuery hook is for querying the images in this component. Alternatively, since this is a page component, you can use a page query to allow access to variables passed in pageContext. None of that is required, however; StaticQuery is much more straightforward.

2. Create and export a functional component called IndexPage. In that component, query the banner image with the query below and assign it to a variable called data.

const IndexPage = () => {
    // fetch images
    const data = useStaticQuery(graphqlquery BannerImage {
      bannerImage: file(name: { eq: "7" }) {
        cloudinary: childCloudinaryAsset {
          fluid(transformations:["e_grayscale"] maxWidth: 1500) {
            ...CloudinaryAssetFluid
          }
        }
      }
    });
    return (
        <Layout>
            [...]
        </Layout>
    )
};

export default IndexPage

3. Assign the returned image from the query into a variable in the component with this code:

const IndexPage = () => {
    // fetch images
    const data = useStaticQuery([...]);

    // Assign the returned images to variables.
    const bannerImage = data.bannerImage.cloudinary.fluid;

    return (
    [...]
    )
};

Returned is a fluid image that fits the entire width of its parent container.

With gatsby-transformer-cloudinary, Cloudinary transformations are passed to the queries and applied to the resulting image. Here, you want an image with a grayscale effect.

Note that you can pass multiple transformations, including chained ones, to the query. For more details, see Cloudinary’s reference documentation on image transformations.

The ...CloudinaryAssetFluid fragment contains aspectRatio, the base64 image, src, sizes, and srcSet of the returned image. Where those data fields must be specified in a query, fragments take the place of the GraphQL queries.

Here’s the original image on Cloudinary:

Here's the transformed grayscale image:

By default, gatsyby-transformer-cloudinary optimizes the image quality and format

All the images for this project render using gatsby-image. This method provides all the native image processing and optimization capabilities of GatsbyJS including image responsiveness and lazy-loading.

Chakra UI builds minimalistic interfaces with no need for CSS. Box, Button, Heading, and Text are the required Chakra UI components for building the page.

The SEO component, which is shipped with the default starter, adds SEO properties to the page.

Once you’ve completed the queries, render the banner image in the component with this code:

const IndexPage = () => {
    [...]
    return (
        <Layout>
            <SEO title="Home"/>
            <Box mb={[10, 20, 100]}>
                <Heading size={'xl'} m={3} textAlign={"center"}>Responsive Banner Image</Heading>
                <Box>
                    <Image fluid={bannerImage}/>
                </Box>
            </Box>
            <Text my={5}>Click any of the buttons below to see the gallery or single Image with the <i>getFluidImageObject</i> API</Text>

            <Box>
                <Button variantColor={'teal'} mr={10} mb={[2, 0, 0]}>
                    <Link to="/gallery"> Gallery Images</Link>
                </Button>
            </Box>
        </Layout>
    )
};

By way of explanation:

  • As a supercharged div element, the Box component functions like a normal div and accepts properties, including their responsive breakpoints, for extended functionality.

  • With the Chakra UI, you create responsive sizes by passing an array value to component properties. That array has sizes that represent mobile, desktop, and tablet, in that order.

  • The Text and Heading components implement responsive typography.

  • gatsby-image's <Image/> component accepts a property to render either a defined fixed-width image or a responsive fluid image. This project displays a lazy-loaded fluid image with generated responsive versions.

  • The Button component navigates to the gallery page through Gatsby's Link component.

Finally, restart the development server to view the homepage with the banner Image and drag the browser width to test the responsiveness. Also test the page in various mobile widths.

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

Just as you did with the single-banner image, make a query for fluid images and then load them into the gallery.

1. Create a file called gallery.js in the pages directory. Import the required dependencies into the file and then make a graphQL query for the first nine images with the ...CloudinaryAssetFluid fragment. Here’s the code:

import Image from "gatsby-image"
import {Box, Heading, SimpleGrid} from "@chakra-ui/core/dist";
import {graphql, useStaticQuery} from "gatsby";


const SinglePage = () => {
    const data = useStaticQuery(graphql`query GalleryImages{
      listImages: allCloudinaryAsset(limit: 9) {
        images: edges {
          node {
            fluid {
              ...CloudinaryAssetFluid
            }
          }
        }
      }
    }`);

    const galleryImages = data.listImages.images;

    return (
        [...]
    )
};

export default SinglePage

2. Render the gallery in the component with the Chakra UI SimpleGrid component, as follows:

const SinglePage = () => {
    const data = useStaticQuery([...]);

    const galleryImages = data.listImages.images;

    return (
        <Layout>
            <SEO title={"single"}/>
            <Box mx={'auto'} my={10}>
                <Heading textAlign={"center"} size={"xl"} mb={10}>Optimized Gallery Images</Heading>
                <SimpleGrid columns={[1, 2, 3]} spacing={2}>
                    {galleryImages.map((val, index) => (
                        <Box key={index} p={3} m={2} my={"auto"} shadow="md" borderWidth="1px" rounded={'lg'}>
                            <Image fluid={val.node.fluid}/>
                        </Box>
                    ))}
                </SimpleGrid>
            </Box>
        </Layout>
    )
};

export default SinglePage

You map through the array of returned images in the render function in a spaced grid with the gatsby-image <Image/> component.

Here's what the final grid gallery looks like:

Have a look at the project deployed on Netlify. For the complete code, see the GitHub repository.

Summary

You’ve now learned how to fetch optimized Images from Cloudinary, render them as fluid images with gatsby-image, which offers all of GatsbyJS's native image-optimization and Cloudinary’s transformation capabilities.

Part 2 will describe how to fetch a single fluid image with the gatsby-transformer-cloudinary API with no need for graphQL queries for passing Cloudinary images into gatsby-image. It’s fun! Do stay tuned.

Like this article? Follow @iChuloo on Twitter