Build a blog with Nuxt 3 + Storyblok

alexandergekov

Alexander Gekov

Posted on February 20, 2023

Build a blog with Nuxt 3 + Storyblok

Introduction

In this article we will be building our very own blog using the help of Nuxt 3 and the Storyblok module.

You can follow along here:

What is Storyblok?

Storyblok website

Storyblok is a headless content management system or CMS for short. A headless CMS provides flexibility because it only handles the content and is responsible for storing and sending it via APIs as opposed to a traditional CMS like Wordpress, Drupal, etc. which handle content but also provide their own frontend without the choice of alternatives. With headless CMS your content can be displayed on mobile and web with no limitation as to which technologies you chose for visualization.

What differentiates Storyblok from other headless CMS is its intuitive visual editor and freedom when it comes to creating your own “bloks”. You can build your custom components and then reuse them or rearrange then in the Storyblok editor. This is especially useful for websites that need to constantly change their data.

Lastly, Storyblok has a great free-tier so anyone can try it out now. Moreover, it has great integrations with tools such as Nuxt to make the whole developer experience even more pleasant.

Let’s get right into it.

This is what we are going to build

We are going to build a simple blog where we can showcase our articles. It will look something like the image below.

final overview

Prerequisites

  • Storyblok account
  • Netlify account
  • Node.js (LTS)
  • Nuxt 3

Creating Storyblok space

  • Once you have your Storyblok account you can go ahead and create a new space.

create space 2

  • When you have created your space you can go to Settings and then Access Tokens.

access token

  • Copy your Token since we are going to need it in the next steps.

Storyblok’s Visual Editor

If you go to the Content tab on the left, you will see that Storyblok has given us a sample Home page.

content home

And if you click on Home you will be greeted by the Visual Editor:

home page sample

Now in order to view our Nuxt App we need to change the default environment URL. We can do that by going to Settings > Visual Editor and setting the Location to https://localhost:3010/.

Since Storyblok needs the url to be in https you can follow one of these tutorials to serve your Nuxt App through a proxy:

If you go back to the Visual Editor, we won’t be able to see our app just yet, the last step is to go to Entry configuration and set the Real path to “/”. Then you will be able to see your locally running app.

Entry config

Installation

In a newly scaffolded Nuxt 3 project, let’s run the following command:

npm install @storyblok/nuxt
Enter fullscreen mode Exit fullscreen mode

Let’s also add storyblok to our nuxt.config.ts:

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
    modules:[
        ["@storyblok/nuxt", { accessToken: "<your-access-token>" }]
    ]
})
Enter fullscreen mode Exit fullscreen mode

Remember the access token we copied from Storyblok, paste it instead of the placeholder.

Feel free to install dotenv npm i dotenv and put the access token in your .env file.

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
    modules: [
        ["@storyblok/nuxt", { accessToken: process.env.STORYBLOK_TOKEN }]
    ]
})
Enter fullscreen mode Exit fullscreen mode

Adding Tailwind

We are going to use Tailwind to style our components. Luckily, there’s a Nuxt module that we can use.

npm install --save-dev @nuxtjs/tailwindcss
Enter fullscreen mode Exit fullscreen mode

And let’s also add it in our nuxt.config.ts:

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
    modules: [
        ["@storyblok/nuxt", { accessToken: process.env.STORYBLOK_TOKEN }],
        '@nuxtjs/tailwindcss'
    ]
})
Enter fullscreen mode Exit fullscreen mode

And that’s it! We can now use Tailwind in our app. If it doesn’t work, try creating a tailwind.config.js file with the following code:

module.exports = {
    content: [
        './storyblok/**/*.{html,js,ts,vue}'
    ]
}
Enter fullscreen mode Exit fullscreen mode

Creating the main components

When creating a new space, Storyblok automatically creates four default components for us:

  • Page
  • Grid
  • Feature
  • Teaser

All of these you can find in the Components section of our space.

Remember, Storyblok handles the content, but not the visualization. That means, we now have to create the Vue components which will correspond to the Storyblok components.

Let’s start by creating ~/storyblok folder in our project. This folder will be auto-detected by the Storyblok module we installed.

Now let’s proceed by creating the four files:

  • storyblok/Page.vue:
