logo
CommunityResearch Program

Resources

BlogForum
Back to blog

March 22, 2024

Building a Restaurant Guide with Next.js and Storyblok
byChakit from StoryblokinCommunity

In this tutorial, we will explore how to integrate Next.js with Storyblok along with learning some Next.js and Headless features. We will also see how Storyblok works and enable the live preview of the Storyblok’s Visual Editor. To make things interesting, we will build a Restaurant Guide with the same. Here is a look of what we will be building –

Hint - If you're in a hurry, you can check out the repo at Github. You can also clone the space by clicking here. 

Requirements

To follow this tutorial there are the following requirements:

  • Basic understanding of JavaScript, React and Next.js
  • Node.js LTS version
  • An account in the Storyblok App

Creating a Next.js Project

Let’s first create a Next.js project by the following command – 

npx create-next-app@latest

Feel free to choose your preferences and make sure to add TailwindCSS to the project as the code of this tutorial contains tailwind for styling. Let’s also install the Storyblok React SDK with the following command –

npm i @storyblok/react

The SDK will allow us to interact with the API and enable live editing for us. Let’s now start the development server –

npm run dev

Once the server is started, open your browser at http://localhost:3000. You should see the following screen – 

Due the securty reasons, http is not supported inside the Visual Editor of Storyblok, so we need to set up a proxy from an https port to http 3000. You can read more about it in setting up a proxy guide. We will be using 3010 port on https.

Hint - Checkout Storyblok’s Kickstart page to see in detail how to connect Storyblok with any technology, both in a new and existing projects.

Creating a Space in Storyblok

Now, let’s login into the Storyblok App. Once logged in, you will see a list of your spaces. Every card is a space here. 

You can consider a space as a content repository, that contains everything like content, users, components, etc. For this tutorial, let’s consider one space as one project. You can read more about spaces in the Structure of Content guide present in the Storyblok website. Let’s now create a new space for our Next.js project by clicking the button present at the right top that says Add Space. Let’s choose creating a New Space from the list of options. There are a couple of plans available to choose from when you create a space. For this tutorial, feel free to choose the Community Plan which is free and requires no payment information.

Then, we can enter the Space name and Server location.

Setting up the Space

Once the space is created, you will see that there is a Home Story created inside the Content section and a couple of blocks (or components) in the Block Library section. We will be creating more stories and blocks according to our requirements of the app. As Storyblok is a headless CMS, you can create stories that are built of different blocks and consume the content inside the stories with the help of APIs on any frontend layer (in this case Next.js)

Hint - You can read more about a Story in the Structure of Content guide, it is basically an entry.

Let’s go to the settings of the space, and then Visual Editor tab. This is the place where you set up the location of the Visual Editor. Let’s change it to https://localhost:3010/ as this is our dev server after the proxy.

Now let’s go to the home story present inside the Content section, you will see that now we have a our Next.js website there. Though there is a 404, but the aim was to setup the Visual Editor Environment.

Integrating Next.js and Storyblok

Now, let’s add some code to our Next.js project to have the integration with Storyblok working and adding the functionality of live editing. Let’s first delete the page.js file inside the app folder. Then let’s create a [[..slug]] folder and a page.js file inside that. We are doing this to catch all the dynamic routes possible. You can read more about Dynamic Routes in official Next.js docs.

We will add the code to render every route inside this page.js file, let’s first initialize Storyblok. To do so, we need to go to the layout.js file and add storyblokInit function to it. You can add the following –


import { storyblokInit, apiPlugin} from "@storyblok/react/rsc" 

// Existing Code

storyblokInit({
  accessToken: 'your-access-token',
  use: [apiPlugin]
})

We are importing storyblokInit and apiPlugin from the package, and are initializing the function. You can find the access token of your space inside the settings of the space. Make sure to copy the preview one. You can create more tokens according to the requirements.

apiPlugin helps us to use the functionality of Storyblok JS Client that allows us to interact with the Storyblok API using the SDK. If you don’t want to add it, you can choose your preferred way to fetch the content by making a get request to the Storyblok’s Content Delivery API.

Once this is done, we also need to add a special wrapper component that will allows us to do the edits in real time. As Next.js by default is doing everything on the Server Side with React Server Components, it is not possible to play around with Javascript and listen to such events. Hence, this component will be a Client Side Component. 

Create a components folder and then create a new file named StoryblokProvider.js in the components folder. You can paste the following code in that – 


/** 1. Tag it as client component */
"use client";
import { storyblokInit, apiPlugin } from "@storyblok/react/rsc";

