Gatsby: Sharp with Images in JSON from NetlifyCMS

Created on 30 Mar 2018  路  24Comments  路  Source: gatsbyjs/gatsby

Description

I'm currently using Gatsby with Netlify CMS to manage my content in JSON files. This is working really well for the most part, but I'd like to be able to resize my images, as the CMS users are uploading multi-MB images as thumbnails.

My content has more data in it than this, but the part I'm interested in talking about here looks like this:

{
  "thumbnail": "/assets/thumbnail.jpg"
}

This is a reference to a file in /static/assets/thumbnail.jpg.

Steps to reproduce

It's not a bug so much as me not understanding how to either access plugins that'll do this for me, or build a local plugin to do this myself.

Right now I can query my JSON which has been transformed with gatsby-transformer-json and get access to the path.

Expected result

I'd like to be able to resize images at build time, then query the path(s) of these generated images on my JSON content.

Actual result

I can't seem to hook this up. I tried 2 different ways:

  • In gatsby-node.js, looking through any JSON nodes that get generated, checking if the value of a key looks like an image that Sharp would recognise, then creating a File node for it so the standard image processing plugins could see it. Gatsby didn't let me do this because I don't own the File node type.
  • Creating a local plugin based off of gatsby-remark-images which could do a similar thing, but with JSON. I created it in the /plugins directory then configured it like this:
`gatsby-plugin-sharp`,
{
    resolve: `gatsby-transformer-json`,
    options: {
        plugins: [
            {
                resolve: `gatsby-json-images`,
            },
        ],
    },
},

but it never seems to get called. I'm guessing the Remark transformer has something built into it that the JSON transformer doesn't for processing child nodes?

Environment

  • Gatsby version (npm list gatsby): [email protected]
  • gatsby-cli version (gatsby --version): Not installed (I use package.json commands)
  • Node.js version: v8.9.4
  • Operating System: OS X 10.13.3

File contents (if changed):

These files are quite large in my case, doing many unrelated things to this question, and I don't really need someone to debug the setup fully, I just want to collaborate with the community on a way to get this hooked up, and contribute any plugin I do build back to the community.

Question

Has anyone done this before, or does anyone know a good approach to getting this working with Gatsby?

Most helpful comment

@georgesboris and @josepjaume I didn't get permission for the whole thing, but I did get permission for this specific code:

In gatsby-node.js:

exports.onCreateNode = function onCreateNode({ node, boundActionCreators }) {
    // We only care about JSON content.
    if (node.internal.owner === 'gatsby-transformer-json') {
        recurseOnObjectProcessingImages(node, node, boundActionCreators);
    }
};

const recurseOnObjectProcessingImages = async (node, content, boundActionCreators) => {
    if (!content) return;
    const { createNodeField } = boundActionCreators;

    if (Array.isArray(content)) {
        for (const object of content) {
            recurseOnObjectProcessingImages(node, object, boundActionCreators);
        }
    } else {
        for (const key of Object.keys(content)) {
            const value = content[key];
            if (typeof value !== 'string') {
                recurseOnObjectProcessingImages(node, value, boundActionCreators);
            } else {
                // Find all values on the object which end in an extension we recognise, then create a
                // file node for them so that all the standard image processing stuff will kick up
                const extensions = new Set([`.jpeg`, `.jpg`, `.png`, `.webp`, `.tif`, `.tiff`]);

                const extension = path.extname(value).toLowerCase();
                if (extensions.has(extension)) {
                    // Ok, we're going to create a field for the image with a relative path
                    // so that the incompatibility between gatsby-transformer-sharp and
                    // NetlifyCMS is avoided.
                    const contentPath = path.join(__dirname, 'content', '<My directory here where the content lives>');
                    const imagePath = path.join(__dirname, 'static', value); // (This is my asset path)
                    const relative = path.relative(contentPath, imagePath);

                    const existingValue = node.fields && node.fields[`${key}_image`];

                    if (existingValue && typeof existingValue === 'string') {
                        createNodeField({
                            node,
                            name: `${key}_image`,
                            value: [existingValue, relative],
                        });
                    } else if (existingValue && Array.isArray(existingValue)) {
                        createNodeField({
                            node,
                            name: `${key}_image`,
                            value: [...existingValue, relative],
                        });
                    } else {
                        createNodeField({ node, name: `${key}_image`, value: relative });
                    }
                }
            }
        }
    }
};