<template>
  <div v-editable="blok">
    <StoryblokComponent
      v-for="blok in blok.body"
      :key="blok._uid"
      :blok="blok"
    />
  </div>
</template>

<script setup>
defineProps({ blok: Object })
</script>
Enter fullscreen mode Exit fullscreen mode
  • storyblok/Grid.vue:
<template>
  <div
    v-editable="blok"
    class="container mx-auto grid grid-cols-3 gap-12 place-items-center py-16"
  >
    <StoryblokComponent
      v-for="blok in blok.columns"
      :key="blok._uid"
      :blok="blok"
    />
  </div>
</template>

<script setup>
defineProps({ blok: Object })
</script>
Enter fullscreen mode Exit fullscreen mode
  • storyblok/Feature.vue:
<template>
  <div v-editable="blok">
    <h1 class="text-xl">{{ blok.name }}</h1>
  </div>
</template>

<script setup>
defineProps({ blok: Object })
</script>
Enter fullscreen mode Exit fullscreen mode
  • storyblok/Teaser.vue:
<template>
  <div v-editable="blok" class="py-16 text-5xl font-bold text-center">
    {{ blok.headline }}
  </div>
</template>

<script setup>
defineProps({ blok: Object })
</script>
Enter fullscreen mode Exit fullscreen mode

For each of the components we use the v-editable directive passing the blok element the component receives. The blok element is the actual blok data coming from Storyblok's Content Delivery API. We also use the <StoryblokComponent :blok="blok" /> which is available globally in our Nuxt app. Next step is to change the app.vue so we can actually see the Storyblok components on-screen.

Change app.vue

Let’s change app.vue like this:

<template>
    <NuxtLayout>
    <NuxtPage/>
    </NuxtLayout>
</template>
Enter fullscreen mode Exit fullscreen mode

And now we can create ~/pages folder and create index.vue file there.

<template>
    <StoryblokComponent v-if="story" :blok="story.content" />
</template>

<script setup>
    const story = await useAsyncStoryblok("home", { version: "draft" });
</script>
Enter fullscreen mode Exit fullscreen mode

useAsyncStoryblok is short-hand equivalent to using useStoryblokApi inside useAsyncData and useStoryblokBridge functions separately. Essentially it is our way to communicate with the Storyblok API by fetching the content for the specified page - in our case “home”.

Once done, we should be able to see this: (I took the liberty of adding a simple navigation bar, you can find it in the source code linked above)

Home page with content

Creating dynamic content pages

Currently we have ~/pages/index.vue corresponding to the / route. However, we want to be able to create pages directly from Storyblok. We can do this by deleting the index.vue and creating a Vue component named [...slug].vue. It will get the slug from the URL and see if we have such a page in Storyblok otherwise return our home page.

<script setup>
const { slug } = useRoute().params;

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

<template>
  <StoryblokComponent v-if="story" :blok="story.content" />
</template>
Enter fullscreen mode Exit fullscreen mode

Creating new stories (pages)

Now if we go back to Storyblok and create a new page, let’s say “About”, we can fill it with other blocks and see the updated page in Nuxt.

About me

We can add new pages and organize them any way we want. Once ready we just click Save and everything is updated live. How awesome is that! Next up we are going to start creating our blog and adding custom components to our arsenal.

Creating Article in Storyblok

We will need to create some custom components for our blog such as Article and ArticleCard.

Let’s go to Storyblok and create a new block in our block library. We name it article and give it:

  • image
  • title
  • description
  • content
  • and author.

Article fields

Once you have done that, you can also create a Blog folder to keep everything organized and in one place:

Blog folder

Now you can create pages for new blog posts and they will be of type Article.

  • Let’s create a new example article and fill in the fields we defined earlier.

    Article fields

Creating All Articles in Storyblok

Let’s now create a nested component that will show all articles when the user visits /blog.

In Storyblok let’s create a new component all-articles. It only needs a headline field.

All articles

Creating the Article component in Vue

Let’s create Article.vue in ~/storyblok:

