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.
Go to "Components" from the left hand side of the menu.
Click "blogOverview" component we just created.
Add "title" and "body" schemas.
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.
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.
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.
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
}
}
`
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.
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💪
It shows us that we can set our eq variable as "blog/"
.
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".
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.)
(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!
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;
}
We'll add the resolve_relations
option of the Storyblok API in this file.
const storyblokInstance = new StoryblokBridge({
resolveRelations: "posts-list.posts"
})
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
Take a look at their GitHub repo of this project.
You'll find a lot of clues in there :)
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.
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
}
}
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".
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
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
Congrats🎉🎉🎉
We've achieved our goal!
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
}
}
}
}
`)
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.
Top comments (2)
Love it 😍 Really well explained and complete 🥳 A must to read!
Happy to hear that😍 I did my best! Also, it helps people to see as many different cases to build blogs with Storyblok😚