Almost EVERYONE who tried headless systems said they saw benefits. Download the state of CMS now!

Storyblok now on AWS Marketplace: Read more

O’Reilly Report: Decoupled Applications and Composable Web Architectures - Download Now

Empower your teams & get a 582% ROI: See Storyblok's CMS in action

Skip to main content

Create Dynamic Menus in Storyblok and Gatsby.js

Try Storyblok

Storyblok is the first headless CMS that works for developers & marketers alike.

INFO:

On May 13th, 2024, Storyblok started gradually rolling out a new design for its Visual Editor. Therefore, the Visual Editor product screenshots depicted in this resource may not match what you encounter in the Storyblok App. For more information and a detailed reference, please consult this FAQ on the new Visual Editor design.

Hint:

If you'd like to build a multilanguage website with Gatsby, please follow this tutorial instead. If you'd like to build a website without internationalization logic, please keep following the rest of this article and the Ultimate Tutorial articles.

In this part of the tutorial series, we'll make the menu in our header dynamic, so that can manage it directly through Storyblok!

Hint:

If you’re in a hurry, have a look at our Ultimate Tutorial GitHub repo part 3 branch!

Section titled Requirements Requirements

This tutorial is part 3 of the Ultimate Tutorial Series for Gatsby.js! We recommend that you follow the previous tutorials before starting this one.

Section titled Setup in Storyblok Setup in Storyblok

First, we will have to create a new content type component wherein our menu entries can be stored. In order to do that, go to the Block Library {1} and create a New block {2}.

Creating a new block in the Block Library
1
2

Creating a new block in the Block Library

Enter the name config {1} and choose Content type block {2}.

Creating a content type block
1
2

Creating a content type block

Now you can create a new field with the name header_menu {1} and choose the field type Blocks {2}.

Creating a field for the header menu
1
2

Creating a field for the header menu

In this field, we would like to provide the possibility to add menu links as nested blocks. To accomplish that, let’s create another new block. This time it should be a Nested block {1} with the name menu_link {2}.

Creating a nested block
1
2

Creating a nested block

Now we can add a new field called link {1} in this newly created block and choose Link as the field type {2}.

Creating a link field
1
2

Creating a link field

Alright, our component schemas are almost done! Just one more step: to avoid that just any block could be nested in our header_menu, we want to make sure that only specific components can be inserted {1}. Now you can choose the menu_link block in the whitelist {2}.

Allowing only specific components to be inserted
1
2

Allowing only specific components to be inserted

With that out of the way, we can now go to the Content section of our Storyblok space. Here, we want to create a new story with the name Config {2}, using our recently created content type Config {3}.

Creating a new Config story
1
2

Creating a new Config story

If you open this newly created Config story, you can now nest as many menu_link blocks in the header_menu field as you would like. For now, let’s add our Blog and About page.

menu link
1
2

Section titled Rendering the Menu in Gatsby.js Rendering the Menu in Gatsby.js

Now, let's create the code that will render our menu in the frontend of our application. First, let’s review what our imports from the Storyblok Gatsby SDK -- storyblokEditable and StoryblokComponent-- do:

  • storyblokEditable makes our components editable in our Real-Time Visual Editor.
  • StoryblokComponent  sets up our page for our Storyblok components.

Then, let's set up our components: config.js, and menuLink.js to match with our blocks created in Storyblok.

Let's start from config.js file.

config.js
        
      import * as React from "react"
import { storyblokEditable, StoryblokComponent } from "gatsby-source-storyblok"
import { Link } from "gatsby"

const Config = ({ blok }) => {
  return (
    <div className="relative bg-white border-b-2 border-gray-100" {...storyblokEditable(blok)}>
      <div className="max-w-7xl mx-auto px-4 sm:px-6">
        <div className="flex justify-between items-center  py-6 md:justify-start md:space-x-10">
          <div className="flex justify-start lg:w-0 lg:flex-1">
            <Link to="/">
              <img
                className="h-20 w-auto sm:h-10"
                src='storyblok-primary.png'
                alt=""
              />
            </Link>
          </div>
          {blok.header_menu.map((nestedBlok) => (
            <StoryblokComponent className='' blok={nestedBlok} key={nestedBlok._uid} />
          ))}
        </div>
      </div>
    </div>
  )
}
export default Config
    

First, we'll make sure that the Config content-type story will be generated through File System Route API from Gatsby.

After creating config.js file, let's modify {storyblokEntry.full_slug}.js file because Config content-type and Page content-type have different structures from draft/published JSON.

1
2

Config content-type story, Draft JSON

1
2
3

Page content-type story, Draft JSON

We'll conditionally render different data paths to render different content-type stories in {storyblokEntry.full_slug}.js file.

{storyblokEntry.full_slug}.js
        
      import * as React from "react"
import { graphql } from "gatsby"

import { StoryblokStory } from "gatsby-source-storyblok"

import Layout from "../components/layout"

const IndexPage = ({ data }) => {
  if (typeof data.storyblokEntry.content === "string") data.storyblokEntry.content = JSON.parse(data.storyblokEntry.content);
  console.log(data.storyblokEntry.content)

  const Templates = () => {
    if (data.storyblokEntry.content.component === "page") {
      return <StoryblokStory story={data.storyblokEntry}/>
    } 
      return (data.storyblokEntry.content.component !== "page" ? <StoryblokStory story={data.storyblokEntry.content} blok={data.storyblokEntry.content}/> : null)
  }


  return (
    <Layout>
      <Templates />
    </Layout>
  )
}

export default IndexPage

export const query = graphql`
  query ($full_slug: String) {
    storyblokEntry(full_slug: { eq: $full_slug}) {
      content
      name
      full_slug
      uuid
      id
      internalId
    }
  }
`
    
