At work we're using Gatsby to create a site; the content is added by Squidex, a headless CMS. Gatsby was great when we were using local Markdown files to create blog posts, but using data from the CMS was slightly trickier.

The problem

In Squidex, author and post schema were separate, so authors can be linked to many posts; image assets are also separate from the actual post content. This means that to create one post in Gatsby, we needed three steps:

  • Get written post content
  • Get assets referenced by the post (such as the hero image)
  • Get the author data

Once we had all the data, we needed a way to associate them in Gatsby so we can query the data with Graphql. We wanted to process the images with gatsby-images to create responsive versions of the images, and then all the data to turn up in one query; effectively the equivalent of a 'join'.

This is what we had in a Graphql query - the hero and author data was just an id referencing Squidex's internal data. We needed to find a way to be able to query the hero and author nodes directly in Graphql.

{
  allCmsMarkdown {
    edges {
      node {
        content

        frontmatter {
          hero # just an id
          author # also just an id - we want to be able to query name and image
        }

      }
    }
  }
}

The solution

Gatsby allows nodes to be linked together via foreign keys - using a special triple underscore syntax, you can reference the data from other nodes, which will automatically be queryable in your Graphql query. In our case, we added hero___NODE = [id] in our gatsby-node file which was fetching data from the CMS, and the hero image was processed with gatsby-image and appeared in the Graphql query.

The example code only shows linking the post and image, but the author node is linked in exactly the same way.


exports.sourceNodes = async({ getNodes, actions, createNodeId, createContentDigest }) => {
  // fetch the image assets from Squidex and download them
  // once downloaded, Gatsby automatically creates nodes for them which can be queried via `getNodes`
  const allAssets = await getAllAssets();

  // get the image assets we've just downloaded to link with posts and authors
  const imgNodes = getNodes().filter(img => img.internal.type === 'File' && img.relativeDirectory === 'cms-images');

  const posts = await getBlogPosts();

  posts.items.forEach(post => {
    const { content, ...frontmatter } = post;

    const node = {
      id: createNodeId(post.id),
      children: [],
      parent: id,
      internal: {
        content: JSON.stringify(content),
        type: 'CmsMarkdown',
        mediaType: 'text/markdown'
      },

      content: content,
      frontmatter: {
        locale: lang,
        _PARENT: id
      }
    };

    // note special *triple* underscore syntax - this allows hero node to be queried in Graphql as part of post
    // link to the hero image node
    // find the image node and use it's id as the foreign key in the post
    const hero = imgNodes.find(img => img.name === frontmatter.hero.iv[0]);
    node.frontmatter.hero___NODE = hero.id;
  });
};

Code breakdown

exports.sourceNodes is one of Gatsby's Node APIs, and is called to create nodes, and after all your plugins.

const allAssets = await getAllAssets(); calls the Squidex API to get asset data, then downloads the images and saves them locally. Gatsby also creates a File node for each image - this is important, since it allows us to link to the image assets. We also need to do this before creating the node for each post, so that we can find the image node through getNodes() (if we created the post first, the image node wouldn't have been created so we couldn't link them).

const imgNodes = getNodes().filter(img => img.internal.type === 'File' && img.relativeDirectory === 'cms-images'); is finding all the nodes for the images we just downloaded. The specific filter you use will depend on where you saved the images after downloading them. We can then get the id from each node to link with our post's node.

We then get the author and post data, and loop through each post to create a node.

const hero = imgNodes.find(img => img.name === frontmatter.hero.iv[0]); finds the hero image node which corresponds to the asset id set by Squidex, which we then use to link the post and hero nodes.

node.frontmatter.hero___NODE = hero.id; is the key line! The triple underscore syntax ___NODE means Gatsby will create a field on our post node called 'hero', which we can then query all the hero information from, including the processed imageSharp fields.

Data from other plugins

If your data is coming from another plugin, then the nodes will already have been created by the time gatsby-node methods are run. Instead of recreating nodes, you can add a field to the existing node using the createNodeField function from the actions argument passed in - there's an example here on Gatsby's Github.

Query the linked data

You can then query the post, and hero image and author information will automatically be joined by Gatsby. (Note that childImageSharp is created by a separate plugin, gatsby-image).

