Gatsby: How to create connection between two (or more) content types

Created on 22 Feb 2020  路  4Comments  路  Source: gatsbyjs/gatsby

Summary

How to create connection between two (or more) content types

Relevant information

Hello,

So I have 2 content types Authors and Books, both sourced from markdown files

In gatsby-config.js:

...
  plugins: [
    {
      resolve: 'gatsby-transformer-remark',
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'Authors',
        path: `${__dirname}/contents/authors`,
        ignore: ['**/_*'],
      },
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'Books',
        path: `${__dirname}/contents/books`,
        ignore: ['**/_*'],
      },
    },
  ],
...

Each author files has the following frontmatter:

---
name: Anne Bront毛
born: 17 January 1820
died: 28 May 1849
---

And the books has e.g.:

---
title: Poems by Currer, Ellis, and Acton Bell
published: 1846
authors:
  - Charlotte Bront毛
  - Emily Bront毛
  - Anne Bront毛
---

What I want to achieve is:

  • In individual author page, I can show a list of books she had published, and
  • In individual book page:

    • author(s) name should link to their respective page, and

    • a list of Related Works written by the same author(s),

I'm guessing that Foreign-key fields is how I should do it but I can't really wrap my head around it.

I have tried this in my gatsby-node.js:

exports.createSchemaCustomization = ({ actions, schema }) => {
  const { createTypes } = actions;
  const typeDefs = [
    `
    type Books implements Node {
      books: Books
    }

    type Books {
        authors: [Authors]
    }

    type Authors implements Node {
      authors: [Authors] @link(by: "authors", from: "name")
    }
    `,
  ];
  createTypes(typeDefs);
};

But when I try querying

query MyQuery {
  allBooks {
    totalCount
  }
  allAuthors {
    totalCount
  }
}

the result is

{
  "data": {
    "allBooks": {
      "totalCount": 0
    },
    "allAuthors": {
      "totalCount": 0
    }
  }
}

So, the question is how can I do this?

I have setup a repo here, if anyone interested to look. The createSchemaCustomization bit is in author-book-relationship branch.

Environment (if relevant)

System:
OS: macOS 10.15.3
CPU: (12) x64 Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
Shell: 5.7.1 - /bin/zsh
Binaries:
Node: 12.15.0 - ~/.nvm/versions/node/v12.15.0/bin/node
Yarn: 1.22.0 - /usr/local/bin/yarn
npm: 6.13.4 - ~/.nvm/versions/node/v12.15.0/bin/npm
Languages:
Python: 2.7.16 - /usr/bin/python
Browsers:
Chrome: 80.0.3987.116
Firefox: 71.0
Safari: 13.0.5
npmPackages:
gatsby: ^2.19.17 => 2.19.17
gatsby-source-filesystem: ^2.1.48 => 2.1.48
gatsby-transformer-remark: ^2.6.51 => 2.6.51

File contents (if changed)

gatsby-config.js:

module.exports = {
  siteMetadata: {
    title: 'The Great Gatsby',
    description: 'Reserving judgements is a matter of infinite hope.',
  },
  plugins: [
    {
      resolve: 'gatsby-transformer-remark',
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'Authors',
        path: `${__dirname}/contents/authors`,
        ignore: ['**/_*'],
      },
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'Books',
        path: `${__dirname}/contents/books`,
        ignore: ['**/_*'],
      },
    },
  ],
};

package.json:

{
  "name": "the-great-gatsby",
  "version": "0.0.0",
  "license": "UNLICENSED",
  "private": true,
  "scripts": {
    "dev": "gatsby develop",
    "build": "gatsby build",
    "serve": "gatsby serve",
    "clean": "gatsby clean"
  },
  "dependencies": {
    "gatsby": "^2.19.17",
    "gatsby-source-filesystem": "^2.1.48",
    "gatsby-transformer-remark": "^2.6.51",
    "react": "^16.12.0",
    "react-dom": "^16.12.0"
  }
}

gatsby-node.js:

const path = require('path');
const { createFilePath } = require('gatsby-source-filesystem');

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions;
  if (node.internal.type === 'MarkdownRemark') {
    const type = getNode(node.parent).sourceInstanceName;
    const slug = createFilePath({ node, getNode });
    const path = `/${type.toLowerCase()}${slug}`;
    createNodeField({
      node,
      name: 'type',
      value: type,
    });
    createNodeField({
      node,
      name: 'slug',
      value: slug,
    });
    createNodeField({
      node,
      name: 'path',
      value: path,
    });
  }
};