And in creating that field, it causes the rest of the image processing stuff to kick up. I'm not sure if the arrays work properly. Recursive here was a lazy solve, and feel free to do it in a more optimal way, but that's what we're using in production (with some paths edited) and it's working well enough for us on our content.

All 24 comments

Hi there !
I think the way to do it is to query Sharp in your GraphQL query and then use gatsby-image to display it.
There is an example there : https://codebushi.com/gatsby-featured-images/
It's done with markdown frontmatter but I guess it should work the same with json.

I'm new to Gatsby though, and haven't implemented this yet, but that's how I figured it worked.

In gatsby-node.js, looking through any JSON nodes that get generated, checking if the value of a key looks like an image that Sharp would recognise, then creating a File node for it so the standard image processing plugins could see it. Gatsby didn't let me do this because I don't own the File node type.

You can create node in your site/plugin saying that gatsby-source-filesystem is owner of node - like this (second argument for createNode function with name of the plugin): https://github.com/gatsbyjs/gatsby/blob/d35eef0fd144406ace8d3df018d0718cf453a2f9/packages/gatsby-source-filesystem/src/create-remote-file-node.js#L109

Possibly we could export createLocalFileNode from gatsby-source-filesystem package - same as we do with createRemoteFileNode that would handle file node creation and you would just have to pass path to file for it - but this would need someone to make PR on gatsby-source-filesystem

Hi @zecakeh, I tried this way but it doesn't seem to work the same as the markdown plugin. I'd be happy to contribute to make it work this way but I couldn't figure out how to get my own custom plugin running when configured as in the issue. Keen for guidance if I can get it working this way though.

@pieh, the other thing that seemed off about that approach is I'm not sure how I actually create the query so that I can find the image then.

So say I have the JSON file in the issue. How do I write a query to get the image referenced in that particular file? It seems like I'd have to know all the filenames and hardcode them into the query, rather than being able to say, "Give me this JSON file and its images." if I create free-floating file nodes that don't have parents. And if I do create file nodes that have parents, I'm not adept enough at the data layer in Gatsby to understand how to query for them.

Am I missing something here?

@blargity you can link to other nodes by creating field with name like ${fieldname}___NODE and value being id of linked node

so you would:

  1. listen to" onCreateNode event in gatsby node
  2. check for node you are interested in (that json file)
  3. use createNodeField with name let's say thumbnailFile___NODE (so it doesn't conflict with thumbnail field) and value being:

    • id of created File node or

    • you could set up gatsby-source-filesystem to watch static directory and then you would need to find already create File node for that image and get its id

then you would query your json file and fields thumbnailFile would be File nodes from which you could use childImageSharp

Thanks @pieh, I really appreciate your help, and I think I'm being a bit thick because it still feels like I'm missing a step.

I'm actually already doing a similar process with markdown fields in my JSON. If the field on the node ends in _markdown then I parse the markdown in the field with remark myself in gatsby-node.js and pop that onto the object with the key minus the markdown name. So in the CMS I create for example body_markdown and that results in a body field that I can query with parsed markdown HTML ready to go. So I get that process.

What I don't get about what you said above is that I want to run sharp on the files. it's not just about getting access to the files themselves, as I can do that with <img> tags by just popping the path in there. So really my questions are:

  • Where's the best place to process the images with sharp exactly?
  • Do I need to write that from scratch or can I use the existing plugin / transformer or both?
  • If I'm not using the existing setup, then once I've done the processing with sharp, where do I put the data? How do I query and get the smaller image? Does it come in as base64 from a node directly or do I put files somewhere as part of the build process and spit the paths out in the nodes, or what? Where is this place?

So I'm comfortable building this if I need to, I just don't understand Gatsby's data model enough to know how to approach it, where to put the results of the processing, and then how to get it out on the query side.

