Our Thoughts
Vertika Jain
30 June, 2021
8 MIN READ

Next.js for Decoupled Menus Initiative

Decoupled or Headless CMS fully or progressively decouples the frontend from the backend. In this approach, Drupal serves as a backend content vault, while one can choose a frontend technology that suits them best for a project. The Decoupled Menus initiative is a step towards exploring potential of Decoupled Drupal integration with various frameworks by making Drupal Menus as an API to be integrated with a modern JS framework such as React/Next.js. This idea has been officially launched as Drupal Core Strategic Initiative. You can find more information on this initiative here

Decoupled Menus Initiative - NextJS

Source - Drupal.org

We, at QED42, have chosen NextJS - a Server Side Rendering and Static Generation framework of React as the frontend to pre-render the menu. Due to its features like prefetching, fast refresh, automatic routing, compilation and bundling, performance optimization, SEO, and much more.


Why do we Decouple our Menus?

Background: Every time a non-developer wants to change a menu item on the Drupal side, a developer has to get involved to implement it on the JS side. That developer must change and rebuild the code, and then deploy those changes to production. These steps may take days to complete depending on the availability of the developer, and the complexity of the deployment process.

The solution suggested by the Drupal team: By providing menu items via Drupal’s JSON:API, these deployment steps can be eliminated and a core Javascript package can reduce the lines of code that a JS developer has to write in the first place. Have a look at the Decoupled menus in Drupal Demo by Gabe Sullice for more details about the initiative, discussing the current user’s authentication status, menu hierarchies and dropdown, user-defined menu items and routing.

Other reasons are as follows –

  1. Recognize improvements for Headless CMS, with the menu as a use case
  2. Not all Drupal modules need to be modules 
  3. Especially while updating the version of Drupal, most of these do not require the database and other features of Drupal
  4. Some Drupal modules do not make complete utilization of Drupal and can be built in other languages
  5. Shorter bandwidth to rebuild all modules in a reasonable timeframe.
  6. NextJS came into view because its modules are much simpler and faster to write.
  7. We can create features that are missing and much needed to develop.
  8. We do not want to create functionality that already exists.
  9. Make Decoupled Drupal and menus attractive to non-Drupal FE developers.

Please Note: You can join the Decoupled-menu-Initiative slack channel here. The weekly meetings take place every Tuesday at 14:00 UTC and again at 4:00 UTC at the Slack channel itself.


Our Goal 

In support of this initiative, we will present you with a NextJS menu component that recursively renders the menu data provided by the Drupal API and displays it in the form of a dropdown. This can be considered Part-1 of our “NextJS for Decoupled menus” series. In this blog we will share the setup, components, prefetching and rendering of the menu. In the next part, we will be integrating this with Drupal. 

Decoupled Menus Initiative - NextJS

Deployed on: https://nextjs-decoupled-menus.vercel.app/


A glimpse of the Drupal menu API (data)

The menu APIs provided by Drupal have JSON data stored in a linear structure rather than a hierarchical structure. Therefore, a frontend developer has to construct a tree-like hierarchy from the data to render the menu in a drop-down format. That is what we have focused on currently and have explained in the next section. Given below are the APIs, with the Drupal-menu-hierarchy key for each sub-menu, revealing its hierarchy level.

https://decoupled-menus.jsonapi.dev/system/menu/Drupal-wiki/linkset

https://decoupled-menus.jsonapi.dev/system/menu/main/linkset

What does our NextJS code do – Static Generation

Decoupled Menus Initiative - NextJS

Architectural Diagram of Current Application Structure

The following steps describe the functionality of our existing application –

Step 1: The _app.js is the root file of our application. The Component returned by it is wrapped in our custom Layout component, with page properties passed as children. This file also utilizes the Head Component of NextJS as the header of a page and includes the title and all the meta-tags that are one of the reasons for our 100% SEO.

function MyApp({ Component, pageProps }) {
  return <>
    <Head>
      <title>Decoupled Menus</title>
      <meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
      <meta name="description" content="Demo of decoupled menus in nextjs with great SEO" />
      <meta name="viewport" content="width=device-width, initial-scale=1"></meta>
    </Head>
  </>
<Layout>
      <Component {...pageProps} />
  </Layout>
}

export default MyApp

https://github.com/vertikajain/nextjs-decoupled-menus/blob/main/pages/_app.js 

Decoupled Menus Initiative - NextJS

Get better performance, accessibility, SEO and much more with NextJS

 

Step 2: The index.js file is the Home page of our application. It performs like a logical component because it fetches data from the provided Drupal API using fetch() in getStaticProps() and returns the JSON data as props to _app.js.