exports.createPages = async ({ actions, graphql, reporter }) => {
  const { createPage } = actions;
  const getTypes = await graphql(`
    query getTypes {
      allFile {
        distinct(field: sourceInstanceName)
      }
    }
  `);
  const templates = {};
  getTypes.data.allFile.distinct.forEach(type => {
    templates[type] = path.resolve(`src/templates/${type.toLowerCase()}.js`);
    // content type index
    createPage({
      path: `/${type.toLowerCase()}/`,
      component: path.resolve(`src/templates/${type.toLowerCase()}-index.js`),
      context: {},
    });
  });

  const getContents = await graphql(`
    query getContents {
      allMarkdownRemark {
        edges {
          node {
            fields {
              type
              path
            }
          }
        }
      }
    }
  `);
  getContents.data.allMarkdownRemark.edges.forEach(({ node }) => {
    createPage({
      path: node.fields.path,
      component: templates[node.fields.type],
      context: {},
    });
  });
};

exports.createSchemaCustomization = ({ actions, schema }) => {
  const { createTypes } = actions;
  const typeDefs = [
    `
    type Books implements Node {
      books: Books
    }

    type Books {
        authors: [Authors]
    }

    type Authors implements Node {
      authors: [Authors] @link(by: "authors", from: "name")
    }
    `,
  ];
  createTypes(typeDefs);
};

gatsby-browser.js: N/A
gatsby-ssr.js: N/A

stale? question or discussion

Most helpful comment

I had been really struggling with a similar challenge (which is why I subscribed to this issue), but my coworker was able to figure it out, so let me share with you an abbreviated snippet from our gatsby-node.js file to show how we're doing this.

gatsby-node.js:

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

  const typeDefs = `
    type MarkdownRemark implements Node {
      frontmatter: Frontmatter
    }
    type Frontmatter {
    title: String
    slug: String
    parentItem: MarkdownRemark @link(by: "frontmatter.slug", from: "parentItem")
    children:   [MarkdownRemark] @link(by: "frontmatter.parentItem", from: "slug")
    related:   MarkdownRemark @link(by: "frontmatter.slug", from: "related")
    }
  `;
  createTypes(typeDefs);
};

Because the syntax is just not logically clear on first read, we also added the following as explanatory comment for future reference:

  // MarkdownRemark @link(by: "frontmatter.slug", from: "parentItem")
  // SELECT * FROM MarkdownRemark WHERE frontmatter.slug = parentItem LIMIT 1

  // [MarkdownRemark] @link(by: "frontmatter.parentItem", from: "slug")
  // SELECT * from MarkdownRemark WHERE frontmatter.parentItem = slug

The other thing that I think is going on here (as @vladar points out) is you have two different types of content, which further complicates things and muddies the water a little.

So do we, and here's how we're handling them.

First, one thing that was a little confusing to me at first was that everything that's markdown+frontmatter gets shoved into the same allMarkdownRemark, so you've got to create a collection and then filter your queries in individual templates.

In our case, we have team bios that are markdown documents, and we have a whole section of our site we call "brains" that is a bunch of individual thought/reference/idea "synapses" networked together via related links and collections (hence the link function above).

We don't want markdown data from the bios to show up in the brains collection and vice versa, so here's what we're doing in order to filter those individual collections:

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

    if (_.get(node, 'internal.type') === `MarkdownRemark`) {
      // Get the parent node
      const parent = getNode(_.get(node, 'parent'));

      createNodeField({
        node,
        name: 'collection',
        value: _.get(parent, 'sourceInstanceName')
      });
    }
  };

The above code grabs the name of the collection set in gatsby-config.js and adds it as a field.

When we're doing createPage in gatsby-node.js, we grab just the relevant records from the collection we want.

    const allEdges = results.data.everything.edges;

    const bioEdges = allEdges.filter(
      edge => edge.node.fields.collection === `bios`
    );

    const brainEdges = allEdges.filter(
      edge => edge.node.fields.collection === `brains`
    );

    const topics = results.data.topicsGroup.group;
    const types = results.data.typesGroup.group;

      const previous =
        index === brainEdges.length - 1 ? null : brainEdges[index + 1].node;
      const next = index === 0 ? null : brainEdges[index - 1].node;

      if (synapse.frontmatter.template) {
        createPage({
          path: `/brains/${synapse.frontmatter.slug}`,
          component: path.resolve(
            `./src/custom/brains/${synapse.frontmatter.template}`
          ),
          context: {
            date: synapse.frontmatter.date,
            topics: synapse.frontmatter.topics,
            type: synapse.frontmatter.type,
            title: synapse.frontmatter.title,
            slug: synapse.frontmatter.slug,
            previous,
            next
          }
        });
      } else {
        createPage({
          path: `/brains/${synapse.frontmatter.slug}`,
          component: path.resolve('./src/templates/SynapsePage.js'),
          context: {
            slug: synapse.frontmatter.slug,
            previous,
            next
          }
        });
      }
    });

