Picture
Calvin Tyndall Drupal Developer Follow
September 16, 2020

Gatsby Image, along with gatsby-transformer-sharp and gatsby-plugin-sharp, is a common way to handle images in a Gatsby project.  It gives us the power to process source images at build time to create a 'srcSet' for  '<img>' or '<picture>' tags, or create versions in grayscale, duotone, cropped, rotated, etc.

When Gatsby builds from a Drupal source, it downloads the source images and processes them to create these image variations which are stored in the public directory. The ability to optimize and art-direct images at build time is great, but build performance suffers while these assets are generated. As a site grows in images, the time it takes to build grows as well. Image processing can take hours, while the rest of the build takes mere minutes.

Drupal has the built in ability to generate its own derivative images on demand and cache them, so why not just build our Gatsby components in a way that leverages Drupal Image Styles and dramatically speed up the Gatsby build process? Gatsby can just query for the image urls and let Drupal serve them directly.

The Experiment: Image Processing Options

For this experiment, I didn’t use Gatsby Image at all, though you could format your image data in a way that the Gatsby Image component expects, and still pass that in. My goal was to explore my options for getting image variants from Drupal via JSON:API and GraphQL, and to pass that to a custom React component, so that’s what I’ll demonstrate in this post.

I’ll show examples using both the core JSON:API module, as well as the GraphQL contrib module, which gives us a different developer experience.

Drupal Modules Used

Core: 
 - jsonapi
 - rest

Contrib:
 
- graphql - An alternative to jsonapi with a developer experience that, so far, I prefer.
 - jsonapi_image_styles - required to access image styles with jsonapi, not required with graphql module.

Gatsby Configuration


{ 
  resolve: "gatsby-source-graphql", 
  options: { 
    typeName: "Drupal", 
    fieldName: "drupal", 
    url: `http://backend.lndo.site/graphql/` 
  }, 
}, 
{ 
  resolve: `gatsby-source-drupal`, 
  options: { 
    baseUrl: `http://backend.lndo.site/`, 
    apiBase: `jsonapi`, // optional, defaults to `jsonapi` 
    skipFileDownloads: true, 
  }, 
},

Here I’m configuring both the graphql and the drupal (jsonapi) sources. As this is only a local experiment, I’m not concerned with authentication and I’m using Lando/Docker for local development. In a real project, I’d extract the relevant parts to environment variables so they can be changed for the production server. I’d also only use one or the other, and not both.

In the graphql config, you’ll notice 'fieldName:' is set to 'drupal', which is what we’ll use to access the GraphQL API, as opposed to the JSON API.

In the 'gatsby-source-drupal' config I’ve set 'skipFileDownloads:' to 'true' because we will not need them for local processing or delivery. This skips all file downloads, but there are ways to be more specific using filters. https://www.gatsbyjs.com/plugins/gatsby-source-drupal/#filters

Option 1: GraphQL

Query Against the GraphQL API


query($id: String!) {
  drupal {
    nodeById(id: $id) {
      ... on Drupal_NodeArticle {
        title: entityLabel
        author: entityOwner {
          authorName: entityLabel
        }
        body {
          processed
        }
        fieldImage {
          alt
          xxl: derivative (style: XXL) {
            width
            url
          }
          xl: derivative (style: XL) {
            width
            url
          }
          large: derivative (style: LARGE) {
            width
            url
          }
          medium: derivative (style: MEDIUM) {
            width
            url
          }
          small: derivative (style: SMALL) {
            width
            url
          }
        }
      }
    }
  }
}


This query is for a specific article node in Drupal that contains a field named 'field_image'.  With the GraphQL module in Drupal, we have access to the 'derivative' function that allows us to query for the values of a specific image style.

In this case we’ve set up 5 image styles in Drupal which scale an image to different widths:

  • Small: 300
  • Medium: 600
  • Large: 1200
  • XL: 1800
  • XXL: 2400

The sizes are meant to cover a “full width” image at various breakpoints or screen resolutions.  Of course, you can set up Image Styles more suited to your needs.

