Using GraphQL to Query Content Relationships from a Menu Singleton

I've built a singleton nav-menu in Prismic that is a group of Content-Relationship fields. Each field points to one instance of a landing-page repeater type. I thought this was a good way to structure a menu - but maybe not? You guys don't have much guidance on that.

Your documentation on Content Relationship queries is equally pretty poor for a product we're paying over $100/mo for - it only outlines using the where argument which doesn't make sense in this use case.

I've gotten a number of queries to work but the responses don't make sense. Where are the IDs/links to my other docs? I see mention of a link resolver mentioned often but again, very poorly explained.

query NavMenu($uid: String!, $lang: String!) {
  nav_menu(uid: $uid, lang: $lang) {
    _meta {
      uid
      id
    }
    main_menu_items {
      item {
        _linkType
        __typename
      }
    }
    topic_menu_items {
      item {
        _linkType
        __typename
      }
    }
  }
}

Pretty disappointed with DX of this platform and it is unlikely I will recommend this to other clients I have. Simple things are just made to be very difficult. It shouldn't take me half a day to figure out how make and query a menu.

I did a bunch of poking around and trying stuff out last couple hours and I think I’ve landed on it.

The idea these docs do not make clear is that, even if you’re successfully querying a node with content relationship fields, they don’t inherently return anything. In a way, it’s kind of a silent bug.

You have to use a GraphQL union inside of the query, even on a singleton which is counterintuitive (as you’d figure the singleton would just return whatever it’s got since there is only one instance of it)

In my scenario, I’ve got a nav menu singleton with two groups of content-relationship fields inside of them: Landing_page and Topic. Within that singleton, I’m passing in my NavMenu UID which is nav-menu - this threw me for awhile.

Inside of the query, you have to set up the union for each group of content relationships, however many you might have. The following works well, and I still think you need a link resolver which would convert the UID to be used with next/link.

query NavMenu($uid: String!, $lang: String!) {
  nav_menu(uid: $uid, lang: $lang) {
    _meta {
      uid
      id
    }
    main_menu_items {
      item {
        ... on Landing_page {
          title
          _linkType
          _meta {
            uid
            id
          }
        }
      }
    }
    topic_menu_items {
      item {
        ... on Topic {
          title
          _linkType
          _meta {
            uid
            id
          }
        }
      }
    }
  }
}

Hello John, thank you for reaching out about this.

The approach you took is the correct way of building a menu. Usually, you only need one Group field to construct your links. Is there any particular reason you chose to use two?

Afterwards, when mapping the group and rendering the links it’s when you’ll need to use Next links for creating your navigation.

It is true that we don’t currently have an example of how to build a menu using NextJS and GraphQL. I’ve added a note in the backlog so we can take this into account, I’ve also added another one for adding a more clear explanation for the union type in the GraphQL docs.

@Pau - I’ve got two menus, so two groups. This thing is pretty wild - maybe I’ll get the hang of your system but this is so confusing to use Prismic right now. I’m sorry I chose it but it’s too late to switch.

Just look at this data response for a simple list - why is each title nested in another array? How is this supposed to work with the htmlResolver from your documentation vs this RichText component mentioned in the prismic-react package we’re supposed to use? I’ve gotten it to work but my components looks CRAZY. I must be doing something wrong…

{
  "data": {
    "nav_menu": {
      "_meta": {
        "uid": "nav-menu",
        "id": "XzwPtREAACAA8PUE"
      },
      "main_menu_items": [
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Policy",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "policy",
              "id": "XzxF3REAACAA8eVf"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Grants",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "grants",
              "id": "XzxF8BEAACIA8eW6"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Locations",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "locations",
              "id": "XzxGLBEAACIA8ebD"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Research",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "research",
              "id": "XzxGPBEAACEA8ecS"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Campaigns",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "campaigns",
              "id": "XzxGTBEAACMA8edY"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "News",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "news",
              "id": "XzxGXhEAACEA8eer"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Events",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "events",
              "id": "XzxGdBEAACIA8egQ"
            }
          }
        }
      ],
      "topic_menu_items": [
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Biking During COVID-19",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "biking-covid-19",
              "id": "XyxFmhEAACAA6FZF"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Cycling Benefits",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "cycling-benefits",
              "id": "XyxFVhEAACMA6FT4"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "E-Bikes",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "e-bikes",
              "id": "XyxFLxEAACMA6FRL"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Bike Policy",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "bike-policy",
              "id": "XyxFDREAACMA6FOm"
            }
          }
        }
      ]
    }
  }
}

Maybe you could weigh in on my linkResolver?

I’ve been following your docs but they don’t match any of the API responses I get back, so again I just kinda figured it out I think. The links just don’t work right.

import React from 'react'
import Link from 'next/link'

/**
 * 
 * You have to read next/link documentation for this to make any sense:
 * https://nextjs.org/docs/api-reference/next/link#dynamic-routes
 * 
 * <Link> requires `href` and `as` args, why both?
 * `as` is a human readable link that changes with UID 
 * `href` is for dynamic routing so you have to conform to Next syntax [uid], which is static
 * 
 * linkResolver() - helper function that returns the arg for next/link `as`
 * hrefResolver() - helper function that returns the arg for next/link `href`
 * 
 * @param {*} doc - accepts document from Prismic endpoint 
 */
export const linkResolver = (doc) => {
  if (doc.__typename === 'Landing_page') {
    return `/${doc._meta.uid}`
  }
  return '/'
}

