Almost EVERYONE who tried headless systems said they saw benefits. Download the state of CMS now!

Storyblok now on AWS Marketplace: Read more

O’Reilly Report: Decoupled Applications and Composable Web Architectures - Download Now

Empower your teams & get a 582% ROI: See Storyblok's CMS in action

Skip to main content

Create and Render Blog Articles in Storyblok and Nuxt

Try Storyblok

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

  • Home
  • Tutorials
  • Create and Render Blog Articles in Storyblok and Nuxt

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:

If you’re in a hurry, have a look at our live demo in Stackblitz! Alternatively, you can explore or fork the code from the Nuxt Ultimate Tutorial GitHub Repository.

Section titled Requirements Requirements

This tutorial is part 5 of the Ultimate Tutorial Series for Nuxt. We recommend that you follow the previous tutorials before starting this one.

Section titled Creating a new Content Type Block for the Blog Articles 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

Section titled Managing all Articles from a Dedicated Folder 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 an image field type
1

Creating an image field type

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:

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.

Section titled Creating a Nested Block for All 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.

Section titled Adapting the Nuxt Project Adapting the Nuxt 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?

Section titled Rendering Single Articles Rendering Single Articles

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

storyblok/Article.vue
        
      <template>
  <div v-editable="blok">
    <img
      v-if="blok.image?.filename"
      :src="blok.image.filename + '/m/1600x0'"
      :alt="blok.image.alt"
      class="w-full h-[360px] lg:h-[450px] object-cover"
    />
    <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 v-html="resolvedRichText" class="prose"></div>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({ blok: Object })

const resolvedRichText = computed(() => renderRichText(props.blok.content))
</script>
    

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/nuxt.

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:

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

storyblok/PopularArticles.vue
        
      <template>
  <div class="py-24">
    <h2 class="text-6xl text-[#50b0ae] font-bold text-center mb-12">{{ blok.headline }}</h2>
    <div class="container mx-auto grid md:grid-cols-3 gap-12 my-12 place-items-start">
      <ArticleCard
        v-for="article in blok.articles"
        :key="article.uuid"
        :article="article.content"
        :slug="article.full_slug"
      />
    </div>
  </div>
</template>

<script setup>
defineProps({ blok: Object })
</script>
    

As you can see, we are looping through the blok.articles field using a nested ArticleCard.vue 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. Except for the article prop, it should also accept a slug prop so that we can pass the article story’s full_slug value and use it in our template. Using NuxtLink, we can now link each of these article cards to the full article view. Let’s create the article card component:

components/ArticleCard.vue
        
      <template>
  <NuxtLink
    :to="'/' + slug"
    v-editable="article"
    class="w-full h-full bg-[#f7f6fd] rounded-[5px] text-center overflow-hidden"
  >
    <img
      :src="article.image.filename + '/m/600x0'"
      :alt="article.image.alt"
      class="w-full h-48 xl:h-72 object-cover pointer-events-none"
    />
    <div class="p-4">
      <h3 class="text-xl text-[#1d243d] font-bold mb-3">
        {{ article.title }}
      </h3>
      <div class="line-clamp-4">
        {{ article.teaser }}
      </div>
    </div>
  </NuxtLink>
</template>

<script setup>
defineProps({ article: Object, slug: String })
</script>
    

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.vue]:

pages/[...slug].vue
        
      const resolveRelations = ['popular-articles.articles']

const story = await useAsyncStoryblok(
  slug && slug.length > 0 ? slug.join('/') : 'home',
  {
    version: 'draft',
    resolve_relations: resolveRelations,
  },
  {
    resolveRelations,
  }
)
    

As you can see, we’re passing the array of relations to be resolved twice: first for the API request, and then for the Storyblok Bridge. 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:

Section titled Rendering All Articles Rendering All Articles

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

storyblok/AllArticles.vue
        
      <template>
  <div class="py-24">
    <h2 class="text-6xl text-[#50b0ae] font-bold text-center mb-12">{{ blok.title }}</h2>
    <div class="container mx-auto grid md:grid-cols-3 gap-12 my-12 place-items-start">
      <ArticleCard
        v-for="article in articles"
        :key="article.uuid"
        :article="article.content"
        :slug="article.full_slug"
      />
    </div>
  </div>
</template>

<script setup>
defineProps({ blok: Object })

const articles = ref(null)
const storyblokApi = useStoryblokApi()
const { data } = await storyblokApi.get('cdn/stories', {
  version: 'draft',
  starts_with: 'blog',
  is_startpage: false,
})
articles.value = data.stories
</script>
    

In the template section, it is very similar to storyblok/PopularArticles.vue. However, there is something odd going on in the script section. 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 for the folder, we can easily exclude it by setting the is_startpage parameter to false.

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

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

Section titled Wrapping Up Wrapping Up

Congratulations, you’ve completed yet another crucial step on your way to becoming a Storyblok and Nuxt expert! By now, you have successfully integrated a blog into your website and took 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 you advantage when developing your own projects in the future.

Next part:

The next part, exploring how to manage multilingual content in Storyblok and Nuxt, is already available!

Author

Manuel Schröder

Manuel Schröder

Manuel is a front-end engineer who loves crafting beautiful and fast websites. International Relations graduate turned WordPress expert, he ultimately developed a passion for the Jamstack. His favorite technologies include Vue, Astro, Tailwind, and Storyblok. These days, Manuel works as a Developer Relations Engineer at Storyblok.