3
votes

I've followed Gatsby tutorial for Working With Images in Markdown Posts and Pages which is working well but what I want to achieve is to fetch image from a static location instead of using a relative path for the image.

Would like to reference image like this (in frontmatter)

featuredImage: img/IMG_20190621_112048_2.jpg

Where IMG_20190621_112048_2.jpg is in /src/data/img instead of same directory as markdown file under /src/posts

I've tried to setup gatsby-source-filesystem like this :

{
  resolve: `gatsby-source-filesystem`,
  options: {
    name: `posts`,
    path: `${__dirname}/src/posts`,
  },
},
{
  resolve: `gatsby-source-filesystem`,
  options: {
    name: `data`,
    path: `${__dirname}/src/data/`,
  },
},

but graphQL query in post template fails :

export const query = graphql`
  query($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      frontmatter {
        title
        featuredImage {
          childImageSharp {
            fluid(maxWidth: 800) {
              ...GatsbyImageSharpFluid
            }
          }
        }
      }
    }
  }

GraphQL Error Field "featuredImage" must not have a selection since type "String" has no subfields.

Any idea how I could fetch image from a location distinct to the post markdown directory ?

3

3 Answers

3
votes

Achieving this in Gatsby used to be pretty troublesome, but thanks to the new createSchemaCustomization Node API docs (since Gatsby 2.5) it's relatively easy.

Here's a demo where I replicate your repo structure: github

Here's where the relevant code lives: github

Here's the code to make it work:

// gatsby-node.js

const path = require('path')

exports.createSchemaCustomization = ({ actions }) => {
  const { createFieldExtension, createTypes } = actions

  createFieldExtension({
    name: 'fileByDataPath',
    extend: () => ({
      resolve: function (src, args, context, info) {
        const partialPath = src.featureImage
          if (!partialPath) {
            return null
          }

        const filePath = path.join(__dirname, 'src/data', partialPath)
        const fileNode = context.nodeModel.runQuery({
          firstOnly: true,
          type: 'File',
          query: {
            filter: {
              absolutePath: {
                eq: filePath
              }
            }
          }
        })

        if (!fileNode) {
          return null
        }

        return fileNode
      }
    })
  })

  const typeDefs = `
    type Frontmatter @infer {
      featureImage: File @fileByDataPath
    }

    type MarkdownRemark implements Node @infer {
      frontmatter: Frontmatter
    }
  `

  createTypes(typeDefs)
}

How it works:

There are 2 parts to this:

  1. Extend markdownRemark.frontmatter.featureImage so graphql resolves to a File node instead of a string via createTypes
  2. Create a new field extension @fileByDataPath via createFieldExtension

createTypes

Right now Gatsby's inferring frontmatter.featureImage as a string. We'll ask Gatsby to read featureImage as a string instead, by modifying its parent type:

  type Frontmatter {
    featureImage: File
  }

This is not enough however, we'll also need to pass this Frontmatter type to its parent as well:

  type Frontmatter {
    featureImage: File
  }

  type MarkdownRemark implements Node {
    frontmatter: Frontmatter
  }

We'll also add the @infer tag, which lets Gatsby know that it can infer other fields of these types, i.e frontmatter.title, markdownRemark.html, etc.

Then pass these custom type to createTypes:

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions

  const typeDefs = `
    type Frontmatter @infer {
      featureImage: File
    }

    type MarkdownRemark implements Node @infer {
      frontmatter: Frontmatter
    }
  `

  createTypes(typeDefs)
}

Now, we can fire up localhost:8000/___graphql and try to query the image

query Post {
  markdownRemark {
    frontmatter {
      featureImage {
        id
      }
    }
  }
}

and we get...

Error: Cannot return null for non-nullable field File.id.

That is because while Gatsby now understands featureImage should be a File node, it has no idea where to get that file.

At this point, we can either use createResolvers to manually resolve the field to a File node, or createFileExtension to do the same thing. I choose createFileExtension because it allows more code reuse (you can extend any fields), while createResolvers, in this case, is more useful for a specific field. Seeing that all you want is to resolve a file from the src/data directory, I'll call this extension fieldByDataPath.

createFileExtension

Let's just look at the resolve attribute. It is a function that takes in the following:

  • source: The data of the parent field (in this case, frontmatter)
  • args: The arguments passed to featureImage in a query. We won't need this
  • context: contains nodeModel, which we'll use to get nodes from Gatsby node store
  • info: metadata about this field + the whole schema

We will find the original path (img/photo.jpg) from src.featureImage, then glue it to src/data to get a complete absolute path. Next, we query the nodeModel to find a File node with the matching absolute path. Since you have already pointed gatsby-source-filesystem to src/data, the image (photo.jpg) will be in Gatsby node store.

In case we can't find a path or a matching node, return null.

  resolve: async function (src, args, context) {
    // look up original string, i.e img/photo.jpg
    const partialPath = src.featureImage
      if (!partialPath) {
        return null
      }

    // get the absolute path of the image file in the filesystem
    const filePath = path.join(__dirname, 'src/data', partialPath)
    
    // look for a node with matching path
    const fileNode = await context.nodeModel.runQuery({
      firstOnly: true,
      type: 'File',
      query: {
        filter: {
          absolutePath: {
            eq: filePath
          }
        }
      }
    })

    // no node? return
    if (!fileNode) {
      return null
    }

    // else return the node
    return fileNode
  }

We've done 99% of the work. The last thing to do is to move this to pass this resolve function to createFieldExtension; as well as add the new extension to createTypes

createFieldExtension({
  name: 'fileByDataPath' // we'll use it in createTypes as `@fileByDataPath`
  extend: () => ({
    resolve,             // the resolve function above
  })
})

const typeDef = `
  type Frontmatter @infer {
    featureImage: File @fileByDataPath // <---
  }
  ...
`

With that, you can now use relative path from src/data/ in frontmatter.

Extra

The way fileByDataPath implemented, it'll only work with fields named featureImage. That's not too useful, so we should modify it so that it'll work on any field that, say, whose name ended in _data; or at the very least accept a list of field names to work on.

Edit had a bit of time on my hand, so I wrote a plugin that does this & also wrote a blog on it.

Edit 2 Gatsby has since made runQuery asynchronous (Jul 2020), updated the answer to reflect this.

2
votes

In addition to Derek Answer which allow assets of any type to be use anywhere (sound, video, gpx, ...), if looking for a solution only for images, one can use :

https://www.gatsbyjs.org/packages/gatsby-remark-relative-images/

-1
votes

The reason in your server schema you may have declared the featuredImage variable as string and in your client graphql query you are trying to call subobjects of the featuredImage variable and that subobjects is not existing.

You may have to check the graphql schema definition and align the query with the schema definition

you current schema might be like this

featuredImage: String

and you need to change it by declaring the proper types based on the requirements in the server side.

For more information about graphql types. please refer this url - https://graphql.org/learn/schema/#object-types-and-fields

Thanks

Rigin Oommen