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?
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.