/** 2. Import your components */
// import Page from "./Page"

/** 3. Initialize it as usual */
storyblokInit({
  accessToken: "your-access-token",
  use: [apiPlugin],
  components: {},
});

export default function StoryblokProvider({ children }) {
  return children;
}

Here we do the initalization again on the client side, the previous initialization in the layout will be used to fetch the data. Here, we also have a components key inside the initialization, which will contain all the map of all the components we create for dynamic rendering.

We need to wrap the app inside this component. The layout.js file should now look like this – 


import { storyblokInit, apiPlugin} from "@storyblok/react/rsc"
import StoryblokProvider from "@/components/StoryblokProvider"
import './globals.css';
export const metadata = {
  title: 'Storyblok and Next.js 14',
  description: 'A Next.js and Storyblok app using app router ',
}
storyblokInit({
  accessToken: 'your-access-token',
  use: [apiPlugin]
})
export default function RootLayout({ children }) {
  return (
    <StoryblokProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </StoryblokProvider>
  )
}

Now let’s add the logic to the page.js file to render all the routes. Paste the following code inside the file – 


import { getStoryblokApi } from "@storyblok/react/rsc";
import StoryblokStory from "@storyblok/react/story";

export default async function Page({ params }) {
  let slug = params.slug ? params.slug.join("/") : "home";

  const storyblokApi = getStoryblokApi();
  let { data } = await storyblokApi.get(
    `cdn/stories/${slug}`,
    { version: "draft" },
    { cache: "no-store" }
  );

  return (
    <div className="mb-40">
      <StoryblokStory story={data.story} bridgeOptions={{}} />
    </div>
  );
}

export async function generateStaticParams() {

  const storyblokApi = getStoryblokApi();
  let { data } = await storyblokApi.get("cdn/links/", {
    version: "draft",
  });

  let paths = [];

  Object.keys(data.links).forEach((linkKey) => {
    if (data.links[linkKey].is_folder) {
      return;
    }
    const slug = data.links[linkKey].slug;
    if (slug == "home") {
      return;
    }
    let splittedSlug = slug.split("/");
    paths.push({ slug: splittedSlug });
  });
  return paths;
}

In this file, we import getStoryblokApi that is used to fetch the data from Storyblok. First thing to see here is the generateStaticParams function here provided by Next.js. This function is used to define the dynamic routes that should statically generate during build time. This is pretty useful for static generation. You can read more about the function in the Next.js Docs.

In this function, we fetch all the links that are inside Storyblok. This means we get all the entries. We are using the links endpoint of the API which gives us the access to all the links so we don’t have to fetch the complete stories. You can take a look at the links endpoing in the API reference. We are ignoring the home slug here, as it will be used for all the other routes along with it. 

Once we have all the links we return the paths array as required by generateStaticParams function in the required format. Once we have all the links, the Page function can handle the logic for rendering it. Inside the Page function, we first retrieve the slug and fetch the content of that slug using the API. With the fetch, the first parameter is the slug, second one is the API Options where we define the version in this case (there are more options that can be passed), and the third parameter is to pass custom fetch options. 

Storyblok stores two versions of content, one is draft and the other is published. By default, when you create something or make changes, the new content is stored as draft until you hit publish. 

Hint - Only Preview token can fetch the draft as well as the published version of content. Public token can only fetch the published version.

We also pass the third parameter as { cache: "no-store" } for Next.js, so that the request is not cached all the time when the content changes.

After we get the data, we use the StoryblokStory  component to render the components received in the content dynamically. We also need to pass the story property which will contain the content of the component.  The other property is the bridgeOptions, which takes the options for the bridge. 

Now with this all done, we are ready to create the new stories and components! 

Creating Stories and Components

Let’s start by creating a Restaurant component first. This will help us understand the basics, and then we can add multiple fields as well as components. Go to the Block Library in Storyblok, you will see that there are already a few blocks created. We won’t be using these, so let’s create a new one by clicking the button on right top that says Create New . Let’s add the name restaurant for this, and choose Content type block. 

There are 3 different types of components in Storyblok, Content type blocks are the ones that can be standalone stories. The Nestable ones are the ones that can be added in other blocks. And, Universal blocks can be used as both.

Now once the component is added, we need to add a couple of fields to it. Let’s start by adding a name field and a background image field. The pre selected field type is text, but you can change it while creating the field or after the field is created. For the background_image, select (or change to) the type asset.

Once the component is created, we can use this to create new Restaurant stories to store the content related to a restaurant. Let’s go the Content section, and add a new folder named Restaurants. This folder will store all the restaurants. Select the default content type as Restaurant