We can alias each derivative by prefixing it with the style name and a colon, allowing us to have multiple derivatives in the same query, setting the style argument to the appropriate style name.

The result of the query includes all the values we need for our Image component.


        "fieldImage": {
          "alt": "Arduino",
          "xxl": {
            "width": 2400,
            "url": "http://backend.lndo.site/sites/default/files/styles/xxl/public/2020-09/arduino-harrison-broadbent_0.jpg?itok=EyvwD2ta"
          },
          "xl": {
            "width": 1800,
            "url": "http://backend.lndo.site/sites/default/files/styles/xl/public/2020-09/arduino-harrison-broadbent_0.jpg?itok=0gub18uQ"
          },
          "large": {
            "width": 1200,
            "url": "http://backend.lndo.site/sites/default/files/styles/large/public/2020-09/arduino-harrison-broadbent_0.jpg?itok=eNUhJHb6"
          },
          "medium": {
            "width": 600,
            "url": "http://backend.lndo.site/sites/default/files/styles/medium/public/2020-09/arduino-harrison-broadbent_0.jpg?itok=Rvsmw0an"
          },
          "small": {
            "width": 300,
            "url": "http://backend.lndo.site/sites/default/files/styles/small/public/2020-09/arduino-harrison-broadbent_0.jpg?itok=sNDME4Ju"
          }
        }

We can see that the url for each style is specific to the path Drupal will use to serve that version of the image. We’re letting Drupal do the work.

Next: An Article Component

Here we put it all together to render a node.


import React from 'react';
import { Link, graphql } from 'gatsby';
import Layout from '../components/layout';
import Image from '../components/image';

export const query = graphql`
query($id: String!) {
  drupal {
    nodeById(id: $id) {
      ... on Drupal_NodeArticle {
        title: entityLabel
        author: entityOwner {
          authorName: entityLabel
        }
        body {
          processed
        }
        fieldImage {
          alt
          xxl: derivative (style: XXL) {
            width
            url
          }
          xl: derivative (style: XL) {
            width
            url
          }
          large: derivative (style: LARGE) {
            width
            url
          }
          medium: derivative (style: MEDIUM) {
            width
            url
          }
          small: derivative (style: SMALL) {
            width
            url
          }
        }
      }
    }
  }
}`

const ArticleTemplate = ({ data: { drupal: { nodeById: article } }}) => {

  return (
    <Layout>
      <div className="article">
        <h1>{article.title}</h1>
        <p className="author">Posted by {article.author.authorName}</p>
        <Image
          image={article.fieldImage}
          sizes="90vw"
        />
        <div dangerouslySetInnerHTML={{ __html: article.body.processed}}></div>
        <Link to='/'>&larr; Back  to all articles</Link>
      </div>
    </Layout>
  );
};

export default ArticleTemplate;

This 'fieldImage' object is passed to the Image component as the first prop named 'image'. Our second prop is named 'sizes' and this is set manually where we call our Image component. It gives us some control over which version of the image is rendered given what we know about how wide it will be in the layout at certain breakpoints.

Next: An Image Component

I want to create a responsive image with a 'srcSet' similar to what we would get with gatsby-image.


import React from 'react';

const Image = ({image, sizes}) => {

  const derivatives = [];

  for (const derivative in image) {
    if (image[derivative].url) {
      derivatives.push(`${image[derivative].url} ${image[derivative].width}w`);
    }
  }

  const srcSet = derivatives.join();

  return (
    <img
      srcSet={srcSet}
      sizes={sizes}
      src={image.small.url}
      alt={image.alt}
    />
  );
};

export default Image;

The '<img>' tag will need 'srcSet', 'sizes', 'src' and 'alt' values. The query to the GraphQL API in Drupal collects all this information, returning it in the 'fieldImage' object that we passed to the 'image' prop. The 'for' loop iterates over the derivatives and concatenates each image URL with its width, pushing each to an array to create a set. The array is later joined to create the full value of the 'srcSet' image attribute. 
The 'src' attribute can have a value of any of the derivative urls. In this case 'image.small.url'.