const menuId = "drupal-wiki"
export async function getStaticProps() {
  const result = await (await fetch(`https://decoupled-menus.jsonapi.dev/system/menu/${menuId}/linkset`)).json()
  return {
    props: { data: result.linkset[0].item }
  }
}
export default function Home() {
  return 
}

https://github.com/vertikajain/nextjs-decoupled-menus/blob/main/pages/index.js 


Step 3: The components folder, which lies outside the pages folder contains our presentational components - menuBar, Layout and Footer.

Decoupled Menus Initiative - NextJS


Step 4: The Layout component further passes the props-children to the menuBar, which was passed by the _app.js component. It also calls the Footer component in the end.

const Layout = ({ children }) => {
    return <div className="content">
<header>
       <h1>Decoupled Menus Demo using NextJS</h1>
   	</header>
<div className="menuBar">
        <MenuBar data={children.props.data} />
	</div>
        <Footer />
    </div>
}
export default Layout;

https://github.com/vertikajain/nextjs-decoupled-menus/blob/main/components/Layout.js 


Step 5: The menuBar component makes use of the Drupal-API data passed as props to render it as a menu of our page. we have used the menuHelper utility to create a menu Tree and return it to the menuBar.

  • Mapping through the menu data array, we have used the given menu-hierarchy of each menu object to add submenus to the mainRoot as children. 
  • We further iterate through each sub-menu to add further children until the leaf node (last menu item) is reached based on the given hierarchy.
  • This creates our menu Tree in the mainRoot object using Loops.
const util = ((data) => {
    let mainRoot = { children: [] } 
  if (data !== undefined)
   data.map(menuObj => {
 
let hierarchyArr = menuObj['drupal-menu-hierarchy'][0].split(".")
let curr = mainRoot;

 for (let hIdx = 1; hIdx < hierarchyArr.length; hIdx++) {
 let menuId = parseInt(hierarchyArr[hIdx])
        if (curr.children[menuId] === undefined) {
         curr.children[menuId] = { ...menuObj, children: [] } 
         } else {
           curr = curr.children[menuId]
                }
            }
        })
return mainRoot
})
export default util;

https://github.com/VertikaJain/nextjs-decoupled-menus/blob/main/utilities/menuHelper.js 

const menuBar = ({ data }) => {
return <div>
        {/* Main root level - using single map */}
        <TreeView
            className="treeView"
            defaultCollapseIcon={<ExpandMoreIcon />}
            defaultExpandIcon={<ChevronRightIcon />}
        >
            {mainRoot.children.map(child => {
                return createTreeItem(child)
            })}
        </TreeView>
    </div>
}
  • Lastly, the inner root levels are rendered Recursively using the TreeItem API of Material UI in the user-defined method createTreeItem(). This method checks if the menu item is the last item in its particular hierarchy. If yes, then it simply displays the content/link, else it recurses further till the last menu-item is reached.
// Inner roots level - using recursion
const createTreeItem = (menuObj) => {
 // Links for Leaf nodes
 if (menuObj.children.length === 0 || menuObj.children === []) {
  return <TreeItem
   className="treeItem"
   key={menuObj['Drupal-menu-hierarchy'][0]}
   nodeId={menuObj['Drupal-menu-hierarchy'][0]}
   label={<a href={menuObj.href}>- {menuObj.title}</a>}>
       </TreeItem>
    }
    // Recursing through sub-menus
 else
  return <TreeItem
  key={menuObj['Drupal-menu-hierarchy'][0]}
  nodeId={menuObj['Drupal-menu-hierarchy'][0]}
  label={menuObj.title}>
  {menuObj.children.map(child => createTreeItem(child))}
        </TreeItem>
}
export default menuBar;

https://github.com/vertikajain/nextjs-decoupled-menus/blob/main/components/menuBar.js 

Step 6: The Footer component simply displays the Copyright content.

https://github.com/vertikajain/nextjs-decoupled-menus/blob/main/components/Footer.js 


Step 7: The _document.js file enables the material UI icons to be available at the first render, avoiding unusual FOUC.

https://github.com/VertikaJain/nextjs-decoupled-menus/blob/main/pages/_document.js 

 

Conclusion & Future Work

The purpose of this blog was to showcase how one can use the Drupal Menu API with NextJS as the frontend for pre-rendering and how it improves SEO ranking. At the moment this application is only deployed to Vercel, and has not yet been converted into a package. Stay tuned for our upcoming blog that will revolve around integrating our NextJS package with Drupal. Interested to know more about the other Drupal Initiatives? Click here to read more. 

 

Vertika Jain