DEV Community

Cover image for Setup Menu Navigation - Guide to Gatsby WordPress Starter Advanced with Previews, i18n and more
Henrik Wirth for NeverNull

Posted on • Edited on

Setup Menu Navigation - Guide to Gatsby WordPress Starter Advanced with Previews, i18n and more

In this part we will create some navigation based on a WordPress Menu, to reference our pages and posts, that we generated in the previous part.

Table of Contents

Create WordPress Menu 🌐

Let's get started by creating content in WordPress.

  1. Before creating a menu, make sure you have some example pages like so:

  2. Create the Main Menu with the pages you want to add. Assign the page to the primary location.
    Menu WordPress

  3. Play around in the GraphiQL explorer:
    Menu GraphQL

    • If you use the WordPress Twenty Twenty Theme and set your menu to the primary position, you have to set the where->location to PRIMARY. Play around with the explorer to see what options you have in the location drop-down.
    • As you can see, the URL we get is not relative and there is now slug. We could extend the WPGraphQL schema to add another field for that. We will do this later. For now we just gonna strip the domain from the URL in the frontend (quick and dirty).

Create Components in Gatsby 💻

Now, let's add some code to render our menu in Gatsby.

1.) Creating a utils function to create relative URLs:

// src/utils/index.js

/**
 * Parses a menu item object and returns Gatsby-field URI.
 *
 * @param {object} menuItem a single menu item
 * @param wordPressUrl
 * @param blogURI
 */
export const CreateLocalLink = (menuItem, wordPressUrl, blogURI='blog/') => {
  const { url, connectedObject } = menuItem;

  if (url === '#') {
    return null;
  }
  /**
   * Always want to pull of our API URL.
   */
  let newUri = url.replace(wordPressUrl, '');

  /**
   * If it's a blog link, respect the users blogURI setting.
   */
  if (connectedObject && connectedObject.__typename === 'WPGraphQL_Post') {
    newUri = blogURI + newUri;
  }

  return newUri;
};
Enter fullscreen mode Exit fullscreen mode

The GraphQL endpoint is serving us with absolute URLs to the WordPress instance. Therefore, we have to do some magic to workaround this and get relative URLs.

While this is an easy fix for the frontend, you can also add another field to the schema and do it in the backend. Then you don't need extra logic and client computation in the frontend. See this docs on how to add custom fields to WPGraphQL.


2.) Creating MenuItem component, that uses our util function:

// src/components/MenuItem.js

import React from "react"
import { CreateLocalLink } from "../utils"
import { Link } from "gatsby"

const MenuItem = ({ menuItem, wordPressUrl }) => {
  return (
    <Link style={{marginRight: '20px' }} to={CreateLocalLink(menuItem, wordPressUrl)}>{menuItem.label}</Link>
  )
}

export default MenuItem
Enter fullscreen mode Exit fullscreen mode

3.) Creating Menu component, that uses our MenuItem component:

// src/components/Menu.js

import React from "react"
import { StaticQuery, graphql } from "gatsby"

import MenuItem from "./MenuItem"

/**
 * Define MenuItem fragment and get all primary menu items.
 */
const MENU_QUERY = graphql`

    fragment MenuItem on WPGraphQL_MenuItem {
        id
        label
        url
        title
        target
    }

    query GETMAINMENU {
        wpgraphql {
            menuItems(where: {location: PRIMARY}) {
                nodes {
                    ...MenuItem
                }
            }
            generalSettings {
                url
            }
        }
    }
`

const Menu = () => {
  return (
    <StaticQuery
      query={MENU_QUERY}
      render={(data) => {
        if (data.wpgraphql.menuItems) {
          const menuItems = data.wpgraphql.menuItems.nodes
          const wordPressUrl = data.wpgraphql.generalSettings.url

          return (
            <div style={{ marginBottom: "20px" }}>
              {
                menuItems &&
                menuItems.map((menuItem) => (
                  <MenuItem key={menuItem.id} menuItem={menuItem} wordPressUrl={wordPressUrl}/>
                ))
              }
            </div>
          )
        }
        return null
      }}
    />
  )
}

export default Menu
Enter fullscreen mode Exit fullscreen mode

Here we are using Gatsby's StaticQuery to get all the MenuItems. Also, we are making use of fragments to be able to reuse the query fields later on.


4.) And finally adding the Menu to layout.js:

// src/components/layout.js

/**
 * Layout component that queries for data
 * with Gatsby's useStaticQuery component
 *
 * See: https://www.gatsbyjs.org/docs/use-static-query/
 */

import React from "react"
import PropTypes from "prop-types"
import { useStaticQuery, graphql } from "gatsby"

import Header from "./header"
import "./layout.css"
import Menu from "./Menu"