HINT:

Line 14 and 16 are conditionally filtering the names of the content-type. Page content-type returns dynamic components by mapping inside of the body . On the other hand, Config content-type returns dynamic components without body .

Next up, we'll create menuLink.js file.

menuLink.js
        
      import * as React from "react"
import { storyblokEditable } from "gatsby-source-storyblok"
import { Link } from "gatsby"

const MenuLink = ({ blok }) => (
  <Link to={blok.link.url} {...storyblokEditable(blok)} className="text-base font-medium text-gray-500 hover:text-gray-900">
    {blok.name}
  </Link>
)

export default MenuLink
    

Let’s make sure those components render. In layout.js, add your components:

layout.js
        
      // ...
import Config from "./config"
import MenuLink from "./menuLink"

const components = {
  // ...
  config: Config,
  "menu_link": MenuLink,
}
    

In the previous tutorial (Render Storyblok Stories Dynamically in Gatsby), we didn't make header navigation to be dynamic. Now, we have Config content-type component and Menu Link nested component ready. We can update navigation.js file to dynamically render header navigation items.

navigation.js
        
      import * as React from "react"
import { useState } from "react"
import { useStaticQuery, graphql, Link } from "gatsby"

const Navigation = () => {
  const { config } = useStaticQuery(graphql`
    {
      config: allStoryblokEntry(filter: {field_component: {eq: "config"}}) {
        edges {
          node {
            name
            uuid
            content
          }
        }
      }
    }
  `)

  const [openMenu, setOpenMenu] = useState(false);

  let thisConfig = config.edges.filter(({ node }) => node.uuid)
  let configContent = thisConfig.length ? JSON.parse(thisConfig[0].node.content) : {}
  let menu = configContent.header_menu.map(menu => menu.link.cached_url.split(','))

  const Nav = () => menu.map(nav => <Link to={nav} key={nav}>{nav}</Link>)

  return (
    <div className="relative bg-white border-b-2 border-gray-100">
      <div className="max-w-7xl mx-auto px-4 sm:px-6">
        <div className="flex justify-between items-center  py-6 md:justify-start md:space-x-10">
          <div className="flex justify-start lg:w-0 lg:flex-1">
            <Link to="/">
              <a>
                <span className="sr-only">Storyblok</span>
                <img
                  className="h-20 w-auto sm:h-10 hidden sm:block"
                  src='https://a.storyblok.com/f/88751/251x53/0d3909fe96/storyblok-primary.png'
                  alt="Storyblok"
                />
                <img
                  className="h-20 w-auto sm:h-10 sm:hidden"
                  src='https://a.storyblok.com/f/88751/92x106/835caf912a/storyblok-logo.png'
                  alt="Storyblok"
                />
              </a>
            </Link>
          </div>
          <div className="-mr-2 -my-2 md:hidden">
            <button
              type="button"
              onClick={() => setOpenMenu(true)}
              className="bg-white rounded-md p-2 inline-flex items-center justify-center text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
              aria-expanded="false"
            >
              <span className="sr-only">Open menu</span>
              {/* <!-- Heroicon name: outline/menu --> */}
              <svg
                className="h-6 w-6"
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
                aria-hidden="true"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                  d="M4 6h16M4 12h16M4 18h16"
                />
              </svg>
            </button>
          </div>
          <div className="hidden md:flex items-center justify-end md:flex-1 lg:w-0 space-x-10">
            <Nav menu={menu} className="text-base font-medium text-gray-500 hover:text-gray-900" />
          </div>
        </div>
      </div>

      {/* <!--
      Mobile menu, show/hide based on mobile menu state.
    --> */}
      {openMenu && (
        <div className="absolute top-0 inset-x-0 p-2 transition transform origin-top-right md:hidden">
          <div className="rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 bg-white divide-y-2 divide-gray-50">
            <div className="pt-5 pb-6 px-5">
              <div className="flex items-center justify-between">
                <div>
                  <img
                    className="h-8 w-auto"
                    src="https://a.storyblok.com/f/88751/92x106/835caf912a/storyblok-logo.png"
                    alt="Storyblok"
                  />
                </div>
                <div className="-mr-2">
                  <button
                    type="button"
                    onClick={() => setOpenMenu(false)}
                    className="bg-white rounded-md p-2 inline-flex items-center justify-center text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
                  >
                    <span className="sr-only">Close menu</span>
                    {/* <!-- Heroicon name: outline/x --> */}
                    <svg
                      className="h-6 w-6"
                      xmlns="http://www.w3.org/2000/svg"
                      fill="none"
                      viewBox="0 0 24 24"
                      stroke="currentColor"
                      aria-hidden="true"
                    >
                      <path
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        strokeWidth="2"
                        d="M6 18L18 6M6 6l12 12"
                      />
                    </svg>
                  </button>
                </div>
              </div>
              <div className="mt-6">
                <nav className="grid gap-y-8">
                  <Nav menu={menu} className="-m-3 p-3 flex items-center rounded-md hover:bg-gray-50" />
                </nav>
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

export default Navigation;
    

Now, if you go back to your Storyblok Visual Editor, you should be able to see your menu being rendered! You can add more links, remove them, or even reorder them if you like.

Section titled Wrapping Up Wrapping Up

Congratulations, you have successfully created a dynamic menu in Storyblok and Gatsby.js!

Next Part:

Continue reading and Create Custom Components in Storyblok and Gatsby.js

Author

Patrick Odey

Patrick Odey

Patrick is a Software Engineer with a passion for web technologies, Cloud computing and community building. He is from Nigeria and works as a Developer Relations Engineer at Storyblok. He is also a skateboarder.