Add ImageSharp as a field to MarkdownRemark nodes

2020-03-27 04:32发布

问题:

I have the following graphQL query I'm trying to get working:

 {
      allMarkdownRemark(
        limit: 1000
      ) {
        edges {
          node {
            id
            parent {
              id
            }
            fields{
              slug
              hero {
                childImageSharp {
                  fixed {
                    src
                  }
                }
              }
            }
            frontmatter {
              template
            }
          }
        }
      }
    }

The hero field currently returns a path to an image using the following code:

exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions

  // Add slug to MarkdownRemark node
  if (node.internal.type === 'MarkdownRemark') {
    const value = createFilePath({ node, getNode, basePath: 'library' })
    const { dir } = getNode(node.parent)
    const getHero = (d) => {
      let hero = `${__dirname}/src/images/no-hero.gif`
      if (fs.existsSync(`${d}/hero.jpg`)) hero = `${d}/hero.jpg`
      if (fs.existsSync(`${d}/hero.png`)) hero = `${d}/hero.png`
      if (fs.existsSync(`${d}/hero.gif`)) hero = `${d}/hero.gif`
      return hero
    }
    createNodeField({
      node,
      name: 'slug',
      value,
    })

    createNodeField({
      node,
      name: 'hero',
      value: getHero(dir),
    })
  }
}

I've seen other people do something similar with an image path in the frontmatter but I don't want to have to use the frontmatter when it's easy enough to get graphql to see the file path without having to specify it.

However when I try the above I get the following error:

Field \"hero\" must not have a selection since type \"String\" has no subfields.

Is there a way I can get childImageSharp to recognize this field?

回答1:

I'm back again to (hopefully) settle this issue once and for all (see our history here).

This time, we'll attach the hero image's ImageSharp to the MarkdownRemark node. Your approach is correct, with 1 caveat: Gatsby seems to only recognize relative paths, i.e path starting with a dot.

You can fix this easily in your code:

    const getHero = (d) => {
      let hero = `${__dirname}/src/images/no-hero.gif`

  -   if (fs.existsSync(`${d}/hero.jpg`)) hero = `${d}/hero.jpg`
  -   if (fs.existsSync(`${d}/hero.png`)) hero = `${d}/hero.png`
  -   if (fs.existsSync(`${d}/hero.gif`)) hero = `${d}/hero.gif`

  +   if (fs.existsSync(`${d}/hero.jpg`)) hero = `./hero.jpg`
  +   if (fs.existsSync(`${d}/hero.png`)) hero = `./hero.png`
  +   if (fs.existsSync(`${d}/hero.gif`)) hero = `./hero.gif`

      return hero
    }
    createNodeField({
      node,
      name: 'hero',
      value: getHero(dir),
    })

This should work, though I want to provide an alternative hero search function. We can get a list of files in dir with fs.readdir, then find a file with the name 'hero':

exports.onCreateNode = async ({
  node, actions,
}) => {
  const { createNodeField } = actions

  if (node.internal.type === 'MarkdownRemark') {
    const { dir } = path.parse(node.fileAbsolutePath)

    const heroImage = await new Promise((res, rej) => {

      // get a list of files in `dir`
      fs.readdir(dir, (err, files) => {
        if (err) rej(err)

        // if there's a file named `hero`, return it
        res(files.find(file => file.includes('hero')))
      })
    })

    // path.relative will return a (surprise!) a relative path from arg 1 to arg 2.
    // you can use this to set up your default hero
    const heroPath = heroImage 
      ? `./${heroImage}` 
      : path.relative(dir, 'src/images/default-hero.jpg')

    // create a node with relative path
    createNodeField({
      node,
      name: 'hero',
      value: `./${heroImage}`,
    })
  }
}

This way we don't care what the hero image's extension is, as long as it exists. I use String.prototype.includes, but you might want to use regex to pass in a list of allowed extensions, to be safe, like /hero.(png|jpg|gif|svg)/. (I think your solution is more readable, but I prefer to access the file system only once per node.)

You can also use path.relative to find the relative path to a default hero image.

Now, this graphql query works:


A (Minor) Problem

However, there's a minor problem with this approach: it breaks graphql filter type! When I try to query and filter based on hero, I get this error:

Perhaps Gatsby forgot to re-infer the type of hero, so instead of being a File, it is still a String. This is annoying if you need the filter to work.

Here's a workaround: Instead of asking Gatsby to link the file, we'll do it ourselves.

exports.onCreateNode = async ({
  node, actions, getNode, getNodesByType,
}) => {
  const { createNodeField } = actions

  // Add slug to MarkdownRemark node
  if (node.internal.type === 'MarkdownRemark') {
    const { dir } = path.parse(node.fileAbsolutePath)
    const heroImage = await new Promise((res, rej) => {
      fs.readdir(dir, (err, files) => {
        if (err) rej(err)
        res(files.find(file => file.includes('hero')))
      })
    })

    // substitute with a default image if there's no hero image
    const heroPath = heroImage ? path.join(dir, heroImage) : path.resolve(__dirname, 'src/images/default-hero.jpg')

    // get all file nodes
    const fileNodes = getNodesByType('File')

    // find the hero image's node
    const heroNode = fileNodes.find(fileNode => fileNode.absolutePath === heroPath)
    createNodeField({
      node,
      name: 'hero___NODE',
      value: heroNode.id,
    })
  }
}

And now we can filter the hero field again:

If you don't need to filter content by hero image though, letting gatsby handle node type is much preferable.

Let me know if you run into issues trying this.