const Layout = ({ children }) => {
  const data = useStaticQuery(graphql`
    query SiteTitleQuery {
      site {
        siteMetadata {
          title
        }
      }
    }
  `)

  return (
    <>
      <Header siteTitle={data.site.siteMetadata.title} />
      <div
        style={{
          margin: `0 auto`,
          maxWidth: 960,
          padding: `0px 1.0875rem 1.45rem`,
          paddingTop: 0,
        }}
      >
        <Menu />
        <main>{children}</main>
        <footer>
          © {new Date().getFullYear()}, Built with
          {` `}
          <a href="https://www.gatsbyjs.org">Gatsby</a>
        </footer>
      </div>
    </>
  )
}

Layout.propTypes = {
  children: PropTypes.node.isRequired,
}

export default Layout
Enter fullscreen mode Exit fullscreen mode

Menu Child Items 🚸

This won't be part of our code-base for now, but in case you need the childItems of a MenuItem to create a multi-level navigation, you can do so, by adjusting the query like so:

const MENU_QUERY_WITH_CHILDREN = graphql`

  fragment MenuItem on WPGraphQL_MenuItem {
    id
    label
    url
    title
    target
  }

  query GETMAINMENU {
    wpgraphql {
      menuItems(where: {location: PRIMARY}) {
        nodes {
          ...MenuItem
          childItems {
            nodes {
              ...MenuItem
              childItems {
                nodes {
                  ...MenuItem
                }
              }
            }
          }
        }
      }
      generalSettings {
        url
      }
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

This will get 3 levels of menuItems. You will need to add child items to the WordPress menu to see any results. Also, you would need to adjust your component, to support the extra 2 levels in a recursive manner.

Support External Links 🔗

Gatsby's Link component doesn't support the use of external links. We want to be able to add external Links to our menu and therefore have to do a little extra work as shown in the Gatsby docs.

1.) Create UniversalLink component:

// src/components/UniversalLink.js

import React from "react"
import { Link as GatsbyLink } from "gatsby"
// Since DOM elements <a> cannot receive activeClassName
// and partiallyActive, destructure the prop here and
// pass it only to GatsbyLink
const UniversalLink = ({ children, to, activeClassName, partiallyActive, ...other }) => {
  // Tailor the following test to your environment.
  // This example assumes that any internal link (intended for Gatsby)
  // will start with exactly one slash, and that anything else is external.
  const internal = /^\/(?!\/)/.test(to)
  // Use Gatsby Link for internal links, and <a> for others
  if (internal) {
    return (
      <GatsbyLink
        to={to}
        activeClassName={activeClassName}
        partiallyActive={partiallyActive}
        {...other}
      >
        {children}
      </GatsbyLink>
    )
  }
  return (
    <a href={to} {...other} target="_blank" rel="noopener noreferrer">
      {children}
    </a>
  )
}
export default UniversalLink
Enter fullscreen mode Exit fullscreen mode

2.) Update MenuItem.js:

// src/components/MenuItem.js

import React from "react"
import { CreateLocalLink } from "../utils"
import UniversalLink from "./UniversalLink"

const MenuItem = ({ menuItem, wordPressUrl }) => {
  return (
    <UniversalLink style={{ marginRight: "20px" }}
                   to={CreateLocalLink(menuItem, wordPressUrl)}>
      {menuItem.label}
    </UniversalLink>
  )
}

export default MenuItem
Enter fullscreen mode Exit fullscreen mode

Final Thoughts 🏁

If you run gatsby develop now, you should see a Navigation with your menu items navigating to you relative page paths.

Find the code here: https://github.com/henrikwirth/gatsby-starter-wordpress-advanced/tree/tutorial/part-3

What's Next ➡️

Next we'll build our navigation based on a WordPress menu.

Part 4 - Deployment

Top comments (4)

Collapse
 
arielnt1993 profile image
Ariel Navarro

Hi, first of all thanks for this series (I had no idea I could fetch the pages and post in a recursive way). Right now I have 2 menus PRIMARY and PRIMARY__ES (the translated version) how can I fetch that? do I have to create two files menu.js and menuEs.js? or I can do that from the same file? I also have two footers, and I guess it would the same process as with the menu. thanks in advance!!!

Collapse
 
davidshq profile image
Dave Mackey

Hi Henrik, thanks for the tutorial series, it has been quite helpful!

When I attempt to run gatsby develop after updating the files per your instructions above I'm getting the following error messages:

ERROR #85901 GRAPHQL
There was an error in your GraphQL query:
Expected file type WPGraphQL_MenuLocationEnum, found PRIMARY.

File: src\components\Menu.js:21:41

ERROR #98123 WEBPACK

Generating development JavaScript bundle failed

D:\learn\gatsby-starter-wordpress-advanced\src\components\Menu.js

21:41 error Expected type WPGraphQL_MenuLocationEnum, found PRIMARY graphql/template-strings

Any ideas on what I might be doing wrong?

Collapse
 
davidshq profile image
Dave Mackey

Seems the theme I was using used MENU_1 instead of PRIMARY for the menu id, once I changed to MENU_1 it began working!

Collapse
 
henrikwirth profile image
Henrik Wirth

Glad you already figured it out ✌