Storyblok Raises $80M Series C - Read News

What’s the True Total Price of Enterprise CMS? Find out here.

Skip to main content

Build Your Personal Blog With Next.js, Storyblok, and Layer0

Try Storyblok

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

  • Home
  • Tutorials
  • Build Your Personal Blog With Next.js, Storyblok, and Layer0

This guide describes how to create & deploy your Personal Blog built with Next.js and Storyblok to Layer0. Clone the repo blog-next-storyblok-layer0-starter to get the entire setup.

Set up Storyblok space

To set up a Storyblok space, log in to your account or create a new one, and click on Create new space

Create new space

Create new space

Create a new Storyblok space by giving it a name.

Name your space

Name your space

Click on Create a new folder to get started with creating the Authors folder.

Create a new folder

Create a new folder

Imagine Storyblok's folder as a collection of items, and each Storyblok entry being an item itself. We'll be creating two folders: Authors and Posts. Each entry that lives inside Authors or Posts represents an individual Author or a Post.

After typing in the Name, click on Add new as the Default content type and select Blank as we'll be giving it our own blueprint. Click save to create an Author folder.

Create Authors folder

Create Authors folder

Authors folder created

Authors folder created

Click on +Entry to create your first Author entry.

Add Author Entry

Add Author Entry

After typing in the Name, click on Save (The parent type is already assigned to Author as it was configured while defining the Authors folder).

Create Author Entry

Create Author Entry

The current Author has an empty blueprint. Let's start defining our own schema.

Define Author Schema

Define Author Schema

Add Name and Image to the schema.

Author Schema

Author Schema

Oops! Image is being considered as text. Don't worry. Click on Image label to change its field type.

Set Image Field Type

Set Image Field Type

From the Type list of icons, select Asset for the content type of Image in the Author schema.

Select Asset Type

Select Asset Type

To further restrict assets to images, select Images in the Filetypes. Click on Save schema to save the changes made.

Restrict to Images

Restrict to Images

After you've defined the schema, input the data (Name and Image) of the first author, and then hit the Publish button to make Author 1 visible to Storyblok's GraphQL API.

Create Author

Create Author

Follow the same steps used to create Authors to create Posts. The only change is that the default content type will now be Post. Storyblok's default content type makes it easier for you to get started without defining your own schema again.

Add Posts Folder

Add Posts Folder

Create a Post entry, and click on Save.

Create Post Entry

Create Post Entry

Once all of the data is inputted, hit the publish button to make the data ready for the API!

Publish Author

Publish Author

Next, obtain the "public" API key (further referred to as STORYBLOK_API_TOKEN) shown in the tab API-Keys of settings. Then, select Public as access level and press Create token to create a public token for your app. Obtain the token from the list as STORYBLOK_API_TOKEN.

Create Public Token

Create Public Token

Get Public Token

Get Public Token

Set up Next.js with Layer0

To set up, just clone the app repo and follow the blog to learn everything that's in it. To fork the project, run:

        
      git clone https://github.com/rishi-raj-jain/blog-next-storyblok-layer0-starter
cd blog-next-storyblok-layer0-starter
npm install // or yarn install
    

After these steps, you should be able to start the local environment using the following command.

        
      npm run layer0:dev

    

Data Fetching

In this section, we'll be diving deep into how the data fetching for the app is done. We make constant use of Incremental Static Regeneration (ISR), and GraphQL queries to fetch and display data statically.

The fetchAPI function

The app uses GraphQL Queries to perform Data Fetching. Browse http://localhost:3000, and you'll see the blog's home page. The first step for you to do now is replace the Storyblok API Token in lib/api.js fetchAPI function as following:

lib/api.js
        
      async function fetchAPI(query, { variables, preview } = {}) {
  const res = await fetch('https://gapi.storyblok.com/v1/api', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Token: YOUR_API_TOKEN, // Insert your token here!
      Version: preview ? 'draft' : 'published',
    },
    body: JSON.stringify({
      query,
      variables,
    }),
  })
  const json = await res.json()
  if (json.errors) {
    console.error(json.errors)
    throw new Error('Failed to fetch API')
  }
  return json.data
}
    
hint:

Do not hardcode the token when deploying a real-world app. Recommended practice is to use environment variables. Refer to Deploy Next SPA with Storyblok to Layer0 for deploying Next.js and Storyblok app with environment variables to Layer0.

Fetching the list of blog posts

On the blog's homepage, we make use of ISR by configuring revalidate property to 1 second. This ensures that the page remains static, and gets regenerated in case you publish or unpublish content on Storyblok. This is done as follows:

index.js
        
      export async function getStaticProps() {
  const allPosts = (await getAllPostsForHome()) || []
  return {
    props: { allPosts },
    revalidate: 1, // ensures regeneration
  }
}
    

getAllPostsForHome function makes use of the fetchAPI function, by passing a GraphQL query to fetch all the PostItems in descending order.

lib/api.js
        
      export async function getAllPostsForHome() {
  const data = await fetchAPI(
    `
      {
        PostItems(sort_by: "first_published_at:desc") {
          items {
            slug
            published_at
            first_published_at
            content {
              long_text
              intro
              title
              image
              author {
                name
                content
              }
            }
          }
        }
      }
    `
  )
  return data?.PostItems.items
}
    

Dynamic blog pages