Once the folder is created, add a new story inside it for a restaurant. Feel free to choose the name of the restaurant. 

Once the story is created, you will see the fields that we added in our component. 

The fields are empty, as well as our frontend is not showing anything at this moment. Add the content to the fields and then let’s hit the save button on the right top. Even when we hit save, nothing is shown in the Visual Editor. This is because we are missing one important step – creation of component on the frontend. You should also see a warning in the terminal saying that the component doesn’t exist.

Let’s now create a Restaurant component in our Next.js project. Create a new file name Restaurant.js  inside the components folder and paste the following code –


import { storyblokEditable } from "@storyblok/react/rsc";

const Restaurant = ({ blok }) => (
  <main {...storyblokEditable(blok)}>
    <div>
      <div
        className={`min-h-[500px] relative
          flex
          items-end
          justify-center
          p-9
          my-6
          rounded-[5px]
          overflow-hidden`}
      >
        <div className="relative z-10 text-center">
          <h1 className="text-7xl text-white font-bold mb-3">{blok.name}</h1>
        </div>
        <img
          src={`${blok.background_image?.filename}`}
          alt={blok.background_image.alt}
          className="absolute top-0 left-0 z-0 w-full h-full object-cover"
        />
      </div>
    </div>
  </main>
);
export default Restaurant;

In this component, we are getting a property named blok that we passed from the StoryblokStory as the story property. This blok property will contain all the information coming from the story. In this case, it will have the name we added along with the background_image. We can directly access those properties from the blok object. For the name, it is blok.name and for the image it is blok.background_image?.filename. This is because asset field gives you another object which contains other information along with image’s url under the property name filename.

The components where live editing is required must have the StoryblokEditable function on the root element as we have it here on the main tag. This allows us to see the dotted lines inside the visual editor and makes the components clickable.

Once we save this component, we also need to add this to the initalization in the StoryblokProvider.js file – 


/** 1. Tag it as client component */
"use client";
import { storyblokInit, apiPlugin } from "@storyblok/react/rsc";

/** 2. Import your components */
import Restaurant from "./Restaurant";

/** 3. Initialize it as usual */
storyblokInit({
  accessToken: "your-access-token",
  use: [apiPlugin],
  components: {
    'restaurant': Restaurant
  },
});
export default function StoryblokProvider({ children }) {
  return children;
}

As soon as we save this, we should see the preview in the Visual Editor! Now, if you try to change the text or the image, you will even see a live preview in real time. Congratulations, we now have everything working for us. Now it is all about creating new fields and new components. 

Let’s now add the following fields (along with the types) to the Restaurant block – 

  • description – Textarea 
  • cuisines – Multi-Options
  • vegan – Boolean
  • city – Text
  • dishes – Blocks

You can add more fields of your choice, once the fields are added we just need to map those in the frontend to be used by our Restaurant component. 

For the cuisines field that we added, you can add options of your choice.

The dishes field that we added is of type blocks. Blocks is a special type that allows other nestable components to be inserted inside it. As it is recommened to be as granular as possible, we can make another component named dish that can be added inside that field.

Let’s add a new nestable block named dish with the following fields – 

  • name – Text
  • description – Textarea
  • image – Asset

Let’s now go to the newly added restaurant and fill the other fields along with adding a dish block there. 

Now to make all these fields visible, we need to add those in our Restaurant component in Next.js and we also need to add a Dish component in our frontned. Paste the following code in a new file named Dish.js  under the components folder –


import { storyblokEditable } from "@storyblok/react/rsc";
const Dish = ({ dish }) => {
  return (
    <section {...storyblokEditable(dish)}>
      <div className="mx-auto flex w-80 flex-col justify-center bg-white rounded-2xl shadow-xl shadow-gray-400/20">
        <img
          className="aspect-video w-80 rounded-t-2xl object-cover object-center"
          src={dish.image.filename}
        />
        <div className="p-6">
          {/* <small className="text-gray-900 text-xs">
            Rating - {dish.rating.value}
          </small> */}
          <h1 className="text-2xl font-medium text-gray-700 pb-2">
            {dish.name}
          </h1>
          <p className="text text-gray-500 leading-6">{dish.description}</p>
        </div>
      </div>
    </section>
  );
};
export default Dish;

