Building a Drupal menu hierarchy using custom GraphQL resolvers in Gatsby

By edisch, 11 August, 2021

Drupal menus allow sitebuilders to create complex hierarchical content structures. However, due to JSON:API's output format these hierarchies end up being flattened out with parent child relationships being broken apart across different menu_link_content resources. This means if you are sourcing your content using gatsby-source-drupal (which uses Drupal's JSON:API under the hood) you are stuck with the task of recreating this heirarchical content structure from flat references. You might also run into issues when referencing a piece of content via the menu link instead of a particular path. Using a few custom GraphQL resolvers like the ones demonstrated in this blog can help make working with this data a much better experience.

Recreating the menu tree

There's a number of ways to recreate this structure on the Gatsby side but we've recently come up with a quite elegant solution that follows the Gatsby paradigm of reliance on GraphQL by implementing the createResolvers API. Using this API you are able to create custom fields which can be especially useful for relationships between GraphQL nodes. Here's an example of how you might link menu_link_content nodes together to recreate your tree:


// gatsby-node.js

/**
  Note: the `parent` attribute on `menu_link_content__menu_link_content`
  had to be renamed via JSON:API Extras to `parent_menu_link` because
  Gatsby internals uses the `parent` key on nodes.
**/

exports.createResolvers = ({ createResolvers }) => {
  createResolvers({
    menu_link_content__menu_link_content: {
      childMenuLinkContent: {
        type: '[menu_link_content__menu_link_content]',
        resolve(source, _, context) {
          return (
            context
              .nodeModel
              .getAllNodes({ type: 'menu_link_content__menu_link_content' })
              .filter((menuLink) => (
                // parent_menu_link = parent attribute
                menuLink.parent_menu_link === `menu_link_content:${source?.drupal_id}`
                && menuLink?.langcode === source?.langcode
              ))
          )
        }
      }
    }
  })
}

Now querying for a menu structure which automatically includes child menu links is very easy to do via your pages templates:


fragment MenuContent on menu_link_content__menu_link_content {
  title
  link {
    uri
  }
}
  
query {
  allMenuLinkContentMenuLinkContent(
    filter: {
      langcode: {eq: "en"},
      menu_name: {eq: "gatsby-site-header"}
      parent_menu_link: {eq: null}
    }
  ) {
    nodes {
      ...MenuContent
      childMenuLinkContent {
        ...MenuContent
        childMenuLinkContent {
          ...MenuContent
        }
      }
    }
  }
}

That query will then generate a menu tree like this:


{
  "data": {
    "allMenuLinkContentMenuLinkContent": {
      "nodes": [
        {
          "title": "Find a restaurant",
          "link": null,
          "childMenuLinkContent": [
            {
              "title": "California",
              "link": {
                "uri": "internal:/restaurants/california"
              },
              "childMenuLinkContent": []
            },
            {
              "title": "Arizona",
              "link": {
                "uri": "internal:/restaurants/arizona"
              },
              "childMenuLinkContent": []
            }
          ]
        }
      ]
    }
  }
}

It's important to note that if you use this method you will have to know the maximum depth of your menu tree ahead of time. By default Drupal supports up to 9 levels of depth in menu trees and if you're staying under that the query won't get too messy. If you go past 9 levels of depth it might be better to recreate the hierarchy after fetching the GraphQL to avoid a deeply nested query.

Referencing a Gatsby SitePage from a menu link

Pages in Gatsby can be found as nodes in the GraphQL under the type SitePage. These nodes contain information about the page including the path and it's context. Using this data you can reference SitePage nodes from menu_link_content__menu_link_content nodes. This is particularly useful when you are referencing a node from a menu link on the Drupal side rather than a direct path as is seen in this image:

drupal edit menu link

If you reference content like this JSON:API spits out a link value like this: entity:node/[nid] rather than a path. It's then up to you to connect this back up to the path on the Gatsby side. Again, custom GraphQL resolvers can help here! Consider a resolver like this:


// gatsby-node.js

exports.createResolvers = ({ createResolvers }) => {
  createResolvers({
    menu_link_content__menu_link_content: {
      sitePage: {
        type: 'SitePage',
        resolve(source, _, context) {
          return (
            context
              .nodeModel
              .getAllNodes({ type: 'SitePage' })
              .filter((node) => {
                // source.link.uri = entity:node/[nid]
                const [referenceType, nodeRef] = source?.link?.uri?.split(':')
                const [entityType, nodeId] = nodeRef?.split('/')

                // don't continue if source.link.uri doesn't follow pattern
                if (referenceType !== 'entity' || entityType !== 'node') {
                  return false
                }

                // The following assumes you are feeding the nid
                // and langcode of each page into your templates
                // as page context. See: node?.context?.x
                return (
                  node?.context?.nid?.toString() === nodeId
                  && node?.context?.langcode === source?.langcode
                )
              })
              ?.[0]
          )
        }
      }
    }
  })
}

With this new resolver you should be able to now ask for the path of the SitePage referenced from a menu link:


query {
  allMenuLinkContentMenuLinkContent(
    filter: {
      langcode: {eq: "en"},
      menu_name: {eq: "gatsby-site-header"}
      parent_menu_link: {eq: null}
    }
  ) {
    nodes {
      title
      sitePage {
        path
      }
    }
  }
}

And if it's all setup correctly you should see the path of the referenced content as it appears on your Gatsby site!

Taking care of hot-reloading

Using the getAllNodes API is powerful but in order to ensure Gatsby understands the dependencies for your pages you should accompany its use with trackPageDependencies. This is a direct way to tell Gatsby which nodes the page running a query depends on. This is handled for you in all cases except when using the getAllNodes API. You can read more about trackPageDependencies on the Gatsby official docs. If you're feeling extra adventurous dig into the source code on GitHub!

An example where you might use this can be demonstrated by building upon our previous code to create a reference to a SitePage. We used the getAllNodes API to fetch a list of SitePage nodes which we might end up referencing but we never told Gatsby which SitePage node specifically was referenced. To do this, we can add the following:


// gatsby-node.js

context.nodeModel.trackPageDependencies(referencedSitePage)

With this little snippet we are able to tell Gatsby that the page rendered at the path context.path depends upon the SitePage node referencedSitePage. Any subsequent changes to the SitePage referencedSitePage will trigger this page to rebuild using the updated data.


Custom GraphQL resolvers in Gatsby provide the tools to reckon with a lot of the issues you may run into when trying to create complex relationships or fields. Hopefully now you have a better idea of how you might use them on your project. Happy coding! 🎉