export const hrefResolver = (doc) => {
  if (doc.__typename === 'Landing_page') {
    return '/[uid]'
  }
  return '/'
}


/**
 * <DocLink> - a component that generates links to others pages on the site
 * It heavily uses the two functions above, linkResolver and hrefResolver
 * 
 * @param {*} children - accepts children components, React classic
 * @param {*} link - the inbound URL from Prismic
 * @param {*} linkClass - the class a link element may have
 */
export const DocLink = ({ children, link, linkClass }) => {
  console.log(link)
  if (link) {

    // If the link is an internal link, then return a NextLink
    if (link._linkType === 'Link.Document') {
      return (
        <Link
          as={ linkResolver(link) }
          href={ hrefResolver(link) }
        >
          <a className={linkClass}>{children}</a>
        </Link>
      )
    }

    // Otherwise return a normal anchor element
    return (
      <a 
        className={linkClass} 
        href={hrefResolver(link)}
      >
        {children}
      </a>
    )
  }
  return null
}

Ok, so if you have two menus then it makes sense.

The Title field is nested this way so that it can be read by the RichText component. And the HTML serializer allows you to do things like adding custom classes to certain elements, modifying the way an element will be displayed, or to avoid images being wrapped in paragraph tags.

The HTML serializer and the RichText field component work together, e.g:
<RichText render={document.data.text_field} htmlSerializer={htmlSerializer} />

In the Link Resolver, you need to read the type of the document. So you’d need to do doc.type instead of doc.__typename

what does your components look like?
If you have a visual reference of what your menu looks like I could help you build your Menu Custom type.

Let’s start from the top because I think you’re missing what I’m saying.

This is my query:

query NavMenu($uid: String!, $lang: String!) {
  nav_menu(uid: $uid, lang: $lang) {
    _meta {
      uid
      id
    }
    main_menu_items {
      item {
        ... on Landing_page {
          title
          _linkType
          _meta {
            uid
            id
          }
        }
      }
    }
    topic_menu_items {
      item {
        _linkType
        __typename
      }
    }
  }
}

It reliably returns this output:

{
  "data": {
    "nav_menu": {
      "_meta": {
        "uid": "nav-menu",
        "id": "XzwPtREAACAA8PUE"
      },
      "main_menu_items": [
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Policy",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "policy",
              "id": "XzxF3REAACAA8eVf"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Grants",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "grants",
              "id": "XzxF8BEAACIA8eW6"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Locations",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "locations",
              "id": "XzxGLBEAACIA8ebD"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Research",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "research",
              "id": "XzxGPBEAACEA8ecS"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Campaigns",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "campaigns",
              "id": "XzxGTBEAACMA8edY"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "News",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "news",
              "id": "XzxGXhEAACEA8eer"
            }
          }
        },
        {
          "item": {
            "title": [
              {
                "type": "heading1",
                "text": "Events",
                "spans": []
              }
            ],
            "_linkType": "Link.document",
            "_meta": {
              "uid": "events",
              "id": "XzxGdBEAACIA8egQ"
            }
          }
        }
      ],
      "topic_menu_items": [
        {
          "item": {
            "_linkType": "Link.document",
            "__typename": "Topic"
          }
        },
        {
          "item": {
            "_linkType": "Link.document",
            "__typename": "Topic"
          }
        },
        {
          "item": {
            "_linkType": "Link.document",
            "__typename": "Topic"
          }
        },
        {
          "item": {
            "_linkType": "Link.document",
            "__typename": "Topic"
          }
        }
      ]
    }
  }
}

In the Prismic documentation, this is what is stated I should use for a linkResolver:

var PrismicDOM = require('prismic-dom');

var linkResolver = function(doc) {
  // Pretty URLs for known types
  if (doc.type === 'blog') return "/post/" + doc.uid;
  if (doc.type === 'page') return "/" + doc.uid;
  // Fallback for other types, in case new custom types get created
  return "/doc/" + doc.id;
};

var html = PrismicDOM.RichText.asHtml(document.data.body, linkResolver);

If I independently call linkResolver there is no doc argument to pass in - just look at the response from the API. There aren’t any doc fields and this is why it never works.

Take me through this step by step, like I’m five years old - how do I build a linkResolver for Next.js given this data structure? What’s going on inside of RichText that it never works when I pass in linkResolver? I’ve looked at the source for all of this and it too is so poorly documented I can’t figure out how to wire this stuff up.

This is the only way I can get the links to work in Next to work with Prismic - and I just have do around all of your poorly documented serializers, resolvers, packages, etc

<MainNav>
  { mainMenu.map( (menu_item) => {
    return (
      <MainNavItem key={ randomID(10000000)} >
        <Link 
          href={ '/' + menu_item.item._meta.uid } >
          <a>
            { RichText.asText(menu_item.item.title) }
          </a>
        </Link>
      </MainNavItem>
    )
  })}
</MainNav>

Hello!

Please take a look at the files in this example project: Multi page navigation site with NextJS and Prismic.

Note: we’re not using GraphQL in this example, we’ll only focus on the way we handle the Links. Using GraphQL is different because, when you retrieve a Content Relationship field to a Document, you need to call it’s metadata to then pass it to the Link resolver, this metadata will be used and understand as the doc that you were mentioning.

Here we created an extra component that will handle both internal and external Links in Next, so you only have to pass the Link to it.

This issue has been closed due to inactivity.