Let's build a restaurant website with a modern front end and use a headless content management system (CMS) to keep the menu up to date.

See the source code for the finished result.

For the website we will use Gatsby. Gatsby is a web framework that uses React as its front end and GraphQL for its data layer.

The CMS will be Contentful. It is a headless CMS with a GraphQL API, so it works very well with Gatsby.

Step 1: Bootstrap the Website

Start a new project with the Gatsby CLI.

npx gatsby new restaurant-website
cd restaurant-website
yarn develop

Our content structure will be a list of menus. Each menu can have multiple sections. Each section can have multiple items and one image.

Pop open your favorite code editor and create the mock data in a JSON file.

// mock-data.json
[
  {
    "name": "Dinner",
    "sections": [
      {
        "name": "Small Plates",
        "imageAlt": "ricotta & spinach gnocchi",
        "image": "https://images.unsplash.com/photo-1479832912902-77089f02b1d2?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1650&q=80",
        "items": [
          {
            "name": "shrimp pil pil",
            "description": "sizzling shrimp, butter, garlic, chile flake",
            "price": 13
          },
          {
            "name": "ricotta & spinach gnocchi",
            "description": "brown butter, parmesan",
            "price": 8
          }
        ]
      },
      {
        "name": "Mains",
        "imageAlt": "Baja Taco",
        "image": "https://images.unsplash.com/photo-1568106690101-fd6822e876f6?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1780&q=80",
        "items": [
          {
            "name": "baja taco",
            "description": "hand battered/fried cod, provolone, chile aioli, tomatillo, shredded cabbage, queso cotija",
            "price": 7
          },
          {
            "name": "bucatini & meatballs",
            "description": "w/ tomato butter",
            "price": 11
          }
        ]
      }
    ]
  }
]

Now let's put that content into React components. All the components will sit on the same index.js file for brevity. In real life, you'd probably split each out into a separate file.

We'll have three components other than the main page component in this file: Menu, MenuSection, MenuItem. We'll use some basic styling so it's somewhat presentable.

// src/pages/index.js
import React from "react"
import Layout from "../components/layout"

import menus from "../../mock-data.json"

const MenuItem = ({ name, description, price }) => (
  <div
    style={{ display: "flex", justifyContent: "space-bewteen", width: "100%" }}
  >
    <div style={{ flexGrow: 1 }}>
      <h4 style={{ marginBottom: 0 }}>{name}</h4>
      <p>{description}</p>
    </div>
    <p>{price}</p>
  </div>
)

const MenuSection = ({ name, items, image, imageAlt }) => (
  <div style={{ marginBottom: "4rem" }}>
    <h3>{name}</h3>
    <img src={image} alt={imageAlt} />
    {items.map(item => (
      <MenuItem
      key={item.name}
      name={item.name}
      price={item.price}
      description={item.description}
      />
    ))}
  </div>
)

const Menu = ({ name, sections }) => (
  <div style={{ marginBottom: "4rem" }}>
    <h2>{name}</h2>
    {sections.map(s => (
      <MenuSection
      key={s.name}
      name={s.name}
      items={s.items}
      image={s.image}
      imageAlt={s.imageAlt}
      />
    ))}
  </div>
)

const IndexPage = () => (
  <Layout>
    <div style={{ maxWidth: "28rem", margin: "0 auto" }}>
      <h1 style={{ marginBottom: "4rem" }}>Menus</h1>
      {menus.map(m => (
        <Menu key={m.key} name={m.name} sections={m.sections} />
      ))}
    </div>
  </Layout>
)

export default IndexPage

And with that, we've layed out our structured content.

The restaurant owner is not going to want to manage content in a JSON file and commit to git. Let's make the move from JSON to Contentful.

Contentful

You can create a free Contentful account if you don't have one. Contentful's onboarding screens may change but you will want to get to a blank "Space".

As of this writing you can create a free space on the free tier and keep the "Create an empty space" option.

Contentful Empty Space


Contentful Space Details

If you've arrived at a blank space through that path or any other path you should see a recommendation to create a new content type.

Contentful Space Home

