How to create connection between two (or more) content types
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:
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.
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
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
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! 馃挭馃挏
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.jsfile to show how we're doing this.gatsby-node.js:
Because the syntax is just not logically clear on first read, we also added the following as explanatory comment for future reference:
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:
The above code grabs the name of the collection set in
gatsby-config.jsand adds it as a field.When we're doing
createPageingatsby-node.js, we grab just the relevant records from the collection we want.Then in individual templates, we have that filter as part of our query.
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
mappingfeature 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!