Make sure to add this to the storyblokInit function as we added the Restaurant component. Paste the following code in the Restuarant.js file – 




    import { storyblokEditable } from "@storyblok/react/rsc";
    import Dish from "./Dish";
    const Restaurant = ({ blok }) => (
      <main {...storyblokEditable(blok)}>
        <div>
          <div
            className={`min-h-[500px]
              relative
              flex
              items-end
              justify-center
              p-9
              my-6
              rounded-[5px]
              overflow-hidden`}
          >
            <div className="relative z-10 text-center">
              <h1 className="text-7xl text-white font-bold mb-3">{blok.name}</h1>
            </div>
            <img
              src={`${blok.background_image?.filename}`}
              alt={blok.background_image.alt}
              className="absolute top-0 left-0 z-0 w-full h-full object-cover"
            />
          </div>
          <div className="px-6">
            <h2 className="text-2xl font-bold ">{blok.description}</h2>
            <hr className="flex-grow border-t mt-2 border-gray-300"/> 
            <div className="flex justify-between mt-4">
              <p className="text-xl">📍{blok.city}</p>
              <div className="flex ">
                {blok.cuisines.map((c) => (
                  <span
                    key={c}
                    className="px-4 py-1 rounded-full mx-2 text-white text-sm  bg-green-500 "
                  >
                    {c}
                  </span>
                ))}
              </div>
            </div>
            <div className="flex justify-between gap-4 mt-4">
              <p className="font-thin">Vegan {blok.vegan ? "✅" : "❌"}</p>
              {/* <p className="font-thin">
                Rating - <span className="font-bold">{blok.rating.value}</span>
              </p> */}
            </div>
          </div>
          <div className="mt-8 grid w-full grid-cols-1 gap-2 mx-auto sm:grid-cols-3">
            {blok.dishes.map((d) => (
              <div className=" ">
                <Dish dish={d}></Dish>
              </div>
            ))}
          </div>
        </div>
      </main>
    );
    export default Restaurant;

Once this is done, we should be able to see the other fields rendered as well. The best part here is that, whenever you change anything like adding or deleting a new dish, changing the images or text – everything is visible in real time. Feel free to try dragging the components as well to see the power of Visual Editor.

At this point, we have covered all important things and we are ready with our fully functional website. Just to make it better, let’s add a few more restaurants in the folder. 

To make our website look good, let’s also make the landing page better as well. Delete the home story that we have, and then go the block library to make some changes and add new blocks. You can go ahead and delete the page block as well. Let’s create the following components now – 

  • landing_page
    • body – Blocks
  • hero
    • headline – Text
    • subheadline – Text
    • image – Asset
    • layout – Single-Option with two options ( constrained and full_width)
  • featured_restaurants
    • restaurants – Multi Option 

In the restaurant field of featured_restaurants block, change the source to Stories and add the path folder as well as the content type as shown below – 

This will allow us to select the restaurants in the block from the Restaurants folder.

Now let’s add the following components to our Next.js project –

LandingPage.js – 


import { storyblokEditable, StoryblokComponent } from "@storyblok/react/rsc";
 
const LandingPage = ({ blok, restaurants }) => (
  <main {...storyblokEditable(blok)}>
    {blok.body.map((nestedBlok) => (
      <StoryblokComponent restaurants={restaurants} blok={nestedBlok} key={nestedBlok._uid} />
    ))}
  </main>
);
 
export default LandingPage;

The StoryblokComponent here allows us the render the components dynamically and is also used behind the scenes while using the StoryblokStory component.

Hero.js


import { storyblokEditable } from "@storyblok/react/rsc";
const Hero = ({ blok }) => {
  return (
    <div {...storyblokEditable(blok)} className={`min-h-[500px]
    relative
    flex
    items-end
    justify-center
    p-9
    my-6
    rounded-[5px]
    overflow-hidden ${blok.layout === 'constrained' ? 'container mx-auto' : ''}`}>
      <div className="relative z-10 text-center">
        <h1 className="text-6xl text-white font-bold mb-3">{blok.headline}</h1>
        <h2 className="text-4xl text-white font-light">{blok.subheadline}</h2>
      </div>
      <img
        src={`${blok.image?.filename}/m/filters:brightness(-50)`}
        alt={blok.image.alt}
        className="absolute top-0 left-0 z-0 w-full h-full object-cover"
      />
    </div>
  );
};
export default Hero;

FeaturedRestaurants – 