We'll add a content type for each object type in our mock JSON content. We will need 3 in total with the following structure:

  • Menu
    • Name
    • List of Menu Sections
  • Menu Section
    • Name
    • Image
    • Image Alt
    • List of Menu Items
  • Menu Item
    • Name
    • Description
    • Price

We'll run through creating the Menu in detail with screenshots but then reference the list above to determine what options to use in Contentful.

First Content Type

It's efficient to go bottom-up and create the Menu Item first because the Menu Section has Menu Items as a field, and Menu has Menu Sections as a field. By starting with Menu Items we can create all content models in one pass.

Fill out the info for the Menu Item.

Contentful Create Content Type

Once we have a content type we can start adding fields. We'll create the "Name" field first.

Contentful Create Content Field

Give it the type "Text".

Contentful Create Content Field

Set the "Name" field to "Name". Then select "Create and configure" there are a few more settings we want to tweak for this field.

Contentful Create Content Field 2

Check the field option for "This field represents the Entry title. This sets the name field as the label in the Contentful editor.

Contentful Create Content Field 3 Options

Select the Validations tab in the top right and check off "Required" and "Unique". Because it doesn't make sense to have a menu item with no name, or to have two menu items with the same name. Save with those settings.

Contentful Create Content Field 4 Options

I won't run through any more fields in detail since that'd be a ton of screenshots but go ahead and create the other two Menu Item fields as follows:

  • Description
    • Name: Description
    • Type: Text, Short Text
  • Price
    • Name: Price
    • Type: Number, Decimal, Required

One down, two to go

Second Content Type

Now the Menu Section. Create the type as the Menu Item and add the following fields:

  • Name
    • Name: Name
    • Type: Text, Short Text, Required, Unique, Entry title
  • Image
    • Name: Image
    • Type: Media, One File, Required, Accept Only Image File Types
  • Image Alt
    • Name: Image Alt
    • Type: Text, Short Text, Required
  • List of Menu Items
    • Name: Items
    • Type: Reference, Many references, Required, Accept Only MenuItem

Okay, the list of Menu Items is a little different. Let's go into a little more detail.

Adding References

References are how you link your custom content types together. Just how with our JSON object we had Menu Items nested inside of Menu Sections, not just primitive types like numbers and strings. References are how to do that in Contentful.

First step is to choose the reference type.

Contentful Create Content Reference Field

And make sure it takes many references.

Contentful Create Content Reference Field 2

And then make it required and force all the referenced items to be MenuItems. You wouldn't want to reference other menu sections or other menus in the items field. Save it with those settings.

Contentful Create Content Reference Field 4

Final Content Type

Our final type "Menu" has only two fields:

  • Name
    • Name: Name
    • Type: Text, Short Text, Required, Unique, Entry title
  • List of Menu Sections
    • Name: Sections
    • Type: Reference, Many references, Required, Accept Only MenuSection

Adding Content

Now we need to add actual content. Jump over to the content tab. This is where the fun begins according to Contentful, okay.

Contentful Create Content

Put all four menu items from our original JSON file in the Menu Item content type. With the content model set up correctly this part is pretty smooth.

Contentful Create Content 2

Once you have all four you should see this in the main content area.

Contentful Create Content 3

Now we add our menu sections. For the images you can download them from the links in the mock JSON content or you can peruse Unsplash for other open license images.

The small plates menu section should look something like this when you're done.

Contentful Create Content 4

Add the mains section in the same way. Then finally add the dinner menu.

Contentful Create Content 5

You should have four menu items, two menu sections, and one menu.

Contentful Create Content 6

More work than a JSON file isn't it? But not nearly as painful as content management often is!

Linking Gatsby and Contentful

Okay, this is really the fun part.

Gatsby Source Plugins

Source plugins make it easy to grab data from various sources such as the filesystem, databases or third-party services. In our case, Contentful is our third-party CMS service and Gatsby has a plugin for it. It saves us from writing API requests by hand. We just need to provide it our Space ID and an API key. Let's install it.

yarn add gatsby-source-contentful

To use the plugin we need to add it to the gatsby-config.js file and give it our Contentful settings. Your Contentful spaceId and accessToken can be found on Contentful on the top navigation Settings > API keys. You can use the example API key they provide or create a new one.

