[Storyblok, Gatsby] Create a blog overview page
Tags: storyblok, gatsby, react
Published: 2021-05-23 15:47:07

Hi! I'm Arisa, a DevRel from this June living in Germany🇩🇪 (A big announcement is coming this June😏)

I have a free online programming learning community called [Lilac](https://note.com/frontendlifeinde/m/m9b8feda1d547), with [free hands-on Frontend e-books](https://zenn.dev/arisa_dev/books)👩‍💻

# Who is this article for?

- Anyone who wants to build a tech blog with Storyblok & Gatsby.js

# Step 1: Create a root entry in a folder

Create a root entry in a folder which I expect you to already have a few blog entries.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/upssj78jgoo873xw2b05.png)

Go to "Components" from the left hand side of the menu.

Click "blogOverview" component we just created.

Add "title" and "body" schemas.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r6ri4te0mm5v9f2juzzr.png)

The "title" schema can stay as it is.

As for the "body" schema, change a type into "blocks".

After that, set up the rest as below.

- Tick "allow only specific components to be inserted"
- Choose "grid", "teaser" and "featured-articles" from "Component whitelist" section
- Set "allow maximum" section as 1000

At this point, you can't find yet the component called "featured-articles".

Let's move on to create that.

In a same "Components" page in a main dashboard, Click an option called "NEW" in the up right corner.

Define one schema with a name of "articles" and select a type as "blocks".

It should look like this.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ojlbtjf599gd4z3xprf.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wjfn4axv6x6qqxuksahw.png)

There's one more component we need to create to add "component whitelist" into a "featured-articles".

We'll create a component called, "article-teaser" with "Link" type.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1g1hncozoq37przg640l.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ht6eewtrefcpqa2a3m0f.png)

# Step 2: Create a `pages/blog.js` page

Next up, we create a blog overview page in Gatsby.

If you are lost why I'm doing this, take a look at the Gatsby's documentation about page creation.

> [Gatsby: Define routes in `src/pages` ](https://www.gatsbyjs.com/docs/reference/routing/creating-routes/#define-routes-in-srcpages)

This time, we know that we only want just one blog overview page.

Which means, we won't create several same page templates like this in this case.

If so, we can save our time to create a page component file under the `pages` directory.

Create `src/pages/blog.js` file.

As an example, it'll be something like this.

```javascript
import * as React from "react"
import { graphql } from 'gatsby'
import SbEditable from 'storyblok-react'

import Layout from "../components/Layout"
import Seo from "../components/seo"
import DynamicComponent from "../components/DynamicComponent"
import useStoryblok from "../lib/storyblok"

const BlogOverview = ({ data, location }) => {
  console.log(data)
  let story = data.storyblokEntry
  story = useStoryblok(story, location)

  const components = story.content.body.map(blok => {
    return (<DynamicComponent blok={blok} key={blok._uid} />)
  })

  return (
  <Layout>
    <SbEditable content={story.content}>
      <Seo title="Blog Posts" />
        <div>
          <div>
            <h1>{ story.content.title }</h1>
          </div>
        </div>
        { components }
    </SbEditable>
  </Layout>
)}

export default BlogOverview

export const query = graphql`
  query BlogPostsQuery {
    storyblokEntry(full_slug: {eq: "blog/"}) {
      content
      name
    }
  }
`
```

How do I know the path to fetch queries?

Gatsby provides us GraphiQL👍

Go to `http://localhost:8000/___graphql` in the browser.

This time, we want a entry page from Storyblok.

(Remember that our Overrview page was created as an entry?)

So, choose `storyblockEntry` and let's take a look at a draft JSON from Storyblok.

You can get access from the Storyblok main dashboard.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jkexmxj3qih3hq4a6k81.png)

Our goal in here is to make a slug in this blog overview page to "/blog/" .

To do so, we need to check a value in `full_slug` from a draft JSON.

There it is💪

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gtma7tkoa0hdewnw7qq6.png)

It shows us that we can set our eq variable as `"blog/"` .

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/933z2fje3s4nzq4izs9f.png)

These are the gems we need to generate a blog overview page💎

And that's why I already knew a path to fetch necessary data.

# Step 3: Create Posts List in Blog Overview component.

Click "Add block".

In the list of the blocks, we can't find a block we want to use this time.

Instead, we add a new block.

Click an input section, and type our new block name as "posts-list".

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tx96lj3hkradypzdmw2u.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/60al5m285yfxgjhcocje.png)

It'll appear as a new block in a body schema.

When you click "Posts List", you'll see all the blog entry pages are prepared.

(Make sure you already created few blog posts.)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ihl054a7zt4xex8ycvuq.png)

