Build a blog with Nuxt 3 + Storyblok
Tags: javascript, vue, webdev, tutorial
Published: 2023-02-20 09:05:00

<meta property="twitter:image" content="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pfj3j7x9xontwmt42mxz.png">
<meta name="twitter:card" content="summary_large_image" />
## 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: 

{% embed https://youtu.be/gGXi0t1x-PM %}

## What is Storyblok?


![Storyblok website](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sgxn7yvg78kmkkcxm3t1.png)

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. 
* [Demo website](https://nuxt-storyblok-gekov.netlify.app)
* [GitHub Code](https://github.com/alexander-gekov/nuxt-storyblok-tutorial)

![final overview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bxxqpz1w81pycb2nsjfb.png)

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/84vwofg89jan45zivkxk.png)

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

![access token](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uw9ejaqwe3l17rku4n13.png)

* 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2mup7fg0lv4qmqacaatx.png)

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

![home page sample](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/du2400hs1hl2jez2uw7r.png)

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:

- Mac/Linux: [https://www.storyblok.com/faq/setup-dev-server-https-proxy](https://www.storyblok.com/faq/setup-dev-server-https-proxy)
- Windows: [https://www.storyblok.com/faq/setup-dev-server-https-windows](https://www.storyblok.com/faq/setup-dev-server-https-windows)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uonvc5vx7dzyjxpf6d0r.png)

## Installation

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

```shell
npm install @storyblok/nuxt
```

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

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

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.

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

## Adding Tailwind

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

```tsx
npm install --save-dev @nuxtjs/tailwindcss
```

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

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

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:

```tsx
module.exports = {
    content: [
        './storyblok/**/*.{html,js,ts,vue}'
    ]
}
```

## 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`:

```tsx
<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>
```

- `storyblok/Grid.vue`:

```tsx
<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>
```

- `storyblok/Feature.vue`:

```tsx
<template>
  <div v-editable="blok">
    <h1 class="text-xl">{{ blok.name }}</h1>
  </div>
</template>
 
<script setup>
defineProps({ blok: Object })
</script>
```

- `storyblok/Teaser.vue`:

```tsx
<template>
  <div v-editable="blok" class="py-16 text-5xl font-bold text-center">
    {{ blok.headline }}
  </div>
</template>
 
<script setup>
defineProps({ blok: Object })
</script>
```

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](https://www.storyblok.com/docs/api/content-delivery/v2?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-nuxt). 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:

```tsx
<template>
	<NuxtLayout>
    <NuxtPage/>
	</NuxtLayout>
</template>
```

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

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

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

`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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z6jt9qq9azg633opa1km.png)

## 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.

```tsx
<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>
```

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7lpmczpy0c24zlla9wsn.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mlaooqui56bc6rwi4vwk.png)

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

![Blog folder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/32p40t4z9rmp2rdplr0l.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tg9ztlswxvi0hat1muy0.png)
    

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r1zz5zo0g8rjjj0d78vc.png)

## Creating the Article component in Vue

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

```tsx
<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>
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rwbb5zk1xudp81n9dcl6.png)

## Creating the All Articles and Article Card Components

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

```tsx
<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>
```

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`:

```tsx
<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>
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h7ab8mz4pzfjt2arjle2.png)

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r21z0vtnfwq90uzk0qux.png)
    
3. Import your project from GitHub and set build command to: `npm run generate`
4. Set up environment variables:
    
![Add env variables](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s4lnacv0ycpo9vtgqg3o.png)
    

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`:

```tsx
version: useRoute().query._storyblok ? 'draft' : 'published'
```

- 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8d90kwqcuz5sj5zxnql3.png)
    
    - 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.
    
![Storyblok hook](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xr36noyxpijwbuo207wg.png)
    

🎉. Now you are officially done! Congrats!

## Useful Resources

[https://www.storyblok.com/tutorials](https://www.storyblok.com/tutorials)

[https://nuxt.com/modules/storyblok](https://nuxt.com/modules/storyblok)

[https://vueschool.io/courses/jamstack-the-complete-guide](https://vueschool.io/courses/jamstack-the-complete-guide)

[https://www.netlify.com/](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](https://twitter.com/AlexanderGekov)

[LinkedIn](https://www.linkedin.com/in/aleksandar-gekov/)

[YouTube](https://www.youtube.com/channel/UCAARceUUB_X6xsgeRBRg16A)