It's highly annoying when things don't work as they're supposed to. It increases dev time, increases cost, and increases your own frustration.
One of the most common and persistent issues with Gatsby and its plugin ecosystem is also its main selling point - the easy inclusion of highly optimised images that can be created from any type of CMS.
You'll most commonly find this problem if you're working with an external GraphQL API that doesn't have its own Gatsby-maintained sourcing plugin, or if you're creating your own sourcing plugin for something like a REST API. You'll also find this problem with Strapi, if you're using gatsby-source-graphql(because you need dynamic zones support).
Approach 1: Using createResolvers and an unsupported GraphQL API/using Strapi
If you're using Strapi, or any other external GQL API with the gatsby-source-graphql plugin, the good news is there's a relatively simple fix to get your locally-sourced images up and running.
Here's a breakdown of what we need to do:
- Find out the name of the Type that our external GraphQL API uses for storing images/files generally
- Find the field that the external GraphQL API provides to the remote image
- Create a resolver in
gatsby-node.jsto resolve our remote images into local ones. - Make sure we always get the field from Step 2 on every image query we write in Gatsby.
Demo: with Strapi as the External GraphQL API
We're going to use Strapi as our demo point because their Gatsby plugin sucks, and your most common use case for this sort of sourcing will be unfortunately with Strapi. Fortunately, it's really easy to fix this issue.
In the createResolvers export on gatsby-node.js we're going to run await createResolvers(). We are going to specify the typename that Strapi gives files as the only key on the first level of the object we pass it (this is called UploadFile for Strapi, and then we are going to add localFile as our second level key, so that our returned image is available via localFile (as per other Gatsby plugin specs, to ensure consistency. We're then going to run the resolver, and inside that resolver, we're going to resolve the field on the type that has our remote URL (in Strapi's case, the url field), and then run createRemoteFileNode, which is an export of gatsby-source-filesystem. Once the remote file node is created, Gatsby's sharp processing will handle everything else without you touching a thing (as long as they're in the gatsby-config).
Here's how this all fits together inside gatsby-node.js:
exports.createResolvers = async ({
actions,
cache,
createNodeId,
createResolvers,
store,
reporter
}) => {
const { createNode } = actions
await createResolvers({
strapi_UploadFile: { // Run this resolver on all the UploadFile types
localFile: { // Call our new field localFile
type: 'File',
async resolve(source, args, context, info) {
// url field on UploadFile contains
// the remote location for Strapi's file.
if(source.url){
return await createRemoteFileNode({
url: source.url, // This is our URL, and this is how we tell createRemoteFileNode to download it
store,
cache,
createNode,
createNodeId,
reporter
})
}
}
}
}
})
}
With this, we are now almost able to resolve UploadFiles in Strapi. Next, we need to make sure that we query the url field everywhere that we run a Gatsby query/static query that has an image.
But why? With gatsby-source-graphql and quite a few other Gatsby plugins, Gatsby dynamically infers the schema. Which means that if you don't query for something, or there's a field that exists in the schema but is never used in the data on the CMS, then that field according to Gatsby does not exist.
Luckily, in this situation, it's an easy solve. All we have to do is query for url wherever we have an image that we want to use with gatsby-image/gatsby-plugin-image.
This means every image query you run should look something like this (in this example, thumbnail is our base field name:
thumbnail {
alternativeText
localFile {
childImageSharp {
gatsbyImageData
}
publicURL
}
url // This field represents our Remote Image File. Without querying this, createResolvers doesn't know the field exists, and can't access it to download the file.
}
Approach 2: Using onCreateNode to convert an external image URL into a type
Sometimes (particularly if building your own sourcing plugins!) you'll find that a Gatsby Node has a singular field for a Remote Image URL, and not a separate linked type. Instead of using createResolvers in this instance, you'll use onCreateNode and Gatsby's largely-undocumented ___NODE syntax to either add an additional field to the Node representing the local file, or replace the field with the local file.
Here's how we might do this:
exports.onCreateNode = async ({node, actions: { createNode }, createNodeId, getCache}) => {
if (node.internal.type === 'Post') { // Our field with the remote image only exists on the Post node type in this example
const fileNode = await createRemoteFileNode({
// the url of the remote image to generate a node for
url: node.remoteImage // Our remoteImage field on the node contains our remote image as a URL (String)
parentNodeId: node.id,
createNode,
createNodeId,
getCache,
})
if (fileNode) {
// Here we can either use this ___NODE syntax to replace the field with the image, while retaining the same field name
node.remoteImage___NODE = fileNode.id
// Or we can create a new field on the node, leaving remoteImage intact
node.localImage___NODE = fileNode.id
}
}
}
Conclusion
With both of these approaches (and any others you might happen to find!) the key is persistence. Working with unsupported external image sources is never easy, and you'll likely try out lots of things that won't work. But hang in there - you'll get it working eventually!