(If you can't find one yet, I recommend you to take a look at this blog post.)

> [[Storyblok, Gatsby] Programmatically create blog post pages from data](https://dev.to/arisa_dev/storyblok-gatsby-programmatically-create-blog-post-pages-from-data-2pgo)

At this point, we can already see our blog overview page!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/td9de1kt0gt53vnkf46v.png)

But not yet all the blog posts list by a Posts List field component.

# Step 4: Resolving relations on multi-options field type

First, we'll edit our file which is dealing with the Storyblok Bridge and visual editor Events.

In my case, I created in a path of `src/lib/storyblok.js` .

But you can create with different names.

If you have already done Storyblok's blog post, ["Add a headless CMS to Gatsby.js in 5 minutes"](https://www.storyblok.com/tp/add-a-headless-cms-to-gatsby-5-minutes), your `arc/lib/storyblok.js` file looks similar with this.

```javascript
import { useEffect, useState } from "react"
import StoryblokClient from "storyblok-js-client";
import config from '../../gatsby-config'
const sbConfig = config.plugins.find((item) => item.resolve === 'gatsby-source-storyblok')
 
const Storyblok = new StoryblokClient({
  accessToken: sbConfig.options.accessToken,
  cache: {
    clear: "auto",
    type: "memory",
  },
});
 
export default function useStoryblok(originalStory, location) {
    let [story, setStory] = useState(originalStory)
 
    if(story && typeof story.content === "string"){
      story.content = JSON.parse(story.content)
    }
    
    // see https://www.storyblok.com/docs/Guides/storyblok-latest-js
    function initEventListeners() {
      const { StoryblokBridge } = window
 
      if (typeof StoryblokBridge !== 'undefined') {
        const storyblokInstance = new StoryblokBridge()
 
        storyblokInstance.on(['published', 'change'], (event) => {
          // reloade project on save an publish
          window.location.reload(true)
        })  
    
        storyblokInstance.on(['input'], (event) => {
          // live updates when editing
          if (event.story._uid === story._uid) {
            setStory(event.story)
          }
        }) 
 
        storyblokInstance.on(['enterEditmode'], (event) => {
          // loading the draft version on initial view of the page
          Storyblok
            .get(`cdn/stories/${event.storyId}`, {
              version: 'draft',
            })
            .then(({ data }) => {
              if(data.story) {
                setStory(data.story)
              }
            })
            .catch((error) => {
              console.log(error);
            }) 
        }) 
      }
    }
 
    function addBridge(callback) {
        // check if the script is already present
        const existingScript = document.getElementById("storyblokBridge");
        if (!existingScript) {
          const script = document.createElement("script");
          script.src = `//app.storyblok.com/f/storyblok-v2-latest.js`;
          script.id = "storyblokBridge";
          document.body.appendChild(script);
          script.onload = () => {
            // call a function once the bridge is loaded
            callback()
          };
        } else {
            callback();
        }
    }
 
    useEffect(() => {
      // load bridge only inside the storyblok editor
      if(location.search.includes("_storyblok")) {
        // first load the bridge and then attach the events
        addBridge(initEventListeners)
      }
    }, []) // it's important to run the effect only once to avoid multiple event attachment
 
    return story;
}
```

We'll add the `resolve_relations` option of the Storyblok API in this file.

```javascript
const storyblokInstance = new StoryblokBridge({
        resolveRelations: "posts-list.posts"
})
```

```javascript
Storyblok
  .get(`cdn/stories/${event.storyId}`, {
    version: 'draft',
    resolve_relations: "posts-list.posts"
  })
