Rewriting A Static Website Using Gatsby and GraphQL - Part 3

Laurie - Mar 5 '19 - - Dev Community

Originally posted on Ten Mile Square's blog.

If you’ve been following this series of posts as I rebuild my personal site using GatsbyJS and GraphQL, continue on. If not, I recommend reading back on parts one and two. At this point I have migrated all of my data from static Yaml files, queried the data using GraphQL and rendered the pages using Gatsby and JXS. I've removed all references to the Liquid templating language I was using in my Jekyll code and the site is in working condition. Now I will turn my attention to images.

Image Processing

My site actually uses a lot of images in what is an otherwise clean design. I have an image included in most of the headers I use, I have an image in my bio and I include images from each of my speaking engagements. So where to start?

Let's begin with the picture for my bio. It's a one-off image in the body of my landing page and looks like this.

There is a straightforward way to handle this image. I can import the image file and reference it directly in my JSX code for the home page. Something like this:

import headshot from '../assets/headers/headshot.jpg'

<img className="headshot" src={headshot}/>
Enter fullscreen mode Exit fullscreen mode

The headshot class handles the nice circular display of the image as well as its center alignment on the page. It looks great! However, it isn't optimized. Since optimization is one of the major benefits of using Gatsby let's look at how to do that. In the process, I'll tackle a slightly more complicated use case.

Gatsby-Image

Image optimization in Gatsby is provided by a plugin called `gatsby-image` which is incredibly performant. In order to make use of it I'll start by using npm to install that plugin and its associated dependencies.

npm install gatsby-image gatsby-transformer-sharp gatsby-plugin-sharp
Enter fullscreen mode Exit fullscreen mode

Once that's done, I want to add the newly installed plugins to my gatsby-config.js file. Our config file ends up looking like this (other plugins we're already using have been removed from this snippet for simplicity). Note that once `gatsby-image` has been installed it does not need to be included in the gatsby-config.js file.

plugins:[
    `gatsby-transformer-sharp`,
    `gatsby-plugin-sharp`
]
Enter fullscreen mode Exit fullscreen mode

Images in Yaml

Now we're set up to tackle the more complicated use case, my speaking page. In my Jekyll site implementation each of my speaking engagements had an associated image, like this.

The image files were all stored in the folder labeled speaking. The yaml file that defined the data for my speaking page had references to the file name of each image. That way, when I looped through each speaking engagement, the file name would be prepended with the path to the speaking folder and the page would render the image.

So how do I do this in Gatsby? I'm going to use GraphQL to query the image. Right now, the image file names are referenced along with the data for each speaking engagement. As a result, getting this to work requires querying the image correctly and making sure the referenced data is properly coupled with a path so the file itself can be found and processed.

I'm actually going to start by addressing the second issue first. To be honest, figuring this out was a weirdly finicky process. It turns out to be a combination of a bunch of different things but I'll try to walk through it with the solution I landed on.

Remember from the very first blogpost on this topic that the scope of what Gatsby can see is defined by the `gatsby-source-filesystem` plugin. In my case, it's defined to expose src/data. So I'll start by placing my speaking folder, filled with all the images for my speaking engagements, within that scope.

From there, I need to make sure that the file names defined in speaking.yaml are matched with the appropriate path so that GraphQL can find the image files. To make this work, I actually changed the data in my yaml file slightly. Instead of just referencing the file name, I put a relative path. The path to the image is relative to the location of the speaking.yaml file (NOT the filesource path defined, this one tripped me up).

image: speaking/kcdc.jpg
Enter fullscreen mode Exit fullscreen mode

Now, I can turn my attention to GraphQL. Right now, image is just a string. I can query it like this. ```graphql { allSpeakingYaml (sort: {fields: [index], order: DESC}) { edges { node { conference year url date image } } } } ```

However, the above doesn't do what I want. It returns a string of the relative path, e.g. "speaking/kcdc.jpg". However, I really like that I can query image as part of the speaking data itself. I'd like to keep that behavior. It turns out, I can.