Next.js makes it super easy to set up dynamic routes. In the app, you'd find blog/[slug].js, which maps pages that start with '/blog/'. Examples include '/blog/blog-1' and '/blog/something-new'.

Incrementally generated blog pages

With the use of getAllPostsWithSlug(), we fetch all of the blog posts that Next.js could prerender while generating static HTML of the app. The use of fallback: 'blocking', will server-render pages on-demand if the path doesn't exist. This configuration ensures that the pages remain static while exporting, whether posts are published, unpublished, or updated.

blog/[slug].js
        
      export async function getStaticPaths() {
  const allPosts = await getAllPostsWithSlug()
  return {
    paths: allPosts?.map((post) => `/blog/${post.slug}`) || [],
    fallback: 'blocking',
  }
}
    

The GraphQL query for fetching all posts with their slugs is as follows:

lib/api.js
        
      export async function getAllPostsWithSlug() {
  const data = await fetchAPI(`
      {
        PostItems {
          items {
            slug
          }
        }
      }
    `)
  return data?.PostItems.items
}
    

Fetching a post

We fetch the data for the current post by making use of the slug query parameter. First fetch the data for the current post, and then use the values of first_published_at and full_slug to fetch the previous and next blog data.

blog/[slug].js
        
      export async function getStaticProps({ params }) {
	// Get the data for the current post
	const data = await getPost(params.slug)
	// Get the previous blog
  const prevBlog = await getPrevBlog(
    data['post']['first_published_at'],
    data['post']['full_slug']
  )
  if (prevBlog.length > 0) prevBlog[0]['indicator'] = 'Previous'
	// Get the next blog
	const nextBlog = await getNextBlog(
    data['post']['first_published_at'],
    data['post']['full_slug']
  )
  if (nextBlog.length > 0) nextBlog[0]['indicator'] = 'Next'

  return {
    props: {
      post: {
        ...data.post,
        html: data.post?.content?.long_text
          ? new RichTextResolver().render(data.post.content.long_text)
          : null,
      },
      morePosts: [...prevBlog, ...nextBlog],
    },
    revalidate: 1,
  }
}
    
lib/api.js
        
      export async function getPost(slug) {
  const data = await fetchAPI(
    `
    query PostBySlug($slug: ID!) {
      PostItem(id: $slug) {
        slug
        full_slug
        published_at
        first_published_at
        id
        content {
          long_text
          intro
          title
          image
          author {
            name
            content
          }
        }
      }
    }
    `,
    {
      variables: {
        slug: `posts/${slug}`,
      },
    }
  )
  return {
    post: data?.PostItem,
  }
}
    

Getting Previous and Next Blogs

To get the previous and next posts, use the first_published_at and excluded_slug attributes of the current post that is shown.

To get the previous blog via a GraphQL query, sort the results in descending order, excluding the current slug from the results. Then request the first blog, which has a first_published_at timestamp that is earlier than the current one.

lib/api.js
        
      export async function getPrevBlog(first_published_at, excluded_slug) {
  const data = await fetchAPI(
    `
      query ($first_published_at:String!, $excluded_slug: String!) {
        PostItems(
          per_page: 1,
          excluding_slugs: $excluded_slug,
          sort_by: "first_published_at:desc",
          first_published_at_lt: $first_published_at, 
        ) {
          items{
            name
            slug
          }
        }
      }
    `,
    {
      variables: {
        first_published_at,
        excluded_slug,
      },
    }
  )
  return data?.PostItems.items
}
    

To get the next blog via a GraphQL query, sort the results in ascending order, excluding the current slug from the results. Then request the first blog, which has a first_published_at timestamp that is later than the current one.

lib/api.js
        
      export async function getNextBlog(first_published_at, excluded_slug) {
  const data = await fetchAPI(
    `
      query ($first_published_at:String!, $excluded_slug:String!) {
        PostItems(
          per_page: 1,
          excluding_slugs: $excluded_slug,
          sort_by: "first_published_at:asc",
          first_published_at_gt: $first_published_at, 
        ) {
          items{
            name
            slug
          }
        }
      }
    `,
    {
      variables: {
        first_published_at,
        excluded_slug,
      },
    }
  )
  return data?.PostItems.items
}
    

Deploy from CLI

You can do a production build of your app and test it locally using:

        
      npm run build && layer0 build
layer0 run --production
    

Deploying requires an account on Layer0. Sign up here for free. Once you have an account, you can deploy to Layer0 by running the following command in the root folder of your project:

        
      layer0 deploy
    

Now we are done with the deployment! Yes, that was all. You should see something like this in the console:

Deployment progress

Deployment progress

Go ahead! Change content and publish it on Storyblok, and browse the ‘/’ route of your website to look for changes (yes, Next.js Revalidation works just like that!).

Summary

In this tutorial, we learned how to build a personal blog with Storyblok CMS in a Next.js project and deploy it to Layer0.

ResourceLink
Github Example Repo blog-next-storyblok-layer0-starter
Next.js DocumentationNext.js Docs
Layer0 Deploying GuideDeploying Guide

Author

Rishi Raj Jain

Rishi Raj Jain

Technical Customer Success Manager at Edgio. Storyblok Ambassador. Synchronising my knowledge with community about Web Development, Caching, Edge Computing, Serverless, front-end ecosystems.