```

If you got exhausted from what I just show you, no worries.

I didn't come up all these code by myself.

Storyblok has prepared over 90% of them in their hands-on blog tutorial.

> [The Complete Guide to Build a Full-Blown Multilanguage Website with Gatsby.js](https://www.storyblok.com/tp/gatsby-multilanguage-website-tutorial)

Take a look at their GitHub repo of this project.

You'll find a lot of clues in there :)

> [Storyblok: gatsbyjs-multilanguage-website](https://github.com/storyblok/gatsbyjs-multilanguage-website/blob/main/src/utils/storyblok.js)

We set up our `src/lib/storyblok.js` to resolve relations with multi-option field type.

But the trick to display all of our blog posts list can't be done by just this single file.

We'll go and take a look at their `gatsby-source-storyblok` README to complete the rest of the settings.

> [Storyblok, `gatsby-source-storyblok` README](https://github.com/storyblok/gatsby-source-storyblok)

At this point, we know that we'll need to deal with `gatsby-node.js` file and `gatsby-config.js` files.

But in our case, our blog posts list page doesn't have much chance to create same structured pages like blog entries.

It means, it might not be useful to create as a template.

In this case, we don't need to create a blog posts list template as well as configuring in `gatsby-node.js` file.

For a moment, we already know that we can add `resolveRelations` value in `gatsby-config.js` file.

Add your value something like this.

```javascript
{
  resolve: 'gatsby-source-storyblok',
  options: {
    accessToken: 'YOUR_TOKEN',
    version: 'draft',
    resolveRelations: ['Post'],
    includeLinks: false
  }
}
```

In my case, I created my blog entry pages with Post content type.

It means, one single Post content type contains one single blog entry page.

If I could map them, technically, I can see all my blog posts list💡

Including the example of the value in `resolveRelations` , it's all in their documentation.

Take a look at the section of [The options object in details](https://github.com/storyblok/gatsby-source-storyblok#the-options-object-in-details).

> [Storyblok `gatsby-source-storyblok` README: "The options object in details"](https://github.com/storyblok/gatsby-source-storyblok#the-options-object-in-details)

# Step 5: Create a PostsList component

We're almost done!

Next up, we'll create a `src/components/PostsList.js` file.

This component file will map contents for us.

In this case, the contents we want are our blog posts.

This component file is also based on what Storyblok wrote in their hands-on blog post and their GitHub repo.

Take a look at the section of ["Resolving Relations on Multi-Options fields"](https://www.storyblok.com/tp/gatsby-multilanguage-website-tutorial#resolving-relations-on-multi-options-fields).

> [Storyblok, "Resolving Relations on Multi-Options fields"](https://www.storyblok.com/tp/gatsby-multilanguage-website-tutorial#resolving-relations-on-multi-options-fields)

You see the PostsList.js file example.

In my case, I don't need `rewriteSlug` function.

And I want to display my blog posted dates like "YYYY-MM-DD".

In that case, it'll look something like this.

```javascript
import React from "react"
import SbEditable from "storyblok-react"
import { useStaticQuery, graphql } from "gatsby"

const PostsList = ({ blok }) => {
  console.log(blok)
  let filteredPosts = [];
  const isResolved = typeof blok.posts[0] !== 'string'

  const data = useStaticQuery(graphql`
    {
      posts: allStoryblokEntry(
        filter: {field_component: {eq: "Post"}}// 👈 change it to your content type
      ) {
        edges {
          node {
            id
            uuid
            name
            slug
            full_slug
            content
            created_at
          }
        }
      }
    }
  `)
  if(!isResolved) {
    filteredPosts = data.posts.edges
    .filter(p => blok.posts.indexOf(p.node.uuid) > -1);

    filteredPosts = filteredPosts.map((p, i) => {
      const content = p.node.content
      const newContent = typeof content === 'string' ? JSON.parse(content) : content
      p.node.content = newContent
      return p.node
    })
  }

  const arrayOfPosts = isResolved ? blok.posts : filteredPosts
  return (
    <SbEditable content={blok} key={blok._uid}>
      <div>
      <ul>
        {arrayOfPosts.map(post => {
          return (
            <li key={post.name}>
              <div>
                <span>
                  { post.created_at.slice(0, 10) }
                </span>
              </div>
              <div>
                <a href={`/${post.full_slug}`}>
                  {post.content.title}
                </a>
                <p>{post.content.intro}</p>
              </div>
              <div>
                <a href={`/${post.full_slug}`}>
                  Read more
                </a>
              </div>
            </li>
          )
        })}
      </ul>
      </div>
    </SbEditable>
  )
}

export default PostsList
```

Last thing but not least, import component into `src/components/DynamicComponent.js` file.

```javascript
import SbEditable from 'storyblok-react'
import Teaser from './Teaser'
import Grid from './Grid'
import Feature from './Feature'
import PostsList from './PostsList'
import React from "react"

const Components = {
  'teaser': Teaser,
  'grid': Grid,
  'feature': Feature,
  'posts-list': PostsList
}

// the rest will continue
```

Congrats🎉🎉🎉

We've achieved our goal!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pyp6vvwtdd4tvox6sur4.png)

One last thing to fix a tiny thing.

If we take a look closer, you notice that the order of the blog posts are not ideal.

We want our blog posts to be ordered by posted date, which means that we want our newest post in top.

To do that, it's not that hard.

Just add `order: DESC` in `src/templates/PostsList.js` query part.

```javascript
  const data = useStaticQuery(graphql`
    {
      posts: allStoryblokEntry(
        filter: {field_component: {eq: "Post"}}
        sort: {fields: [created_at], order: DESC} //👈
      ) {
        edges {
          node {
            id
            uuid
            name
            slug
            full_slug
            content
            created_at
          }
        }
      }
    }
  `)
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r1syeh7r8wbeuzb4ycpj.png)

Looks much better👍

# Trouble shooting

If you encounter the error says "Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.", probably, it could be a case you forgot to create/load `src/pages/blog.js`file.

I accidentally commented out entire source code in this file while I was still figuring out.

And turned out that it just was that I forgot to load this file😅

Silly but you might also get into this rabbit hole.

React pointed this out too, if you'd like to take a look at what others were having this issue.

> [React.js, "Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object."](https://github.com/facebook/react/issues/13445)