Then in individual templates, we have that filter as part of our query.

import React from 'react';
  import { graphql, StaticQuery } from 'gatsby';
  import SynapseListItem from './SynapseListItem';

  const SynapseList = () => {
    return (
      <StaticQuery
        query={graphql`
          query {
            allMarkdownRemark(
              filter: { fields: { collection: { eq: "brains" } } }
              sort: { order: ASC, fields: frontmatter___date }
            ) {
              edges {
                node {
                  id
                  html
                  frontmatter {
                    date
                    title
                    slug
                    image
                    topics
                    type
                    priority
                  }
                }
              }
            }
          }
        `}
        render={data => (
          <div>
            {data.allMarkdownRemark.edges.map(edge => (
              <SynapseListItem
                key={edge.node.id}
                slug={edge.node.frontmatter.slug}
                title={edge.node.frontmatter.title}
                image={edge.node.frontmatter.image}
              />
            ))}
          </div>
        )}
      />
    );
  };

  export default SynapseList;

I'm planning to open another issue highlighting that Gatsby's explanation of the @link feature is seriously lacking. I still have no idea how the mapping feature works, as I could never get that to work either. I also think it might also be valuable to have a better explanation for how/when/why to filter collections. (Maybe I learned that from the Gatsby docs, but I feel like I picked that pattern up somewhere else.)

Hope that's helpful!

All 4 comments

So the problem is that you never really have nodes of Authors or Books types. All nodes from markdown are of the same MarkdownRemark type.

If you want to treat them as distinct node types you should transform each MarkdownRemark node to another node (of type Authors or Books depending on sourceInstanceName of the original file).

Kind of implementing own transformer plugin but in your site's gatsby-node.

Also, check out this thread which may be also useful (starting from the following comment):
https://github.com/gatsbyjs/gatsby/issues/3129#issuecomment-360915122

I had been really struggling with a similar challenge (which is why I subscribed to this issue), but my coworker was able to figure it out, so let me share with you an abbreviated snippet from our gatsby-node.js file to show how we're doing this.

gatsby-node.js:

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

  const typeDefs = `
    type MarkdownRemark implements Node {
      frontmatter: Frontmatter
    }
    type Frontmatter {
    title: String
    slug: String
    parentItem: MarkdownRemark @link(by: "frontmatter.slug", from: "parentItem")
    children:   [MarkdownRemark] @link(by: "frontmatter.parentItem", from: "slug")
    related:   MarkdownRemark @link(by: "frontmatter.slug", from: "related")
    }
  `;
  createTypes(typeDefs);
};

Because the syntax is just not logically clear on first read, we also added the following as explanatory comment for future reference:

  // MarkdownRemark @link(by: "frontmatter.slug", from: "parentItem")
  // SELECT * FROM MarkdownRemark WHERE frontmatter.slug = parentItem LIMIT 1

  // [MarkdownRemark] @link(by: "frontmatter.parentItem", from: "slug")
  // SELECT * from MarkdownRemark WHERE frontmatter.parentItem = slug

The other thing that I think is going on here (as @vladar points out) is you have two different types of content, which further complicates things and muddies the water a little.

So do we, and here's how we're handling them.

First, one thing that was a little confusing to me at first was that everything that's markdown+frontmatter gets shoved into the same allMarkdownRemark, so you've got to create a collection and then filter your queries in individual templates.

In our case, we have team bios that are markdown documents, and we have a whole section of our site we call "brains" that is a bunch of individual thought/reference/idea "synapses" networked together via related links and collections (hence the link function above).

We don't want markdown data from the bios to show up in the brains collection and vice versa, so here's what we're doing in order to filter those individual collections:

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

    if (_.get(node, 'internal.type') === `MarkdownRemark`) {
      // Get the parent node
      const parent = getNode(_.get(node, 'parent'));

      createNodeField({
        node,
        name: 'collection',
        value: _.get(parent, 'sourceInstanceName')
      });
    }
  };

