Modify Gatsby's GraphQL data types using createSchemaCustomization
Hi friends, yesterday I published a little post about how to Add data to Gatsby’s GraphQL layer using sourceNodes, this post will be expanding on the topic of data management but this time i’m going to hone in on how to modify GraphQL’s inferred data types so that you can use the new upgraded gatsby-plugin-image with remotely sourced images.
If you’d prefer to jump ahead here’s a demo repo: https://github.com/PaulieScanlon/nasa-image-type
… and a live demo can be seen here: https://nasaimagetype.gatsbyjs.io/
The Problem
The “problem” with remotely sourced images is that they are usually returned by API’s as url’s, E.g
http://example.com/images/some-image.jpg
When GraphQL see’s this it correctly infers the data type as a String
. However, in order for
gatsby-plugin-image to process the image there are two
requirements.
- The GraphQL node must be of type
File
- The image needs to have been downloaded and exist on your local filesystem
In this post i’ll explain how you can satisfy both of these requirements using a combination of createRemoteFileNode from gatsby-source-filesystem and createSchemaCustomization which is utility function available in the Gatsby Node API
The code i’ll be explaining below expands on yesterday’s post: Add data to Gatsby’s GraphQL layer using sourceNodes so i’d advise you have a read of that before diving in.
Pre-Flight Checks
You’ll need to have all the following dependencies installed and configured in gatsby-config.js
The docs can be found
here: gatsby-plugin-image
yarn add gatsby-plugin-image gatsby-plugin-sharp gatsby-source-filesystem gatsby-transformer-sharp # npm install gatsby-plugin-image gatsby-plugin-sharp gatsby-source-filesystem gatsby-transformer-sharp --save
To use the approach I’ll be using below you’ll also need to have
gatsby-source-filesystem installed, but
don’t worry about adding it to gatsby-config.js
yarn add gatsby-source-filesystem # npm install gatsby-source-filesystem --save
If you don’t already have one, you’ll need a gatsby-config.js
at the root of you project:
...
src
gatsby-config.js
package.json
And finally you’ll need to add the following to gatsby-config.js
// gatsby-config.js
module.exports = {
plugins: [
`gatsby-plugin-image`,
`gatsby-transformer-sharp`,
{
resolve: `gatsby-plugin-sharp`,
options: {
defaults: {
quality: 70,
formats: ['auto', 'webp', 'avif'],
placeholder: 'blurred',
},
},
},
],
};
I prefer to set the defaults for gatsby-plugin-sharp
in my gatsby-config.js
, this is optional, but i’d advise it.
1. Source The Image
The following code can all be written in gatsby-node.js
I use onCreateNode which is a Gatsby
function called each time a new node is created. By using an if
condition I’m able to only call createRemoteFileNode
if the node.internal.type
equals apod
, which is the new node I created in yesterday’s post.
// gatsby-node.js
const { createRemoteFileNode } = require('gatsby-source-filesystem');
exports.onCreateNode = async ({ node, actions: { createNode }, createNodeId, cache, store }) => {
if (node.internal.type === 'apod') {
node.image = await createRemoteFileNode({
url: node.url,
parentNodeId: node.id,
createNode,
createNodeId,
cache,
store,
});
}
};
Starting from the top I destructure createRemoteFileNode
from gatsby-source-filesystem
, more on that in a moment.
Next I define and export onCreateNode
. onCreateNode can be an async
function and accepts a number of parameters
including but not limited to the following.
- node
- actions
- createNodeId,
- cache
- store
node
This is the new node type sourced from the NASA API and already exists in Gatsby’s data layer
actions
Actions are the equivalent to actions bound with bindActionCreators in
Redux. actions
contains a function called
createNode and this is how you add data to
the Redux state object / Gatsby’s data layer
createNodeId
This is effectively a helper function that aids in the creation of unique id’s. Under the hood Gatsby are using uuid, you can of course use your preferred method but since uuid is already part of the Gatsby bundle it makes sense to use it.
cache
Cache is the .cache
directory Gatsby creates in/on your local filesystem
store
This is Gatsby’s Data layer / The Redux state object
To the best of my knowledge all of the above are required. You might not see errors if you don’t include cache
or
store
when creating a remote file node but I have experienced odd behavior if I failed to included them.
The next bit deals with sourcing the image using the image url returned by the NASA API.
node.image
I create a new object on the node and call it image
which will be the response from createRemoteFileNode
.
createRemoteFileNode | params
This function comes from gatsby-source-filesystem and accepts the following parameters
- url
- parentNodeId
- createNode
- createNodeId
- cache
- store
url
The source url of the remote file
parentNodeId
The id of the parent node (i.e. the node to which the new remote File node will be linked to.
createNode
The action used to create nodes, I covered this in more detail in yesterday’s post #actions-createNode
createNodeId
A helper function for creating node ids, I covered this in more detail in yesterday’s post #createNodeId
cache
As above
store
As Above
With all of the above in place you should now be able to query the new image
node in the GraphiQL explorer. Visit
http://localhost:8000/___graphql to investigate.
{
apod {
url
image {
relativePath
}
}
}
Which should give you a response similar to the below
{
"data": {
"apod": {
"url": "https://apod.nasa.gov/apod/image/2107/AR2835_20210701_W2x1024.jpg",
"image": {
"relativePath": ".cache/caches/default-site-plugin/bcd18c3c0f372d1ad0d180fa82cde702/AR2835_20210701_W2x1024.jpg"
}
}
},
}
You can see the new image
node and by querying the relativePath
you can see that the file exists on disc in the
.cache/caches
directory. Compare this to the url
which has remained as it was, a remote url.
This satisfies one of the two requirements I mentioned above, but GraphQL still thinks the data type is a
String
… but we know it’s now actually a File
2. Modify the GraphQL type
To see GraphQL’s inferred types Gatsby have exposed an additional action called printTypeDefinitions which can be called from Gatsby Node using this function: createSchemaCustomization
// gatsby-node.js
exports.createSchemaCustomization = ({ actions: { createTypes, printTypeDefinitions } }) => {
printTypeDefinitions({ path: './typeDefs.txt' });
};
If you’ve added the above to gatsby-node.js
you can now run gatsby build
and you should see a file pop up in your
filesystem called typeDefs.txt
. Open it and scroll to the bottom, i’ve removed quite a lot from the below snippet for
brevity, but the main thing to notice is that GraphQL has inferred that the new image
node has a child node called
url
and is typed as a String
👎
type apod implements Node @derivedTypes @dontInfer {
...
title: String
url: String
image: apodImage
}
type apodImage @derivedTypes {
...
url: String
}
To correct this you can manually override GraphQL’s type inference and provide your own type definitions. You can do
this by using createTypes
from actions
exports.createSchemaCustomization = ({
actions: { createTypes, printTypeDefinitions }
}) => {
+ createTypes(`
+ type apod implements Node {
+ image: apodImage
+ }
+ type apodImage @dontInfer {
+ url: File @link(by: "url")
+ }
+ `);
printTypeDefinitions({ path: './typeDefs.txt' });
};
This looks a little peculiar if you’re new to GraphQL and being honest I found this really difficult so here’s my best attempt to explain what’s going on.
type apodImage
apodImage
first needs to be set to @dontInfer
. This is a way to tell GraphQL that I know best and i’ll handle the
types so don’t worry about inferring the data type.
url
Finally it’s here where I tell GraphQL that the image.url
is of type File
and I link it to the url
defined by the
url
parameter from createRemoteFileNode
🥵
If you delete the typeDefs.txt
file from your local filesystem and run gatsby build
again and investigate the types
you should now see the following.
type apod implements Node @dontInfer {
...
title: String
url: String
image: apodImage
}
type apodImage {
url: File @link(by: "url")
}
And now GraphQL correctly understands that image.url
is of type File
— Hooray! 🎉
This now satisfies both of the above mentioned requirements!
If you see any weird looking errors in your terminal it might be best to run gatsby clean
before running
gatsby build
since we’re messing with a few low level things
Using GatsbyImage
gatsby-plugin-image
exports two components, <StaticImage />
and <GatsbyImage />
I won’t explain why we need to use
<GatsbyImage />
but there’s a good explanation in the docs:
Using the Gatsby Image components
With the type now set as File
you can now query the image.url
using childImageSharp.gatsbyImageData
. The query
i’ve used in index.js looks a little
something like this
{
apod {
id
date
explanation
media_type
service_version
title
url
image {
url {
childImageSharp {
gatsbyImageData
}
}
}
}
}
Which should return something similar to the below. You should be able to see the various image data objects,
placeholder
, images
, and sources
. All of this can be passed on to the <GatsbyImage />
component.
{
"data": {
"apod": {
"id": "63a09eef-2a28-5632-94c6-50061b62a0bf",
"date": "2021-07-02",
"explanation": "Awash in a sea of incandescent plasma and anchored in strong magnetic fields, sunspots are planet-sized dark islands in the solar photosphere, the bright surface of the Sun. Found in solar active regions, sunspots look dark only because they are slightly cooler though, with temperatures of about 4,000 kelvins compared to 6,000 kelvins for the surrounding solar surface. These sunspots lie in active region AR2835. The largest active region now crossing the Sun, AR2835 is captured in this sharp telescopic close-up from July 1 in a field of view that spans about 150,000 kilometers or over ten Earth diameters. With powerful magnetic fields, solar active regions are often responsible for solar flares and coronal mass ejections, storms which affect space weather near planet Earth.",
"media_type": "image",
"service_version": "v1",
"title": "AR2835: Islands in the Photosphere",
"url": "https://apod.nasa.gov/apod/image/2107/AR2835_20210701_W2x1024.jpg",
"image": {
"url": {
"childImageSharp": {
"gatsbyImageData": {
"layout": "constrained",
"placeholder": {
"fallback": "data:image/jpeg;base64,/9j/2wBDAAoHBwgHBgoICAgLCgoLDhgQDg0NDh0VFhEYIx8lJCIfIiEmKzcvJik0KSEiMEExNDk7Pj4+JS5ESUM8SDc9Pjv/2wBDAQoLCw4NDhwQEBw7KCIoOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozv/wgARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAECAwQF/8QAFwEAAwEAAAAAAAAAAAAAAAAAAAEEBv/aAAwDAQACEAMQAAAB6lTeeukZwf8A/8QAGRAAAgMBAAAAAAAAAAAAAAAAAQIAAxAh/9oACAEBAAEFAhGuRbIGwt3/xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAwEBPwFH/8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BR//EABkQAAIDAQAAAAAAAAAAAAAAAAEQABExA//aAAgBAQAGPwLIOZ0u6X//xAAbEAADAAIDAAAAAAAAAAAAAAAAAREhUTFBgf/aAAgBAQABPyGF6iKAdY+RuhtN4LsbR//aAAwDAQACAAMAAAAQ+x//xAAWEQADAAAAAAAAAAAAAAAAAAAQESH/2gAIAQMBAT8QVD//xAAXEQADAQAAAAAAAAAAAAAAAAAAAREx/9oACAECAQE/EFFqKP/EABwQAAMAAgMBAAAAAAAAAAAAAAABETFRIUFh0f/aAAgBAQABPxBkpNlZF374QMUmWzJIaX0LDw6ciSJh7P/Z"
},
"images": {
"fallback": {
"src": "/static/8574311e0c9d3b7520b2714c8baa995e/862d2/AR2835_20210701_W2x1024.jpg",
"srcSet": "/static/8574311e0c9d3b7520b2714c8baa995e/ac769/AR2835_20210701_W2x1024.jpg 256w,\n/static/8574311e0c9d3b7520b2714c8baa995e/0e233/AR2835_20210701_W2x1024.jpg 512w,\n/static/8574311e0c9d3b7520b2714c8baa995e/862d2/AR2835_20210701_W2x1024.jpg 1024w",
"sizes": "(min-width: 1024px) 1024px, 100vw"
},
"sources": [
{
"srcSet": "/static/8574311e0c9d3b7520b2714c8baa995e/c4e41/AR2835_20210701_W2x1024.avif 256w,\n/static/8574311e0c9d3b7520b2714c8baa995e/542bf/AR2835_20210701_W2x1024.avif 512w,\n/static/8574311e0c9d3b7520b2714c8baa995e/59a35/AR2835_20210701_W2x1024.avif 1024w",
"type": "image/avif",
"sizes": "(min-width: 1024px) 1024px, 100vw"
},
{
"srcSet": "/static/8574311e0c9d3b7520b2714c8baa995e/053d8/AR2835_20210701_W2x1024.webp 256w,\n/static/8574311e0c9d3b7520b2714c8baa995e/93623/AR2835_20210701_W2x1024.webp 512w,\n/static/8574311e0c9d3b7520b2714c8baa995e/41185/AR2835_20210701_W2x1024.webp 1024w",
"type": "image/webp",
"sizes": "(min-width: 1024px) 1024px, 100vw"
}
]
},
"width": 1024,
"height": 683
}
}
}
}
}
},
}
Jsx
To return the above image data you can use <GatsbyImage />
with the getImage
helper to pass the data on to
<GatsbyImage />
via theimage
prop
import React from 'react';
import { useStaticQuery, graphql } from 'gatsby';
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
const IndexPage = () => {
const {
apod: { id, date, explanation, media_type, service_version, title, image },
} = useStaticQuery(graphql`
query {
apod {
id
date
explanation
media_type
service_version
title
image {
url {
childImageSharp {
gatsbyImageData
}
}
}
}
}
`);
return (
<main>
<p>{date}</p>
<h1>{title}</h1>
<p>{explanation}</p>
<GatsbyImage alt={title} image={getImage(image.url)} /> // oh hai!
<p>{`id: ${id}`}</p>
<p>{`media_type: ${media_type}`}</p>
<p>{`service_version: ${service_version}`}</p>
</main>
);
};
export default IndexPage;
… and there you have it, modifying GraphQL’s data types for remotely sourced images! I’ve used this approach many times in various projects and covered it quite conclusively with Benedicte Raae on our pokey internet show Gatsby Deep Dives with Queen Raae and the Nattermobs Pirates
If you’re looking for a similar solution when working with remote images in Markdown or MDX I wrote a post that can be found on the Gatsby blog: MDX Embedded Images with the All-New Gatsby Image Plugin