DEV Community

loading...
Cover image for [Storyblok, Gatsby] Create a blog overview page

[Storyblok, Gatsby] Create a blog overview page

arisa_dev
Developer Relations Engineer at Storyblok. Love Aikido🥋 A free tech knowledge-sharing community, Lilac organizer💪 A host of Anonymous.fm https://dev.to/anonymousfm-arisa
Updated on ・9 min read

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, with free hands-on Frontend e-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

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

Click "blogOverview" component we just created.

Add "title" and "body" schemas.

Alt Text

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

Alt Text

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

Alt Text

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

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.

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
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

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

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

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

Alt Text

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

Alt Text

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

(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

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

Alt Text

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", your arc/lib/storyblok.js file looks similar with this.

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;
}
Enter fullscreen mode Exit fullscreen mode

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

const storyblokInstance = new StoryblokBridge({
        resolveRelations: "posts-list.posts"
})
Enter fullscreen mode Exit fullscreen mode
Storyblok
  .get(`cdn/stories/${event.storyId}`, {
    version: 'draft',
    resolve_relations: "posts-list.posts"
  })
Enter fullscreen mode Exit fullscreen mode

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

Take a look at their GitHub repo of this project.

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

Storyblok: gatsbyjs-multilanguage-website

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

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.

{
  resolve: 'gatsby-source-storyblok',
  options: {
    accessToken: 'YOUR_TOKEN',
    version: 'draft',
    resolveRelations: ['Post'],
    includeLinks: false
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Storyblok gatsby-source-storyblok README: "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".

Storyblok, "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.

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

Congrats🎉🎉🎉

We've achieved our goal!

Alt Text

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.

  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
          }
        }
      }
    }
  `)
Enter fullscreen mode Exit fullscreen mode

Alt Text

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.jsfile.

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."

Discussion (2)

Collapse
dawntraoz profile image
Alba Silvente 💃🏼

Love it 😍 Really well explained and complete 🥳 A must to read!

Collapse
arisa_dev profile image
arisa_dev Author

Happy to hear that😍 I did my best! Also, it helps people to see as many different cases to build blogs with Storyblok😚