If you use gatsby-plugin-sharp and gatsby-transformer-sharp gatsby will automatically add childImageSharp (which enables image processing via graphql) to File nodes (only requirement is that file extension is one of supported ones https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-transformer-sharp#parsing-algorithm ). You don't need to run sharp manually in your code for that.

Alrighty, awesome. Thanks again for your help. I'll give that a try and will report back if I hit something more specific as a blocker.

I've tested the implementation I proposed and it works as for markdown : the catch is that the image path has to be relative. So it's actually an incompatibility between NetlifyCMS and gatsby-transformer-sharp...
Looks like they're working on integrating public folder with relative path in NetlifyCMS: https://github.com/netlify/netlify-cms/issues/325
So I guess the simplest/smartest would be to fix that issue, or have a smarter "relative path" finder in gatsby-transformer-sharp.

@zecakeh I'm not using Markdown files though, it's specifically with gatsby-transformer-json that I need to get this to work.

I'm sorry if my previous message wasn't clear enough. I'm saying it works with gatsby-transformer-json too, I tried it. But the issue is the same as in Markdown: the image path has to be relative for gatsby-transformer-sharp to transform the image node into a childImageSharp node.

@zecakeh, Ah, sorry for the misunderstanding, that's my bad. I actually think I'l be fine in this case because my content all lives in subdirectories, so I'm pretty sure all my content links start with a /. I'll give it a go.

@zecakeh, I tried your way, but I still can't figure out what to do. The example you've given above leverages gatsby-remark-images which doesn't exist for JSON. What exactly was the setup that you did which worked? Could you show me an example gatsby-config.js so I know what you hooked up?

Hey @blargity I just answered another question that's similar to this one here.

To make this short, in your gatsby-node.js you can add fields and add a relative path to your image that gatsby will recognise, and then you access childImageSharp on that.

@randomchars I do really appreciate the help but it's still not working. I must be being really thick here. My content file is in <project root>/content/team/professor-janice-russell.json. In that file NetlifyCMS has put a field like this:

{
  "avatar": "/assets/janice russell.jpg",
}

In my gatsby-node.js I have code that adds a field that is a relative path (relative with respect to the content JSON file) to find the image. I call the field avatar_image in this case. When I query in GraphiQL, I see it fine:

Query

query SearchIndexExampleQuery {
  allTeamJson(filter: { avatar: {eq: "/assets/janice russell.jpg"}}) {
    edges {
      node {
        avatar
        fields {
          avatar_image
        }
      }
    }
  }
}

Result

{
  "data": {
    "allTeamJson": {
      "edges": [
        {
          "node": {
            "avatar": "/assets/janice russell.jpg",
            "fields": {
              "avatar_image": "../../static/assets/janice russell.jpg"
            }
          }
        }
      ]
    }
  }
}

If I go into the content directory where the content file lives and run open "../../static/assets/janice russell.jpg" my computer happily opens the file.

Yet I cannot query childImageSharp anywhere on the node. This:

Query

query SearchIndexExampleQuery {
  allTeamJson(filter: { avatar: {eq: "/assets/janice russell.jpg"}}) {
    edges {
      node {
        avatar
        fields {
          avatar_image
          childImageSharp
        }
      }
    }
  }
}

Result

{
  "errors": [
    {
      "message": "Cannot query field \"childImageSharp\" on type \"fields_3\".",
      "locations": [
        {
          "line": 9,
          "column": 11
        }
      ]
    }
  ]
}

And obviously if I try it as a child of avatar_image then string doesn't have children...

I also tried relative paths but relative to the gatsby-node.js file which didn't work either. In my config I have both gatsby-transformer-sharp and gatsby-plugin-sharp in the pipeline after gatsby-transformer-json. I tried deleting .cache to be sure and still didn't get it showing up.

What in the world am I missing? Maybe you could post a working example so I can compare? I'm just missing something obvious and I can't find it. Sorry for the trouble.

Ok, I figured it out!

In order for the relative paths to work, you also need to add the directory where the images are located as a file source. So I needed to add this to gatsby-config.js as well as gatsby-transformer-sharp and gatsby-plugin-sharp:

{
    resolve: `gatsby-source-filesystem`,
    options: {
        path: path.join(__dirname, 'static', 'assets'),
        name: 'assets',
    },
},

Thank you @randomchars and @zecakeh for all your help!

For anyone finding this issue later, I can now query like this:

query SearchIndexExampleQuery {
  allTeamJson(filter: { avatar: {eq: "/assets/janice russell.jpg"}}) {
    edges {
      node {
        fields {
          avatar_image {
            childImageSharp{
              resize(width: 150, height: 150) {
                src
              }
            }
          }
        }
      }
    }
  }
}

Which correctly generates a thumbnail and gives me the image source back.

@blargity hey 鈥撀燾ould you provide the repo with the complete solution for this? This would help out so much!

I was facing the same exact problem and encountered this thread. Just noticed that this conversation was happening at the exact same time. I love the internet.

@georgesboris unfortunately this isn't open source code yet. I'll have a chat with the client and see if I can get permission.

Hi @blargity - thanks for posting your final solution, but I'm missing a bit - how do you add the field in gatsby-node.js? I'm always being returned a String type.

@georgesboris and @josepjaume I didn't get permission for the whole thing, but I did get permission for this specific code:

In gatsby-node.js:

exports.onCreateNode = function onCreateNode({ node, boundActionCreators }) {
    // We only care about JSON content.
    if (node.internal.owner === 'gatsby-transformer-json') {
        recurseOnObjectProcessingImages(node, node, boundActionCreators);
    }
};

const recurseOnObjectProcessingImages = async (node, content, boundActionCreators) => {
    if (!content) return;
    const { createNodeField } = boundActionCreators;

    if (Array.isArray(content)) {
        for (const object of content) {
            recurseOnObjectProcessingImages(node, object, boundActionCreators);
        }
    } else {
        for (const key of Object.keys(content)) {
            const value = content[key];
            if (typeof value !== 'string') {
                recurseOnObjectProcessingImages(node, value, boundActionCreators);
            } else {
                // Find all values on the object which end in an extension we recognise, then create a
                // file node for them so that all the standard image processing stuff will kick up
                const extensions = new Set([`.jpeg`, `.jpg`, `.png`, `.webp`, `.tif`, `.tiff`]);

                const extension = path.extname(value).toLowerCase();
                if (extensions.has(extension)) {
                    // Ok, we're going to create a field for the image with a relative path
                    // so that the incompatibility between gatsby-transformer-sharp and
                    // NetlifyCMS is avoided.
                    const contentPath = path.join(__dirname, 'content', '<My directory here where the content lives>');
                    const imagePath = path.join(__dirname, 'static', value); // (This is my asset path)
                    const relative = path.relative(contentPath, imagePath);

                    const existingValue = node.fields && node.fields[`${key}_image`];

                    if (existingValue && typeof existingValue === 'string') {
                        createNodeField({
                            node,
                            name: `${key}_image`,
                            value: [existingValue, relative],
                        });
                    } else if (existingValue && Array.isArray(existingValue)) {
                        createNodeField({
                            node,
                            name: `${key}_image`,
                            value: [...existingValue, relative],
                        });
                    } else {
                        createNodeField({ node, name: `${key}_image`, value: relative });
                    }
                }
            }
        }
    }
};

And in creating that field, it causes the rest of the image processing stuff to kick up. I'm not sure if the arrays work properly. Recursive here was a lazy solve, and feel free to do it in a more optimal way, but that's what we're using in production (with some paths edited) and it's working well enough for us on our content.

Thanks so much! This has been really helpful!

@josepjaume no worries!

@thekevinbrown Using your code snippet, I get the following error The \"path\" argument must be of type string. Received type undefined.

Strangely the error disappears after I restart Gatsby and the query runs from the cache. But changing anything will make the error come back again.

Is there any documentation on how Gatsby decides when a String should be File node?

@hmbrg It seems to be just when it finds a file at that path. I wasn't able to find any further documentation on the behaviour here, and it's particularly painful if files can get removed, as from what I could tell, if a single file isn't in its place, then all of them are treated as strings.

My project ended up using a preprocessor of sorts where if it couldn't find the file a piece of content referenced, it'd clear the field in the content rather than let Gatsby get confused.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

benstr picture benstr  路  3Comments

mikestopcontinues picture mikestopcontinues  路  3Comments

signalwerk picture signalwerk  路  3Comments

andykais picture andykais  路  3Comments

magicly picture magicly  路  3Comments