Storyblok Raises $80M Series C - Read News

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

Skip to main content

Create and Render Blog Articles in Storyblok and Remix

Try Storyblok

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

This tutorial will show how to add and render blog articles to our website. We will also add an Article Teaser component to show all the articles on the blog overview page and popular articles in the home story. While building this, we will also take a look at what is the resolve_relations parameter of the Content Delivery API and how to resolve relations in Storyblok.

hint:

If you’re in a hurry, you can look at our demo in source code on GitHub! And take a look at the live version on Netlify.

Requirements

This is a part of the Ultimate Tutorial Guide for Remix. You can find the previous part of the series here, which shows how to create custom components in Storyblok and Remix. We recommend that tutorial before starting this one.

Creating a new Content Type Block for the Blog Articles

First of all, we need to consider how we would like to manage our blog articles. As of right now, we have just one content type block: the page block for all of our pages. This particular block allows for a lot of flexibility in arranging nested blocks exactly how a content creator might need them in a variety of different use cases. However, when it comes to blog articles, we most likely need less flexibility and more coherency instead. Therefore, we need to consider what a blog article would typically consist of and how that would translate to our selection of Storyblok fields. Let’s go for the following fields:

  • image: field type Asset {1}
  • title: field type Text {2}
  • teaser: field type Textarea {3}
  • content: field type Richtext {4}

Alright, so let’s create our new content type block - let’s call it article:

Creating a new article content block type
1
2
3
4

Creating a new article content block type

Managing all Articles from a Dedicated Folder

In order to keep everything nice and tidy, Storyblok makes it easy for you to manage all of your content in folders. Let’s create a new folder called Blog to organize all of our blog articles. When creating a new folder, you can even choose to set the default content type, so we can employ article block we just created {1}:

Creating a blog folder
1

Creating a blog folder

Now you can click on the fresh new folder and whenever you create a new story, it will be of the type article by default.

Once you’ve created the first article, you’ll see the schema we set up in action:

Storyblok Remix Ultimate tutorial part 5 new article

New Article

At this point, I would suggest creating 3-4 articles with some dummy content so that we have some articles to choose from later on.

Having taken care of the block schema for our articles, we can now move on and create a new nested block called popular-articles. This will be used to choose up to three articles that should be displayed in a preview format. For that block, we need to define the following fields:

  • headline: field type Text {1}
  • articles: field type Multi-Options {2}

Let’s create it:

Creating a new popular articles block in the Block Library
1
2

Creating a new popular articles block in the Block Library

For the articles field, we need to take some additional steps to configure it properly. First of all, we have to select Stories as the Source of our available options {1}. Since it should not be possible to select just any story, we can now take advantage of the Blog folder we set up earlier. Simply set blog/ as a value for Path to folder of stories {2}. Additionally, we should make sure only stories of the content type article are included in the options to choose from {3}. Finally, let’s limit the maximum number of articles that can be selected to 3 {4}.

Configuring the popular articles block in the Block Library
1
2
3
4

Configuring the popular articles block in the Block Library

Once you’ve created this block, you can use it anywhere on the Home story (at the root level of the content section in the Storyblok Space) and select up to three of your previously created articles.

Creating a Nested Block for All Articles

This nested block is needed to display previews of all existing articles at once. It is fairly straightforward to set up: all that is needed is a new nested block by the name all-articles with one text field called headline. The logic to retrieve all articles will be implemented in the frontend.

Once created, this block should be added on the Home story in our Blog folder, where it will serve to render an overview of all blog articles.

Adapting the Remix Project

Fantastic, we’re done with everything we need to configure on the Storyblok side of things - now let’s dive into the code, shall we?

First, we are going to map the components in our root.jsx file. Once done, we can start creating these files and start our coding.

root.jsx
        
      ...
+import Article from "./components/Article";
+import AllArticles from "./components/AllArticles";
+import PopularArticles from "./components/PopularArticles";

const components = {
  feature: Feature,
  grid: Grid,
  teaser: Teaser,
  page: Page,
  hero: Hero,
+        'popular-articles': 'PopularArticles',
+        'all-articles': 'AllArticles',
+        article: 'Article',
};
...
    

Rendering Single Articles

Now, we can create a new component that renders a single view of our articles:

Article.jsx
        
      import { renderRichText } from "@storyblok/react";

const Article = ({ blok }) => {
  return (
    <section className="text-gray-600 body-font">
      <div className="container mx-auto flex px-5 py-24 items-center justify-center flex-col">
        {blok.image && (
          <img
            className="  md:h-96 w-full mb-10 object-cover object-center rounded"
            alt={blok.image.alt}
            src={`${blok.image.filename}/m/1600x0`}
          />
        )}
        <div className="text-center lg:w-2/3 w-full">
          <h1 className="title-font sm:text-4xl text-3xl mb-4 font-medium text-gray-900">
            {blok.title}
          </h1>
          <h2 className="title-font sm:text-3xl text-2xl mb-4 font-medium text-gray-600">
            {blok.subtitle}
          </h2>
          <div
            className="mb-8 leading-relaxed text-left max-w-full prose"
            dangerouslySetInnerHTML={{ __html: renderRichText(blok.content) }}
          />
        </div>
      </div>
    </section>
  );
};
export default Article;
    

Most of this code should be somewhat familiar to you at this point. However, two interesting things are happening here. First, we are attaching two parameters to the image URL (/m/1600x0), resulting in an optimized, resized image generated by the Storyblok Image Service. You will also see that we use an API here named renderRichText. This is because we have a richtext field (content) in a blog article (Article Component). We use this API to render the rich text we are getting in the content field from Storyblok.

hint:

When using TailwindCSS, installing the @tailwindcss/typography plugin and using the prose class results in beautifully formatted text.

If you open any of your articles in the Visual Editor, everything should be rendered correctly now:

Blog Article

Blog Article

Displaying Article Teasers

Now, let's take a look at how to show all the article teasers on the Blog Home story. Each blog teaser will be styled as a card to show the details about the blog article, along with a link to it.

Create a new component in the components folder named ArticleTeaser. Add the following code to the ArticleTeaser.jsx file.

ArticleTeaser.jsx
        
      import { Link } from "@remix-run/react";

const ArticleTeaser = ({ article }) => {
  return (
    <div className="column feature">
      <div className="p-6">
        {article.image && (
          <img
            className="object-cover object-center w-full mb-8 lg:h-48 md:h-36 rounded-xl"
            src={`${article.image.filename}/m/360x240`}
            alt={article.image.alt}
          />
        )}
        <h2 className="mx-auto mb-8 text-2xl font-semibold leading-none tracking-tighter text-neutral-600 lg:text-3xl">
          {article.title}
        </h2>
        <div className="mx-auto text-base leading-relaxed text-gray-500 line-clamp-2">
          {article.teaser}
        </div>
        <div className="mt-4">
          <Link
            to={`/blog/${article.slug}`}
            prefetch="intent"
            className="inline-flex items-center mt-4 font-semibold text-blue-600 lg:mb-0 hover:text-neutral-600"
          >
            Read More »
          </Link>
        </div>
      </div>
    </div>
  );
};
export default ArticleTeaser;
    

This teaser component will just be in our front-end, as we will pass the blog article data as props. Now, let's create an all-articles block (Nested block) in Storyblok with only one field named title of type text. We will now add this to the home story inside the blog folder. You can add any title you'd like for the block.

In our Storyblok and Remix application, we'll efficiently handle data retrieval and component rendering in two main steps:

1- Fetching Data in the $.jsx Route:
We fetch data for all articles in the $.jsx route. This is done because Remix is designed to load data at the route level before rendering components. Fetching data in the parent route ($.jsx) ensures all necessary data is available upfront, improving the page load time.
Here, we are fetching all the stories from the Blog folder and excluding the Home story. The first parameter starts_with , helps us get all the stories inside the blog folder. We are also excluding the Blog Home with the second parameter, which is is_startpage . We are setting it to false. The Blog Home is the start page of the blog folder as we made it the root of the folder.

Here's how we set up our loader in $.jsx

$.jsx
        
      ...

export const loader = async ({ params}) => {
  let slug = params["*"] ?? "home";
  let blogSlug = params["*"] === "blog/" ? "blog/home" : null;
 
  let sbParams = {
    version: "draft",
    resolve_relations: ["popular-articles.articles"],
  };
    let { data } = await getStoryblokApi()
    .get(`cdn/stories/${blogSlug ? blogSlug : slug}`, sbParams)
    .catch((e) => {
      console.log("e", e);
      return { data: null };
    });

  if (!data) {
    throw new Response("Not Found", { status: 404 });
  }
  let { data: articles } = await getStoryblokApi().get(`cdn/stories`, {
    version: "draft", // or 'published'
    starts_with: "blog/",
    is_startpage: 0,
  });
  return json({ story: data?.story, articles: articles?.stories });
};
    

2- Creating the AllArticles Component:
In the AllArticles.jsx component, we retrieve the fetched data using the useLoaderData hook. This hook accesses the data loaded by the route's loader function. By writing const { articles } = useLoaderData(); in our component, we're directly accessing the article's data fetched in the $.jsx route, making it available for rendering in the component.
This is the new code for our loader in $.jsx .

In AllArticles.jsx, we use the following code:

AllArticles.jsx
        
      import { useLoaderData } from "@remix-run/react";
import ArticleTeaser from "./ArticleTeaser";
import { storyblokEditable } from "@storyblok/react";