// gatsby-config.js
[
    {
      resolve: `gatsby-source-contentful`,
      options: {
        spaceId: `your_space_id`,
        accessToken: 'your_access_token'
      },
    }
    // .. other plugins
]

Remember your access token is a secret so you don't want to commit it to source control. Follow the plugin instructions for an example of keeping it safe.

Use GraphQL to Access Content

In Gatsby, all website data and metadata is accessed through a GraphQL query. Since we've setup a source plugin for Contentful in the last step, we will see Contentful data coming through the Gatsby GraphQL layer.

Make sure your development instance is still running, if it's not run yarn develop again. The Gatsby develop command automatically creates a Graphiql instance for us to write GraphQL queries with a visual editor. It runs on http://localhost:8000/___graphql.

Throw this query into the Graphiql editor and see what data comes back.

query ALL_CONTENTFUL_MENU {
  allContentfulMenu {
    nodes {
      name
      sections {
        altText
        items {
          name
          description
          price
        }
        image {
          fluid(maxWidth: 1000) {
            src
          }
        }
      }
    }
  }
}

Play around with the left-hand side pane and see what other fields are available - there're tons of metadata that Contentful provides.

Create a React Hook to Isolate Content Access

Now that we know how to query the data, let's write a React hook that brings that result in the context of our Gatsby site. Create a file at src/hooks/useMenus.js.

We will create a simple wrapper for that GraphQL query that can be used by any part of the website.

// src/hooks/useMenus.js
import { graphql, useStaticQuery } from "gatsby"

export default () => {
  const { allContentfulMenu } = useStaticQuery(
  graphql`
    query ALL_CONTENTFUL_MENU {
      allContentfulMenu {
        nodes {
          name
          sections {
            altText
            items {
              name
              description
              price
            }
            image {
              fluid(maxWidth: 1000) {
                src
              }
            }
          }
        }
      }
    }
  `
  )
  return allContentfulMenu.nodes
}

Now we modify our src/pages/index.js file to use the hook instead of the mock content.

// src/pages/index.js
import React from "react"
import Layout from "../components/layout"

import useMenus from "../hooks/useMenus"

const MenuItem = ({ name, description, price }) => (
  <div
    style={{ display: "flex", justifyContent: "space-bewteen", width: "100%" }}
  >
    <div style={{ flexGrow: 1 }}>
      <h4 style={{ marginBottom: 0 }}>{name}</h4>
      <p>{description}</p>
    </div>
    <p>{price}</p>
  </div>
)

const MenuSection = ({ name, items, image, imageAlt }) => (
  <div style={{ marginBottom: "4rem" }}>
    <h3>{name}</h3>
    <img src={image} alt={imageAlt} />
    {items.map(item => (
      <MenuItem
        key={item.name}
        name={item.name}
        price={item.price}
        description={item.description}
      />
    ))}
  </div>
)

const Menu = ({ name, sections }) => (
  <div style={{ marginBottom: "4rem" }}>
    <h2>{name}</h2>
    {sections.map(s => (
      <MenuSection
        key={s.name}
        name={s.name}
        items={s.items}
        image={s.image.fluid.src}
        imageAlt={s.imageAlt}
      />
    ))}
  </div>
)

const IndexPage = () => {
  const menus = useMenus()
  return (
    <Layout>
      <div style={{ maxWidth: "28rem", margin: "0 auto" }}>
        <h1 style={{ marginBottom: "4rem" }}>Menus</h1>
        {menus.map(m => (
          <Menu key={m.key} name={m.name} sections={m.sections} />
        ))}
      </div>
    </Layout>
  )
}

export default IndexPage

Note that the image usage also changed slightly to handle the way the image object changed from a straight source URL to an object with a fluid.src property.

I like to create hooks for GraphQL queries. It provies a nice seam in the layers of code. Components get to remain dumb just accepting props. The top-level page component makes a single call to a hook to get all the content needed. You could have fetched the Contentful data with a page query too.

Fin

And we're done! See the source code for the finished result and try tinkering around with Contentful and Gatsby.

Resources