Connecting two gatsby nodes
Asked Answered
V

1

7

So, I'm using the gatsby-mdx plugin to create a site from MDX files. I want to create an association between the SitePage object and the Mdx object so that I can do one graphQL query of the SitePage edges in order to construct a site navigation.

Much of my code is in TypeScript, so ignore any type annotations if you're wondering WTF those are.

Things I've tried

Using Fields

My first thought was to use the onCreateNode API, grab the MDX node, and add it to the SitePage using the createNodeField action. That all works great, B-U-T the gatsby-mdx plugin adds a bunch of other info to their node later using the setFieldsOnGraphQLNodeType API (which occurs after the onCreateNode API). I want those fields (such as frontmatter and tableOfContents) to be available in later graphql queries, but they aren't using this method.

Implementing my own setFieldsOnGraphQLNodeType

I figured I could just extend the SitePage object the same way gatsby-mdx was extending the Mdx node.

The key problem I ran into here was that I couldn't figure out how to create the Mdx GraphQL node type.

export const setFieldsOnGraphQLNodeType = ({type, actions, getNodes}: any, pluginOptions: any) => {
    if (type.name === "SitePage") {
        const {createParentChildLink} = actions
        return new Promise((resolve) => {
            return resolve({
                "childMdx": {
                    type: new GraphQLObjectType({
                        name: 'Mdx'
                    }),
                    async resolve(sitePageNode: any) {
                        const allNodes = getNodes()
                        if (sitePageNode.component &&
                            (sitePageNode.component.endsWith(".mdx") || sitePageNode.component === DefaultLayout)
                        ) {
                            const associatedMdx = allNodes.find((mdxNode: any) =>
                                mdxNode.internal.type === 'Mdx' && mdxNode.fileAbsolutePath === sitePageNode.component
                            )
                            if (associatedMdx) {
                                console.log("Found associated MDX node", associatedMdx.id)
                                console.log("Adding it to the sitepage node", sitePageNode.id)
                                return associatedMdx
                            }
                        }
                    }
                }
            })
        })
    }
    return {}
}

I also tried simply passing the type as a String ('Mdx'), but that failed too.

Using Parent-Child Links

That plugin creates a parent-child link between the File node and the parsed MDX node in the onCreateNode API, using the createParentChildLink action (source).

I tried implementing that...

export const onCreateNode = ({node, actions, getNodes}: OnCreateNodeArgument) => {
    const {createParentChildLink} = actions
    const allNodes = getNodes()
    if (node.internal && node.internal.type === 'SitePage' && node.component &&
        (node.component.endsWith(".mdx") || node.component === DefaultLayout)
    ) {
        const associatedMdx = allNodes.find((mdxNode: any) =>
            mdxNode && mdxNode.internal && mdxNode.internal.type === 'Mdx' &&
                (mdxNode.fileAbsolutePath === node.component || mdxNode.fileAbsolutePath === node.context.fileAbsolutePath)
        )
        if (associatedMdx) {
            console.log("Found associated MDX node", associatedMdx.id)
            console.log("Adding it to the sitepage node as a child", node.id)
            createParentChildLink({parent: node, child: associatedMdx})
        }
    }
}

At first, that appears to succeed, but the tableOfContents property that gatsby-mdx adds to the Mdx node still isn't available in a graphQL query like:

{
    allSitePage(filter: {fields: {childMdx: {id: {ne: null}}}}) {
        edges {
            node {
                path
                fields{
                    childMdx {
                        tableOfContents
                        fileAbsolutePath
                        frontmatter {
                            title
                        }
                    }
                }
                context {
                    roughFilePath
                    id
                }
            }
        }
    }
}

Other (possibly irrelevant) info

I'm creating some pages programmatically in gatsby-node.js.

I've seen a suggestion for similar use cases to use node type mappings, but I since my mapping between the SitePage & the MDX object requires a bit of finesse (specifically, reading some things from siteMetadata and doing a string comparison), I don't think that will work for my use case.

Vasculum answered 15/1, 2019 at 16:52 Comment(5)
Maybe I'm missing something, but shouldn't all mdx files already be a part of sitePage nodes? if your purpose is to construct a site navigation, won't you be able to find all the generated pages there?Higherup
So, the SitePage nodes don't directly link to their associated Mdx node in graphql queries without some intervention. There's some specific information on the Mdx nodes that I want for the navigation: frontmatter and tableOfContents. Those aren't on the SitePage node itself. While I can do one big query of both allSitePage and allMdx, then finagle them together using JavaScript, that... well, kind of sucks! By all appearances, I should be able manipulate the SitePage nodes to include the information I want, but the timing of the various phases complicates matters.Vasculum
The createParentChildLink action seems like the most appropriate solution. I may just need to make a deep debugger dive on that TypeError: Cannot read property 'internal' of undefined message.Vasculum
Updated question, as that cryptic error went away after I cleared the .cache folder and restarted.Vasculum
Thanks for providing additional context, I've never dealt with this but it's probably good to know how. I've just shared my findings as an answer belowHigherup
H
11

So I finally found a better solution (than my previous attempt, which involves pumping mdx node into page’s context).

Gatsby has a undocumented method to link nodes to one another:

Yes, you can can use createNodeField with the not-yet-documented ___NODE syntax to create links between nodes.

So, the steps are like this:

  • In createPage, store the id of the Mdx node to SitePage node.
  • In onCreateNode, if node is SitePage, use createNodeField with the Mdx___NODE as field name and Mdx node's id as value.

My gatsby-node.js:

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

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

  if (node.internal.type === "SitePage" && node.context && node.context.id) {

    createNodeField({
      name: "Mdx___NODE",
      value: node.context.id,
      node,
    })
  }

  if (node.internal.type === "Mdx") {
    const value = createFilePath({ node, getNode })
    createNodeField({
      // 1) this is the name of the field you are adding,
      name: "slug",
      // 2) this node refers to each individual MDX
      node,
      value: `/blog${value}`
    })
  }
}


exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions;
  const { data, errors } = await graphql(`
    {
      allMdx {
        edges {
          node {
            id
            fields {
              slug
            }
          }
        }
      }
    }
  `)

  if (errors) throw errors
  data.allMdx.edges.forEach(({ node }) => {
    createPage({
      path: node.fields.slug,
      component: path.resolve(`./src/components/posts-page-layout.js`),
      context: { id: node.id }
    });
  });
};

Result:

graphiql

Hope it helps!

Higherup answered 16/1, 2019 at 3:21 Comment(1)
It definitely does! Thanks.Vasculum

© 2022 - 2024 — McMap. All rights reserved.