Finally, the 'alt' attribute comes as part of the query, at 'image.alt'.

Option 2: JSON:API Version 

If you’re more comfortable with, or more invested in using the JSON:API rather than the GraphQL module in Drupal, you can achieve the same results with the addition of the 'jsonapi_image_styles' module, which exposes the image styles to the JSON:API.

In this case the query would look more like this:


  query($id: String!) {
    nodePage(id: { eq: $id }) {
      title
      field_image {
        alt
      }
      relationships {
        uid {
          display_name
        }
        field_image {
          image_style_uri {
            small
            medium
            large
            xl
            xxl
          }
        }
      }
      body {
        processed
      }
    }
  }

However, the results are a bit unexpected.


        "field_image": {
          "image_style_uri": [
            {
              "small": null,
              "medium": null,
              "large": "http://backend.lndo.site/sites/default/files/styles/large/public/2020-09/engin-akyurt-girl-blond-hair-unsplash.jpg?itok=MrcdTwGV",
              "xl": null,
              "xxl": null
            },
            {
              "small": null,
              "medium": "http://backend.lndo.site/sites/default/files/styles/medium/public/2020-09/engin-akyurt-girl-blond-hair-unsplash.jpg?itok=vQx__izk",
              "large": null,
              "xl": null,
              "xxl": null
            },
            {
              "small": "http://backend.lndo.site/sites/default/files/styles/small/public/2020-09/engin-akyurt-girl-blond-hair-unsplash.jpg?itok=hKO4dR34",
              "medium": null,
              "large": null,
              "xl": null,
              "xxl": null
            },
            {
              "small": null,
              "medium": null,
              "large": null,
              "xl": "http://backend.lndo.site/sites/default/files/styles/xl/public/2020-09/engin-akyurt-girl-blond-hair-unsplash.jpg?itok=MPph1lH4",
              "xxl": null
            },
            {
              "small": null,
              "medium": null,
              "large": null,
              "xl": null,
              "xxl": "http://backend.lndo.site/sites/default/files/styles/xxl/public/2020-09/engin-akyurt-girl-blond-hair-unsplash.jpg?itok=sz-ymDam"
            }
          ]
        }
      },

We get an object for each image style that contains keys for all the image styles queried, with 'null' values, except for the style corresponding to that part of the query. I’m not sure why this is, but we have to process our results differently to build the 'srcSet'.  Let’s illustrate with a Picture component so we don’t have to change the Image component.


import React from 'react';

const Picture = ({image, alt, sizes}) => {

  const derivatives = [];
  let src = "";

  image.forEach(derivative => {
    if (derivative.small) {
      src = derivative.small
      derivatives.push(`${derivative.small} 300w`)
    }
    if (derivative.medium) {
      derivatives.push(`${derivative.medium} 600w`)
    }
    if (derivative.large) {
      derivatives.push(`${derivative.large} 1200w`)
    }
    if (derivative.xl) {
      derivatives.push(`${derivative.xl} 1800w`)
    }
    if (derivative.xxl) {
      derivatives.push(`${derivative.xxl} 2400w`)
    }
  })

  const srcSet = derivatives.join();

  return (
    <picture>
      <source srcSet={srcSet} sizes={sizes}/>
      <img src={src}
        alt={alt}
        />
    </picture>
  );
};

export default Picture;

Notice that we also have an 'alt' prop because the 'alt' data comes from a different part of the query. It can’t be accessed from 'relationships.field_image' like the image styles are. 

Conclusion

Whichever API type you use on the backend, I hope I’ve shown that we can still leverage the power of Drupal to process images as they’re needed instead of having to download and process them during the Gatsby build, bloating the build time and the size of the build artifact. 

Combine this with the 'image_effects' module in Drupal and create image styles to meet many different needs. You can have your fast Gatsby build times and responsive images too!
 

"Code like nobody is watching... but then refactor. ;)"