The above code grabs the name of the collection set in gatsby-config.js and adds it as a field.

When we're doing createPage in gatsby-node.js, we grab just the relevant records from the collection we want.

    const allEdges = results.data.everything.edges;

    const bioEdges = allEdges.filter(
      edge => edge.node.fields.collection === `bios`
    );

    const brainEdges = allEdges.filter(
      edge => edge.node.fields.collection === `brains`
    );

    const topics = results.data.topicsGroup.group;
    const types = results.data.typesGroup.group;

      const previous =
        index === brainEdges.length - 1 ? null : brainEdges[index + 1].node;
      const next = index === 0 ? null : brainEdges[index - 1].node;

      if (synapse.frontmatter.template) {
        createPage({
          path: `/brains/${synapse.frontmatter.slug}`,
          component: path.resolve(
            `./src/custom/brains/${synapse.frontmatter.template}`
          ),
          context: {
            date: synapse.frontmatter.date,
            topics: synapse.frontmatter.topics,
            type: synapse.frontmatter.type,
            title: synapse.frontmatter.title,
            slug: synapse.frontmatter.slug,
            previous,
            next
          }
        });
      } else {
        createPage({
          path: `/brains/${synapse.frontmatter.slug}`,
          component: path.resolve('./src/templates/SynapsePage.js'),
          context: {
            slug: synapse.frontmatter.slug,
            previous,
            next
          }
        });
      }
    });

Then in individual templates, we have that filter as part of our query.

import React from 'react';
  import { graphql, StaticQuery } from 'gatsby';
  import SynapseListItem from './SynapseListItem';

  const SynapseList = () => {
    return (
      <StaticQuery
        query={graphql`
          query {
            allMarkdownRemark(
              filter: { fields: { collection: { eq: "brains" } } }
              sort: { order: ASC, fields: frontmatter___date }
            ) {
              edges {
                node {
                  id
                  html
                  frontmatter {
                    date
                    title
                    slug
                    image
                    topics
                    type
                    priority
                  }
                }
              }
            }
          }
        `}
        render={data => (
          <div>
            {data.allMarkdownRemark.edges.map(edge => (
              <SynapseListItem
                key={edge.node.id}
                slug={edge.node.frontmatter.slug}
                title={edge.node.frontmatter.title}
                image={edge.node.frontmatter.image}
              />
            ))}
          </div>
        )}
      />
    );
  };

  export default SynapseList;

I'm planning to open another issue highlighting that Gatsby's explanation of the @link feature is seriously lacking. I still have no idea how the mapping feature works, as I could never get that to work either. I also think it might also be valuable to have a better explanation for how/when/why to filter collections. (Maybe I learned that from the Gatsby docs, but I feel like I picked that pattern up somewhere else.)

Hope that's helpful!

Hiya!

This issue has gone quiet. Spooky quiet. 馃懟

We get a lot of issues, so we currently close issues after 30 days of inactivity. It鈥檚 been at least 20 days since the last update here.
If we missed this issue or if you want to keep it open, please reply here. You can also add the label "not stale" to keep this issue open!
As a friendly reminder: the best way to see this issue, or any other, fixed is to open a Pull Request. Check out gatsby.dev/contribute for more information about opening PRs, triaging issues, and contributing!

Thanks for being a part of the Gatsby community! 馃挭馃挏

Hey again!

It鈥檚 been 30 days since anything happened on this issue, so our friendly neighborhood robot (that鈥檚 me!) is going to close it.
Please keep in mind that I鈥檓 only a robot, so if I鈥檝e closed this issue in error, I鈥檓 HUMAN_EMOTION_SORRY. Please feel free to reopen this issue or create a new one if you need anything else.
As a friendly reminder: the best way to see this issue, or any other, fixed is to open a Pull Request. Check out gatsby.dev/contribute for more information about opening PRs, triaging issues, and contributing!

Thanks again for being part of the Gatsby community! 馃挭馃挏

Was this page helpful?
0 / 5 - 0 ratings

Related issues

theduke picture theduke  路  3Comments

ferMartz picture ferMartz  路  3Comments

signalwerk picture signalwerk  路  3Comments

totsteps picture totsteps  路  3Comments

dustinhorton picture dustinhorton  路  3Comments