I can use gatsby-image features inside the query. When the query runs, the relative path will point to the location of the image file and the resulting query processes the file as an image for display.

{
    allSpeakingYaml (sort: {fields: [index], order: DESC}) {
        edges {
            node {
                conference
                year
                url
                date
                image {
                    childImageSharp {
                        fluid {
                            ...GatsbyImageSharpFluid
                        }
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, when I loop through my speaking data with a JSX map, there is an image in each of those objects instead of a string. So I want to use JSX to access those images. As it turns out `gatsby-image` has its own tag that I can use, so I'll import that.

import Img from "gatsby-image";
Enter fullscreen mode Exit fullscreen mode

My first instinct is to write something like this.

<Img className="selfie" fluid={node.image} alt={node.conference}/>
Enter fullscreen mode Exit fullscreen mode

Unfortunately, that doesn't work. The page rendered with an icon where the image should be. For some reason this took me more than a minute to crack, but the answer is relatively simple.

In many of our GraphQL queries the structure of the query is based on the structure of our yaml data. So the return object structure looks roughly the same as the yaml file. We saw an exception to that rule when we added the node and edge objects to access the first level of the yaml results. This is the same thing, I just didn’t notice it. The actual processed image is at the ...GatsbyImageSharpFluid level. What I was accessing with node.image was not the processed image. So the resulting successful code is

<Img className="selfie" fluid={node.image.childImageSharp.fluid}
alt={node.conference}/>
Enter fullscreen mode Exit fullscreen mode

Single Image Query

Now I want to go back and optimize the “easy” use case. The first thing to do is to remove the import of the file and set it up as a GraphQL query that runs through gatsby-image processing. This will look a lot like what I did for the series of speaking images.

export const query = graphql`
  query {
   <strong> file(relativePath: { eq: "headers/headshot.jpg" }) {
      childImageSharp {
        <strong> fixed(width: 125, height: 125) {
          ...GatsbyImageSharpFixed
        }
      }
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

There are a couple of things to note here. Based on my previous code I would expect the relative path I need to be relative to the file the code sits in, in this case that's index.js. However, that doesn't work. The relative path is actually based on the line of code we put in the `gatsby-source-filesystem` config, which points to src/data. That actually took me a bit to recognize.

Another thing to note in the query is that we’re using GatsbyImageSharpFixed instead of fluid. To be honest, this should also be a fluid image and in my final site it will be. However, for the purposes of testing out all the features the image plugin offered, I wanted to try both. In `gatsby-image`, fluid images are meant for images that don’t have a finite size depending on the screen, where as other images are fixed.

After figuring out all of those little idiosyncrasies I can finally display this image using JSX. This is more or less the same as what I did to display my speaking data. The only difference is that I've chosen to process the image as fixed instead of fluid, so I need to reference it as such.

<Img className="headshot" fixed={data.file.childImageSharp.fixed}
alt="headshot"/>
Enter fullscreen mode Exit fullscreen mode

Aspect Ratio

This is a good time to go on a quick tangent. In the process of trying to style my images I noticed a surprising number of complexities. As it turns out, each image has some implicit styling that comes with the wrapper the processor puts around it. This was messing with all kinds of CSS I had attempted to use. This could be a whole other post, but I discovered one neat trick as part of my never ending googling for the answer. The plugin supports sizes where you can set an aspect ratio. This can be used for fixed or fluid processed images, it doesn't matter.

<Img sizes={{...data.banner.childImageSharp.fluid, aspectRatio: 21/9}}/>
Enter fullscreen mode Exit fullscreen mode

Static Query

The next thing I want to do is handle my header images. In my previous site I had a Header.js file that was included in my layout and rendered on all my pages. So I want to have the same reusable component here. I'll start by using the same code I used to render my headshot above. Well, that doesn't work. As it turns out, the reason for this is that there are restrictions to what GraphQL can do on non-page components.

The way to solve this is to use a Static Query. The first thing I need to do is change the structure of my Header.js component.

export default () => (
 <StaticQuery 
    query={graphql`
    query {
      file(relativePath: { eq: "headers/default.jpg" }) {
        childImageSharp {
          fixed(width: 125, height: 125) {
            ...GatsbyImageSharpFixed
          }
        }
      }
    }
  `}
    render={data => (
      <section id="header">
         <h2>LAURIE BARTH</h2>
         <Img fixed={data.file.childImageSharp.fixed} />
      </section>
    )}
  />
)
Enter fullscreen mode Exit fullscreen mode

Instead of a query constant and data that references the result, I have to use a static query directly in the JXS code and then reference it. Note that the query language didn’t change and neither did the Img tag syntax, the only change was the location of the query and the usage of the StaticQuery tag to wrap it.

Multiple Queries and Aliasing

The last use case I need to figure out is how to handle a situation where I have multiple queries in the same file/page. I may or may not need this in the final site, but it's a worthwhile exercise.

In this case I want to query for all my data in my speaking.yaml file AND I want to query for my headshot separately. The answer to this problem is to use aliasing, but I found most write ups on this topic explained the concept but missed some gotchas. The first thing to know is that an alias is assigning a name to a query. Below is a simple example.

talks: allSpeakingYaml(sort: {fields: [index], order: DESC}) {
        edges {
            node {
                conference
                year
                url
                date
                image {
                    childImageSharp {
                        fluid {
                            ...GatsbyImageSharpFluid
                        }
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

When you do that, you’ve changed the reference to that object in your JXS. While it was previously referenced as

{data.allSpeakingYaml.edges.map(({ node }) => ())
Enter fullscreen mode Exit fullscreen mode

giving it an alias does not add a level of complexity to the response object, it just replaces it. So you end up with the same structure referenced as

{data.talks.edges.map(({ node }) => ())
Enter fullscreen mode Exit fullscreen mode

The top-level object name of data is implicit. This is important because when I added multiple queries to this, I was still only passing in the data object

const SpeakingPage = ({ data}) => {}
Enter fullscreen mode Exit fullscreen mode

everything else was referenced from that top-level return name.

With that understanding, I can combine two queries and use aliasing to distinguish between them.

{
    allSpeakingYaml (sort: {fields: [index], order: DESC}) {
        edges {
            node {
                conference
                year
                url
                date
                location
                image {
                    childImageSharp {
                        fluid {
                            ...GatsbyImageSharpFluid
                        }
                    }
                }
                talks {
                    title 
                    video
                }
            }
        }
    }
    banner: file(relativePath: { eq: "headers/default.jpg" }) {
      childImageSharp {
        fluid {
          ...GatsbyImageSharpFluid
        }
      }
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice that I decided I didn’t need to alias the first query. This is allowed; there is no requirement that all your queries use aliasing. So I reference the speaking data array the same way I was before.

{data.allSpeakingYaml.edges.map(({ node }) => ())
Enter fullscreen mode Exit fullscreen mode

Then I access my image using my alias name, banner.

<Img fluid={data.banner.childImageSharp.fluid} />
Enter fullscreen mode Exit fullscreen mode

The End

So that's it. I've now optimized all my images. This post included a number of different possible use cases, so don't feel as if you need to explore them all. Pick the examples and tips that apply to your implementation.

In my case, my site should now pass a Lighthouse audit with a much higher grade and this blog series comes to a close. Hopefully these posts were helpful for those who ran into the same micro problems that I did. My next challenge is to remove the starter template I used and make a far more responsive design. So until next time!

Bonus Error

When I went back and changed my images from fixed to fluid I received an error.

Despite its appearance, solving this doesn't actually require flushing any kind of cache. In reality, it has to do with incompatible references. I triggered it because I had changed my query to process the image as fluid but the JSX key was still set to fixed.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .