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 Astro

Try Storyblok

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

In this part, we’ll add a blog to our website. More specifically, we’ll include featured articles on the home page and an overview of all articles on a dedicated blog page. Along the way, you’ll learn many exciting things, including how to create and manage folders in Storyblok, how to reference stories in a multi-options field and how to resolve relations using the Storyblok Content Delivery API. Are you ready? Let’s get started!

Live demo:

In a hurry? Check out the source code on GitHub and take a look at the live version on Netlify.

Requirements

This tutorial is part 5 of the Ultimate Tutorial Series for Astro. We recommend that you follow the previous tutorials 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 Astro Ultimate tutorial part 5 empty article

At this point, I would suggest creating 3-4 articles with some dummy content so that we actually 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 Astro 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 astro.config.mjs file. Once done we can start creating these files and start our coding.

astro.config.mjs
        
      import { defineConfig } from 'astro/config'
import storyblok from '@storyblok/astro'
import { loadEnv } from 'vite'
import tailwind from '@astrojs/tailwind'
import basicSsl from '@vitejs/plugin-basic-ssl'
const env = loadEnv('', process.cwd(), 'STORYBLOK')

// https://astro.build/config
export default defineConfig({
  integrations: [
    storyblok({
      accessToken: env.STORYBLOK_TOKEN,
      components: {
        page: 'storyblok/Page',
        feature: 'storyblok/Feature',
        grid: 'storyblok/Grid',
        teaser: 'storyblok/Teaser',
        hero: 'storyblok/Hero',
        config: 'storyblok/Config',
+        'popular-articles': 'storyblok/PopularArticles',
+        'all-articles': 'storyblok/AllArticles',
+        article: 'storyblok/Article',
      },
    }),
    tailwind(),
  ],
  vite: {
    plugins: [basicSsl()],
    server: {
      https: true,
    },
  },
})
    

Rendering Single Articles

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

src/storyblok/Article.astro
        
      ---
import { storyblokEditable, renderRichText } from '@storyblok/astro'

const { blok } = Astro.props
const renderedRichText = renderRichText(blok.content)
---

<article {...storyblokEditable(blok)}>
  <img
    class="w-full h-[360px] lg:h-[450px] object-cover"
    alt={blok.image.alt}
    src={`${blok.image.filename}/m/1600x0`}
  />
  <div class="container mx-auto mb-12">
    <h1 class="text-6xl text-[#50b0ae] font-bold mt-12 mb-4">
      {blok.title}
    </h1>
    <h2 class="text-2xl text-[#1d243d] font-bold mb-4">
      {blok.subtitle}
    </h2>
    <div class="prose" set:html={renderedRichText} />
  </div>
</article>
    

Most of this code should be somewhat familiar to you at this point. However, there are two interesting things happening here. First, we are attaching two parameters to the image url (/m/1600x0), resulting in an optimized, resized image being generated by the Storyblok Image Service. Second, we are resolving the rich text field we created for the main content of our articles. This is achieved using the built-in renderRichText function of @storyblok/astro.

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:

Storyblok Astro Ultimate tutorial part 5 render article

Next, let’s create another new component that serves to render the selected popular articles.

src/storyblok/PopularArticles.astro
        
      ---
import ArticleCard from '../components/ArticleCard.astro'
import { storyblokEditable } from '@storyblok/astro'

const { blok } = Astro.props
let articles = blok.articles
---

<section class="mx-6 my-12" {...storyblokEditable(blok)}>
  <h2 class="text-6xl text-[#50b0ae] font-bold text-center mb-12">
    {blok.headline}
  </h2>
  <ul class="grid grid-cols-1 gap-6 md:grid-cols-3">
    {
      articles?.length &&
        articles.map((article) => {
          article.content.slug = article.slug
          return (
            <li>
              <ArticleCard article={article.content} />
            </li>
          )
        })
    }
  </ul>
</section>
    

As you can see, we are looping through the blok.articles field using a nested ArticleCard.astro component. Outsourcing the code for the article cards is quite helpful because we can easily reuse it later for All Articles block.

Since this component exclusively exists in our code base but not in the block library of our space, I would suggest putting it in the components rather than the storyblok folder. Let’s create the article card component:

components/ArticleCard.astro
        
      ---
const { article } = Astro.props
---

<article class="column feature bg-gray-100 rounded">
  <img
    class="object-cover object-center w-full lg:h-48 md:h-36 rounded"
    src={`${article.image.filename}/m/360x240`}
    alt={article.image.alt}
  />
  <div class="p-4">
    <h1 class="text-2xl font-semibold lg:text-3xl">
      {article.title}
    </h1>
    <p class="leading-relaxed text-gray-700 line-clamp-2 my-4">
      {article.teaser}
    </p>
    <a
      href={`/blog/${article.slug}`}
      class="font-semibold text-blue-600 hover:underline"
      title="read more"
    >
      Read More »
    </a>
  </div>
</article>
    

Now there’s just one more thing we need to take care of in order to have our Popular Articles block render correctly. Right now, the articles field of our block would only include the uuids of the stories (blog articles) we referenced. By default, the Storyblok Content Delivery API would only return the data of the main story you are requesting (which would be the Home story in this case, as that is where we included the Popular Articles block earlier). However, using a really convenient API parameter called resolve_relations, we can retrieve the data of the stories we referenced, too.

In order to do that, we have to specify the block and the field containing the uuids that should be resolved. We can easily make that change in our […slug.astro]:

pages/[...slug].astro
        
      const storyblokApi = useStoryblokApi()
const { data } = await storyblokApi.get(
  `cdn/stories/${slug === undefined ? 'home' : slug}`,
  {
    version: 'draft',
+    resolve_relations: ['popular-articles.articles'],
  }
)
    

To learn more about how that works, you can head over to the @storyblok/js documentation.

hint:

You can experiment with the resolve_relations parameter by opening the Draft JSON in a new tab and attaching the parameter to the URL, e.g. &resolve_relations=popular-articles.articles. Resolved stories will be shown in a rels array at the end of the JSON response.

Wonderful, now our popular articles should show up correctly:

Storyblok Astro Ultimate tutorial part 5 popular article

Rendering All Articles

Finally, we also have to create a component that renders all articles:

src/storyblok/AllArticles.astro
        
      ---
import { storyblokEditable, useStoryblokApi } from '@storyblok/astro'
import ArticleCard from '../components/ArticleCard.astro'

const { blok } = Astro.props

const storyblokApi = useStoryblokApi()
const { data } = await storyblokApi.get(`cdn/stories`, {
  version: 'draft', // or 'published'
  starts_with: 'blog/',
  is_startpage: 0,
})

const articles = data.stories
---

<section class="mx-6 my-12" {...storyblokEditable(blok)}>
  <h2 class="text-6xl text-[#50b0ae] font-bold text-center mb-12">
    {blok.title}
  </h2>
  <ul class="grid grid-cols-1 gap-6 md:grid-cols-3">
    {
      articles?.length &&
        articles.map((article) => {
          article.content.slug = article.slug
          return (
            <li>
              <ArticleCard article={article.content} />
            </li>
          )
        })
    }
  </ul>
</section>
    

It is very similar to storyblok/PopularArticles.astro. However, there is a few extra lines of code we have to write. No worries, let’s go through it together!

Up until now, we’ve always requested a single story from the API. Here, using cdn/stories, we’re requesting multiple stories. To narrow down our results, we’re employing the starts_with parameter. In this particular case, we’re telling the API to deliver all stories that exist in our Blog folder. Also, we want to make sure that our Blog Home page is not included. Since it is defined as the root of the folder, we can easily exclude it by setting the is_startpage parameter to 0.

Having taken care of that, we can just reuse our ArticleCard.astro component to loop through our articles and pass on the relevant properties.

And just like that, all of our articles should render correctly on the blog overview page:

Storyblok Astro Ultimate tutorial part 5 all article

Wrapping Up

Congratulations, you’ve completed yet another crucial step on your way to becoming a Storyblok and Astro expert! By now, you have successfully integrated a blog into your website and taken a deeper dive into the Storyblok CMS and its Content Delivery API. While there’s still so much to discover, I hope that the concepts we explored together in this part already help you to gain a more thorough understanding of the workings of Storyblok and how to use these to your advantage when developing your own projects in the future.

Next part:

Continue reading and learn how to manage multilingual content in Storyblok and Astro.

Author

Dipankar Maikap

Dipankar Maikap

Dipankar is a seasoned Developer Relations Engineer at Storyblok, with a specialization in frontend development. His expertise spans across various JavaScript frameworks such as Astro, Next.js, and Remix. Passionate about web development and JavaScript, he remains at the forefront of the ever-evolving tech landscape, continually exploring new technologies and sharing insights with the community.