{
  allCmsMarkdown {
    edges {
      node {
        content

        frontmatter {
          # hero has been added
          hero {
            childImageSharp {
              fluid(maxWidth: 1000) {
                ...GatsbyImageSharpFluid
              }
            }
          }

          # author node has been added
          author {
            name
            image {
              childImageSharp {
                fluid(maxWidth: 300) {
                  ...GatsbyImageSharpFluid
                }
              }
            }
          }

        }
      }
    }
  }
}

More info

Gatsby's documentation is a bit haphazard: I found a lot of the most useful examples in Github issues.

Gotchas

  • Make sure you wait for all images to be downloaded, otherwise they won't show up in getNodes()

Example data from API

Post data

{
    "total": 2,
    "items": [
        {
            "id": "6df6471b-6514-47f6-9854-fdb2b0a2735f",
            "createdBy": "subject:5bd9e65bd6017700010fbd9c",
            "lastModifiedBy": "subject:5bd9e65bd6017700010fbd9c",
            "data": {
                "headline": {
                    "en": "First post!",
                },
                "path": {
                    "en": "first-post-cms",
                },
                "hero": {
                    "iv": [
                        "6e0ef345-0703-476b-bae6-d8ef135e6e05" // links to asset of the same id
                    ]
                },
                "author": {
                    "iv": [
                        "c8cc0f22-8996-4771-874c-aae486432ccb" // links to the author
                    ]
                },
                "published": {
                    "iv": "2018-11-13T15:33:40Z"
                },
                "content": {
                    "en": "This is a post from the Squidex CMS.\n\n!\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\ntempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,\nquis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\nconsequat. Duis aute irure dolor in reprehenderit in voluptate velit esse\ncillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non\nproident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n",
                },
            },
            "isPending": false,
            "created": "2018-11-13T15:34:21Z",
            "lastModified": "2018-11-19T09:53:55Z",
            "status": "Published",
            "version": 5
        }

  ...
}
```<h4 id="authors">Authors</h4>

```json
{
    "total": 2,
    "items": [
        {
            "id": "c8cc0f22-8996-4771-874c-aae486432ccb", // this is the author linked to by the post
            "createdBy": "subject:5bd9e65bd6017700010fbd9c",
            "lastModifiedBy": "subject:5bd9e65bd6017700010fbd9c",
            "data": {
                "name": {
                    "iv": "Hans Gruber"
                },
                "image": {
                    "iv": [
                        "de45bbab-6fd6-4dec-8d63-a2b97575cc3f"
                    ]
                }
            },
            "isPending": false,
            "created": "2018-11-16T15:29:52Z",
            "lastModified": "2018-11-16T15:29:52Z",
            "status": "Published",
            "version": 1
        },
  ...
}


<h4 id="assets">Assets</h4>

```json
{
    "items": [
        {
            "id": "6e0ef345-0703-476b-bae6-d8ef135e6e05", // this is the hero image referenced by the post
            "fileName": "kalpesh-patel-786182-unsplash.jpg",
            "mimeType": "image/jpeg",
            "fileType": "jpg",
            "tags": [
                "type/jpg",
                "image",
                "image/large"
            ],
            "fileSize": 896475,
            "fileVersion": 0,
            "isImage": true,
            "pixelWidth": 1667,
            "pixelHeight": 2500,
            "createdBy": "subject:5bd9e65bd6017700010fbd9c",
            "lastModifiedBy": "subject:5bd9e65bd6017700010fbd9c",
            "created": "2018-11-16T15:13:11Z",
            "lastModified": "2018-11-16T15:13:11Z",
            "version": 0
        },
        {
            "id": "da339b50-6a8b-4a1c-95ba-4375517ce82f",
            "fileName": "george-kedenburg-iii-415172-unsplash.jpg",
            "mimeType": "image/jpeg",
            "fileType": "jpg",
            "tags": [
                "type/jpg",
                "image",
                "image/large"
            ],
            "fileSize": 770222,
            "fileVersion": 0,
            "isImage": true,
            "pixelWidth": 2500,
            "pixelHeight": 1667,
            "createdBy": "subject:5bd9e65bd6017700010fbd9c",
            "lastModifiedBy": "subject:5bd9e65bd6017700010fbd9c",
            "created": "2018-11-16T15:13:10Z",
            "lastModified": "2018-11-16T15:13:10Z",
            "version": 0
        },
  ...

}