Skip to main content

Create Dynamic Menus in Storyblok and Gatsby.js

Contents
    Try Storyblok

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

    Hint:

    Please note that this article has already been updated to match Storyblok V2. If you haven’t already started using it, you can find out how to make the switch here.

    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 are in a hurry, check out this demo in boilerplate repo!

    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.

    Setup in Storyblok

    First, we will have to create a new content-type component where our menu items can be stored. Go to {1} Block Library, and then select {2} + New Block.

    app.storyblok.com
    An annotated screenshot of the Block library in Storyblok
    1
    2

    Name this block, config {1} and then choose the Content Type block {2}.

    app.storyblok.com
    Storyblok editing capabilities
    1
    2

    Next, create a new field with the name header_menu {1} and choose the field type Blocks {2}.

    app.storyblok.com
    Storyblok editing capabilities
    1
    2

    We need menu item links to add to our header_menu, so we need to create a new block. This time, choose the block type {2} Nested Block and {1} name it menu_link.

    app.storyblok.com
    Storyblok editing capabilities
    1
    2

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

    app.storyblok.com
    Storyblok editing capabilities
    1
    2

    We also need to add a name for our menu_link so let's add a new field called, name! {1}Type in name into the field. Since the default field type is text {2}, there is no need to change it. Now to officially add it, click on {3} Add.

    app.storyblok.com
    Storyblok editing capabilities
    1
    2
    3

    Next, we need to make sure that only menu_link blocks are allowed to be added to our header_menu block.

    {1} Choose the config block, and {2} select header_menu

    app.storyblok.com
    Storyblok editing capabilities
    1
    2

    Under the {1} Block Field Options heading and {2} select Allow only specific components to be inserted.

    Then, in the Components Whitelist input field, {3} type in menu_link to add to the whitelist.

    app.storyblok.com
    Storyblok editing capabilities
    1
    2
    3

    There's just one more step left in this setup, and that's to create the Content for our Storyblok space. Go to the {1} Content tab, and select {2} + Create new. Then choose {3} Story

    app.storyblok.com
    Storyblok editing capabilities
    1
    2
    3

    Here, we want to create a new story with the name {1} Config, using our recently created content type {2} Config.

    app.storyblok.com
    Storyblok editing capabilities
    1
    2

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

    app.storyblok.com
    Storyblok editing capabilities
    1
    2

    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.

    api.storyblok.com/v2/cdn/stories/config?version=draft
    Storyblok editing capabilities
    1
    2

    Config content-type story, Draft JSON

    api.storyblok.com/v2/cdn/stories/config?version=draft
    Storyblok editing capabilities
    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 { StoryblokComponent, storyblokEditable, useStoryblokState } from "gatsby-source-storyblok"
    
    import Layout from "../components/layout"
    
    export default function Page({ data }) {
      let story = data.storyblokEntry
      story = useStoryblokState(story)
    
      const Templates = () => {
        if (story.content.component === 'page') {
          return story.content.body.map(blok => <StoryblokComponent blok={blok} key={blok._uid} />)
        }
        return (story.content.component !== 'page' ? <StoryblokComponent blok={story.content} key={story.content._uid} /> : null)
      }
    
      return (
        <Layout>
          <div {...storyblokEditable(story.content)}>
            <Templates blok={story.content} key={story.content._uid} />
          </div>
        </Layout>
      )
    }
    
    export const query = graphql`
      query ($full_slug: String!) {
        storyblokEntry(full_slug: { eq: $full_slug }) {
          content
          name
          full_slug
          uuid
          id
          internalId
        }
      }
    `

    HINT:

    Line 13 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.

    app.storyblok.com
    Storyblok editing capabilities

    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

    Developer Newsletter

    Want to stay on top of the latest news and updates in Storyblok?
    Subscribe to Code & Bloks - our headless newsletter.

    An error occurred. Please get in touch with marketing@storyblok.com

    Please select at least one option.

    Please enter a valid email address.

    This email address is already registered.

    Please Check Your Email!

    Almost there! To confirm your subscription, please click on the link in the email we’ve just sent you. If you didn’t receive the email check your ’junk folder’ or