import { storyblokEditable } from "@storyblok/react/rsc";
const FeaturedRestaurants = ({ blok }) => {
  return (
    <section {...storyblokEditable(blok)}>
      <p className="text-5xl mt-8 text-center">Featured Restaurants</p>
      <div className="mt-8 grid w-full grid-cols-1 gap-2 mx-auto sm:grid-cols-3">
      {blok.restaurants.map((r) => (
          <div key={r.slug}>
            <div className="mx-auto flex w-80 flex-col justify-center bg-white rounded-2xl shadow-xl shadow-gray-400/20">
              <img
                className="aspect-video w-80 rounded-t-2xl object-cover object-center"
                src={r.content.background_image?.filename}
              />
              <div className="p-6">
                <small className="text-gray-900 text-xs">
                  {/* Rating - {reestaurant.rating.value} */}
                </small>
                <h1 className="text-2xl font-medium text-gray-700 pb-2">
                  {r.content.name}
                </h1>
                <p className="text text-gray-500 leading-6">{r.content.description}</p>
              </div>
            </div>
          </div>
      ))}
      </div>
 
    </section>
  );
};
export default FeaturedRestaurants;

The final version of StoryblokProvider.js with all the components should like this – 


/** 1. Tag it as client component */
"use client";
import { storyblokInit, apiPlugin } from "@storyblok/react/rsc";
/** 2. Import your components */
import FeaturedRestaurants from "./FeaturedRestaurants";
import LandingPage from "./LandingPage";
import Hero from "./Hero";
import Restaurant from "./Restaurant";
/** 3. Initialize it as usual */
storyblokInit({
  accessToken: "your-access-token",
  use: [apiPlugin],
  components: {
    restaurant: Restaurant,
    hero : Hero,
    "landing_page": LandingPage,
    "featured_restaurants": FeaturedRestaurants,
  },
});
export default function StoryblokProvider({ children }) {
  return children;
}

Now let’s go ahead and create a new home story using the landing page block. Add two blocks, the hero and featured_restaurants to its body field. Let’s also add data to these fields along with selecting the restaurants in the Featured Restaurants field. You will see that we get an error here, this is because the restaurants we select aren’t stored as the complete stories but as references. If you try checking the API response, you will see there are just a few UUIDs under the field. 

We need to resolve this field to get the data for the restaurants inside the component. For this case, there is a parameter that we can pass named as resolve_relations. We also need to resolve the relations for the bridge. 

Resolving Relations(for referencing one story in another story)

In the Page.js file where we fetch the data, we need to add the parameter as mentioned below –

let { data } = await storyblokApi.get(`cdn/stories/${slug}`, {version: 'draft', resolve_relations: ["featured_restaurants.restaurants"]}, {cache: "no-store"});

Also, as mentioned earlier, there is an option for passing the bridge options in the StoryblokStory. To pass that please change the component tag to the following in the same file –

<StoryblokStory story={data.story} bridgeOptions={{resolveRelations: ["featured_restaurants.restaurants"]}} />

And that is all! The error should be gone now. The preview will display the content rendered the way it should be along with the enabled live editing. We can now make any number of changes, add different blocks and it will all be editable in real time. 

Similarly, there can be more new blocks (components) and fields created. This was just a basic app to show the capabilities of Next.js and Headless. There are endless possibilities. Make sure to check out the space mentioned in the hint at the top along with the repository for a few more ideas. You can even try creating a demo space that is available while creating a new space. 

Conclusion

In this tutorial, we saw how to integrate Next.js and Storyblok. We saw how to manage and consume content using the API, and how to enable a real-time visual experience using the Visual Editor. We also went through a couple of features that Next.js offers to create great user experiences. 

Resources to look at – 

ResourceLink
Storyblok Technologies Hubhttps://www.storyblok.com/technologies
Storyblok React SDKhttps://github.com/storyblok/storyblok-react
Next.js Docshttps://nextjs.org/
Storyblok Visual Editorhttps://www.storyblok.com/docs/guide/essentials/visual-editor
Storyblok JS Bridgehttps://www.storyblok.com/docs/Guides/storyblok-latest-js
Next.jsRestaurant GuideStoryblok

Recent Posts

Cross-Platform Development Tools

May 14, 2024

Cross-Platform Development Tools: Comparing the Latest Frameworks

See post

AI in Transforming Customer Service

May 14, 2024

Role Of AI in Transforming Customer Service

See post

Developer Productivity

May 10, 2024

Boosting Developer Productivity with Healthy Snacking

See post

Contact us

Swan Buildings (1st floor)20 Swan StreetManchester, M4 5JW+441612400603community@developernation.net
HomeCommunityResearch ProgramBlog

Resources

Knowledge HubPulse ReportReportsForumEventsPodcast
Code of Conduct
SlashData © Copyright 2024 |All rights reserved