Build a blog with Nuxt 3 + Storyblok
Alexander Gekov
Posted on February 20, 2023
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 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.
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.
- When you have created your space you can go to Settings and then Access Tokens.
- 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.
And if you click on Home you will be greeted by the Visual Editor:
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
- 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.
Installation
In a newly scaffolded Nuxt 3 project, let’s run the following command:
npm install @storyblok/nuxt
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>" }]
]
})
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 }]
]
})
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
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'
]
})
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}'
]
}
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>
-
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>
-
storyblok/Feature.vue
:
<template>
<div v-editable="blok">
<h1 class="text-xl">{{ blok.name }}</h1>
</div>
</template>
<script setup>
defineProps({ blok: Object })
</script>
-
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>
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>
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>
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)
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>
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.
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.
Once you have done that, you can also create a Blog folder to keep everything organized and in one place:
Now you can create pages for new blog posts and they will be of type Article.
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.
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>
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:
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>
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>
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.
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:
- Push your code to GitHub and remember to not push any secret keys
- Go to Netlify and select “Add new site” and Import an existing project
- Import your project from GitHub and set build command to:
npm run generate
- Set up environment 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'
- 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:
- 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.
🎉. 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
💚 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.
Posted on February 20, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.