const AllArticles = ({ blok }) => {
  const { articles } = useLoaderData();

  return (
    <>
      <p className="text-3xl">{blok.title}</p>
      <div
        className="grid w-full grid-cols-1 gap-6 mx-auto lg:grid-cols-3   lg:px-24 md:px-16"
        {...storyblokEditable(blok)}
      >
        {articles?.length &&
          articles.map((article) => {
            article.content.slug = article.slug;
            return (
              <ArticleTeaser article={article.content} key={article.uuid} />
            );
          })}
      </div>
    </>
  );
};
export default AllArticles;
    

Additionally, we are also adding the slug to the content of the blog to use it for the link in ArticleTeaser.

Now, we should see all the article teasers like this in our Blog Home.

All Articles

All Articles

Let's now see how we can use all the existing blog articles and reference them in the Home story of our space (root folder). For this, we will need to create a new nested block named popular-articles. It will have just one field named articles. This field should be of the type Multi-Options {1}

Popular Articles Field
1

Popular Articles Field

Let's change the Display name to Popular Articles {1} and change the Source to Stories {2}.

Field Settings
1
2

Field Settings

We will also need to set the path of the folder for stories, which will be blog/ {1}. And let's also restrict the content type here to article {2}. Lastly, let's set the maximum number to 3 {3}.

Field Settings
1
2
3

Field Settings

This will allow us to select three blog articles from the list of all the blog articles. Let's go ahead and add it to the Home story. You should see something like this:

Popular Articles Selection

Popular Articles Selection

You can select any three articles from the list of Articles. We won't be able to see anything yet, as we still need to add a component to our frontend. But before that, if you look at how our draft JSON looks after selecting the blogs, you will see something like this in the articles array.

Draft Json
        
      "articles": [
    "8c9877f0-6ef5-4cd0-9f7d-abc88ceaab14",
    "aafc0ccb-0339-4545-b7dd-6a5879ffa059",
    "81c1f9f8-fdb8-4e3b-ab7e-56648adb51ac"
],
    
Hint:

You can see the draft or published JSON simply by clicking the dropdown button next to the publish button and selecting the option. You can even check the page history from there.

This is the array containing the _uid s of the selected articles. The API parameter comes into play, helping you resolve the relations based on these _uids. It will allow us to get all the content for these blogs. If you see the JSON from the URL you get after clicking the dropdown button next to the publish button, try appending &resolve_relations=popular-articles.articles to the URL.

The complete URL should look something like this:

https://api.storyblok.com/v2/cdn/stories/home?version=draft&token=UatY9FBAFasWsdHl7UZJgwtt&cv=1655906161&resolve_relations=popular-articles.articles

Now, you will see a key rels in the JSON, which gives you the content for all your selected blogs.

We need this functionality in our frontend. To do that, let's go to the root.jsx file and add resolve_relations to the Storyblok parameters, which we use while fetching the data with the storyblokApi. Update the params as follows:

root.jsx
        
      let sbParams = {
    version: "draft", // or 'published',
    resolve_relations: ["popular-articles.articles"],
  };
    

This will automatically resolve the relations when it sees the array of articles inside the popular_articles. Let's now add the PopularArticle component to our Remix project. Create a file named PopularArticle.jsx and the following code to it:

PopularArticles.jsx
        
      import ArticleTeaser from "./ArticleTeaser";
import { storyblokEditable } from "@storyblok/react";

const PopularArticles = ({ blok }) => {
  return (
    <>
      <h2 className="text-3xl">{blok.headline}</h2>
      <div
        className="grid w-full grid-cols-1 gap-6 mx-auto lg:grid-cols-3   lg:px-24 md:px-16"
        {...storyblokEditable(blok)}
      >
        {blok.articles.map((article) => {
          article.content.slug = article.slug;
          return <ArticleTeaser article={article.content} key={article.uuid} />;
        })}
      </div>
    </>
  );
};
export default PopularArticles;
    

After hitting save, our website should now have the ArticleTeaser cards for the popular articles as well, and should look like this:

Home Story

Home Story

However, you will see that if you now try to select or deselect any article from the list of popular articles, you will get an error, and it won't work correctly. You won't be able to see the live changes unless you hit save. This is because we still need to resolve the relations for the Storyblok Bridge, which is the last thing we must do.

In the $.jsx file, let's pass another argument to the useStoryblokState for resolving relations. The code should now be:

$.jsx
        
      story = useStoryblokState(story, {
    resolveRelations: ["popular-articles.articles"],
});
    

And that's it! Now, you can play with article teasers here, and it will show you the live edits in real time.

Wrapping Up

This tutorial taught you how to create and render blog articles with Storyblok and Remix. You also saw how to resolve relations when referencing stories in other stories.

Next Part:

In the next part of this series, we will see how to manage multilingual content in Storyblok and Remix. You can find it here.

Author

Alexandra Spalato

Alexandra Spalato

Alexandra Spalato is a Developer Relations Engineer at Storyblok, specializing in JAMstack & headless architecture. With a background in freelance dev & entrepreneurship, she brings a unique perspective to successful web projects. Alexandra drives progress in tech industry through speaking at conferences & empowering developers