<template>
    <div v-editable="blok">
      <img
        :src="blok.image.filename + '/m/1600x0'"
        :alt="blok.image.alt"
        class="mx-auto w-3/4 object-cover"
      />
      <div class="container mx-auto mb-12">
        <h1 class="text-6xl text-gray-800 font-bold mt-12 mb-4">{{ blok.title }}</h1>
        <h2 class="text-2xl text-gray-500 font-bold mb-4">
          {{ blok.description }}
        </h2>
        <div class="text-gray-600 mb-3">Written by: <b>{{ blok.author }}</b></div>
        <div v-html="resolvedRichText"></div>
      </div>
    </div>
  </template>

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

  const resolvedRichText = computed(() => renderRichText(props.blok.content));
  </script>
Enter fullscreen mode Exit fullscreen mode

As you can see we use the v-editable directive and the blok prop. What’s new is we optimize the image using Storybloks Image service and we use the v-html directive and renderRichText() to render what is resolved from the rich text content field.

Now if we go to the example blog post it should look something like this:

Blog post page

Creating the All Articles and Article Card Components

Let’s create ArticleCard.vue in ~/components:

<template>
    <NuxtLink
      :to="'/' + slug"
      v-editable="article"
      class="w-full h-full border rounded-[5px] text-left 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">
        <h2 class="text-2xl text-[#1d243d] font-bold mb-1">
          {{ article.title }}
        </h2>
        <div class="text-gray-600 mb-3">{{ article.author }}</div>
        <div class="line-clamp-4">
          {{ article.description }}
        </div>
      </div>
    </NuxtLink>
  </template>

  <script setup>
  defineProps({ article: Object, slug: String });
  </script>
Enter fullscreen mode Exit fullscreen mode

In this file, we use <NuxtLink> to navigate to the corresponding article page. We display all necessary information we get from the article prop.

Now let’s create AllArticles.vue in ~/storyblok:

<template>
    <div class="py-24">
      <h2 class="text-6xl text-gray-800 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 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>
Enter fullscreen mode Exit fullscreen mode

Here we use the Storyblok API hook useStoryblokApi() and use it to fetch all stories, filtering them by our blog folder and making sure we don’t take the blog/ route. We then loop over the articles displaying them in our <ArticleCard> component.

All Articles Page

Deploying to Netlify

Now it’s time to deploy our Nuxt app to Netlify. Netlify is a web platform that includes build, deploy, and serverless backend services for web applications and dynamic websites. It will make it easy for us to deploy our blog in a few simple steps:

  1. Push your code to GitHub and remember to not push any secret keys
  2. Go to Netlify and select “Add new site” and Import an existing project

Netlify Deploy Existing

  1. Import your project from GitHub and set build command to: npm run generate
  2. Set up environment variables:

Add env variables

And you’re done. Now you should be able to visit your live website as soon as the build is deployed. (might need to refresh)

Last steps

  • Make sure you change the URL in Storyblok’s Visual Editor so you see the live version.
  • Let’s make sure to display drafts only in preview mode, otherwise display only published content. We can do this by looking at the URL query if it contains _storyblok.

In [...slug].vue and AllArticles.vue:

version: useRoute().query._storyblok ? 'draft' : 'published'
Enter fullscreen mode Exit fullscreen mode
  • Let’s setup Webhooks, so that once we create new content the site is redeployed automatically:
    • In Netlify go to Site Settings > Build & deploy and go to Build Hooks.
    • Create a new hook called Storyblok and copy the unique URL:

Build hooks

- Go to Storyblok’s settings and find Webhooks. Under the field “Story published & unpublished” paste the copied URL. Now everytime a story is published or unpublished the deploy will trigger.
Enter fullscreen mode Exit fullscreen mode

Storyblok hook

🎉. Now you are officially done! Congrats!

Useful Resources

https://www.storyblok.com/tutorials

https://nuxt.com/modules/storyblok

https://vueschool.io/courses/jamstack-the-complete-guide

https://www.netlify.com/

💚  I hope this tutorial will be useful to you. The guys at Storyblok have made it super easy for developers and non-developers to manage content. Go ahead and give it a try. Make sure to also follow me on my socials for more Vue content.

Twitter

LinkedIn

YouTube

💖 💪 🙅 🚩
alexandergekov
Alexander Gekov

Posted on February 20, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related