Storyblok Raises $80M Series C - Read News

What’s the True Total Price of Enterprise CMS? Find out here.

Skip to main content

Add a headless CMS to VueJS in 5 minutes

In this short article, we’ll have a look at how we can use data from the Storyblok API with a Vue.js project to create a website. At the end of this article, you will have a Vue.js Application that renders components filled with data from Storyblok.

In case you don’t know what’s a Headless CMS or what Storyblok does, please read first the Storyblok Guide.

Environment Setup

Requirements

To follow this tutorial make sure to meet these requirements:

  • Basic understanding of Vue.js and Javascript
  • Node.js LTS version
  • An account in the Storyblok App

Create a Vue.js project

Let’s use Vue.js version 3 for this tutorial, since it is now the new default.

Following the Vue.js official installation guide, we can create our project using the official Vue project scaffolding tool: create-vue. It’s Vite based and uses all new recommendations, meaning you’ll get an incredible DX. Use it by running:

        
      npm init vue@latest
    

Let's choose the following options:

  • Add TypeScript? No
  • Add JSX Support? No
  • Add Vue Router for Single Page Application development? No
  • Add Pinia for state management? No
  • Add Vitest for Unit Testing? No
  • Add Cypress for both Unit and End-to-End testing? No
  • Add ESLint for code quality? No

Once you install the dependencies and run npm run dev in the project folder, you’ll see this screen when you open http://localhost:3000 in your browser:

Welcome screen of the Vue project after running npm run dev.

Configuration of the space

Create a new space in the Storyblok app by choosing the Create new space {1} option. Pick a name for it {2}.

1
2

Create a new space in Storyblok

Now, a Storyblok space with sample content has been created. If you open the Home story you will see the Visual Editor on your screen:

Default screen on the Home story of the Visual Editor

Default screen on the Home story of the Visual Editor

hint:

Understand what represents Story in our Storyblok Guide.

Enabling the Visual Editor

To see your website in Storyblok Visual Editor you need to set the default environment URL. For that, in your space open Settings > Visual Editor {1} and set the Location field to https://localhost:3000 {2}:

Setting a default environment URL
1
2

Setting a default environment URL

Storyblok v2 requires that your app is served via HTTPS. Luckily, this is very easily achievable by setting the following option in your vite.config.js and installing the SSL plugin for the localhost certificates @vitejs/plugin-basic-ssl

        
      npm install @vitejs/plugin-basic-ssl -D
    

vite.config.js
        
      export default defineConfig({
  plugins: [
    vue(),
    basicSsl(),
  ],
  server: {
    port: 3000,
    https: true,
  }
})
    

Now go back to the Home story under the Content section. When you open it, you’ll see the Visual Editor, but you won’t see yet your Vue application in there.

Just open the Entry configuration {1} on the right-hand form, and set the Real Path to “/” {2}. Save, and if you’re still running your Vue app you will see it now in the Visual Editor.

1
2

Overriding the real path for the Home story

Connecting Vue to Storyblok

First of all, let’s install @storyblok/vue, our official SDK for Vue 3:

        
      npm install @storyblok/vue
    

The SDK allows you to interact with Storyblok API and enable the real-time editing experience. Let's configure it.

First of all, you need to grab your API token from your space Settings > API-Keys:

1
2
3

Where to get the preview access token of your Storyblok space.


Then add the preview API token in a .env file:

.env
        
      VITE_STORYBLOK_PREVIEW_TOKEN=your_preview_access_token_goes_here
    
src/main.js
        
      import { createApp } from 'vue';
import { StoryblokVue, apiPlugin } from '@storyblok/vue';
import App from './App.vue';

const app = createApp(App);

app.use(StoryblokVue, {
  accessToken: import.meta.env.VITE_STORYBLOK_TOKEN,
  bridge: import.meta.env.NODE_ENV !== 'production', // optimizes by excluding the bridge on production
  use: [apiPlugin],
});

app.mount('#app');
    

Setting the correct region

Depending on whether your space was created in the EU, the US, Australia, Canada, or China, you may need to set the region parameter of the API accordingly:

  • eu (default): For spaces created in the EU
  • us: For spaces created in the US
  • ap: For spaces created in Australia
  • ca: For spaces created in Canada
  • cn: For spaces created in China

Here's an example of a space created in the US:

        
      app.use(StoryblokVue, {
  // Other options
  apiOptions: {
     region: "us",
  },
});

    
warn:

Note: For spaces created in the United States or China, the region parameter must be specified.


Displaying Components on the Vue App

The idea when using Storyblok for this case is:

  • A content manager (even if it’s yourself) can create pages (or stories) composed of different sections, called components.
  • As a developer, you get the page as a JSON structure using Storyblok API and render the components (which you have to implement in your Vue App).

When you create a space, Storyblok creates 4 components by default for you:

  • feature (nestable component)
  • grid (nestable component)
  • teaser (nestable component)
  • page (content type)

You can find them all in the Components section of your Storyblok space.

hint:

Understand the difference between the nestable components and content type in our Structures of Content tutorial.

Create Vue Components

Let’s implement the previous 4 components in your Vue App, right under the components folder.

components/Feature.vue
        
      <script setup>
defineProps({ blok: Object })
</script>
 
<template>
  <div v-editable="blok" class="p-4 shadow-lg rounded-lg">
    <h1 class="text-lg font-bold text-gray-600">{{ blok.name }}</h1>
  </div>
</template> 
    
components/Grid.vue
        
      <script setup>
defineProps({ blok: Object })
</script>

<template>
  <div v-editable="blok" class="flex py-8 mb-6">
    <div v-for="inblok in blok.columns" :key="inblok._uid" class="flex-auto px-6">
      <StoryblokComponent :blok="inblok" />
    </div>
  </div>
</template>
    
components/Page.vue
        
      <script setup>
defineProps({ blok: Object })
</script>

<template>
  <div v-editable="blok" class="px-6">
    <StoryblokComponent
      v-for="inblok in blok.body"
      :blok="inblok"
      :key="inblok._uid"
    />
  </div>
</template>
    
components/Teaser.vue
        
      <script setup>
defineProps({ blok: Object })
</script>
 
<template>
  <div v-editable="blok" class="py-8 mb-6 text-5xl font-bold text-center text-gray-600">
    {{ blok.headline }}
  </div>
</template>
    

You can see in their template that the Teaser.vue component has a headline property, and that Feature.vue has a name.

The question is: how do you know what properties have a blok? You can check the component schemas to find out.

Finally, make these components available by loading them from main.js:

main.js
        
      import App from "./App.vue";
import Grid from "./components/Grid.vue";
import Page from "./components/Page.vue";
import Teaser from "./components/Teaser.vue";
import Feature from "./components/Feature.vue";

const app = createApp(App);
// ...
app.component("Grid", Grid);
app.component("Page", Page);
app.component("Teaser", Teaser);
app.component("Feature", Feature);
    

You can also auto-import all the components under /components by using this code instead:

        
      // Automatically import all Vue components from the /components directory
const modules = import.meta.glob('./components/**/*.vue');

for (const path in modules) {
  modules[path]().then((mod) => {
    // Extract the component name from the file path
    const componentName = path
      // Remove the "./components/" from the beginning
      .replace('./components/', '')
      // Remove the file extension from the end
      .replace(/\.\w+$/, '')
      // Split up kebabs and slashes
      .split(/[-/]/)
      // Convert to PascalCase (CamelCase with the first letter capitalized)
      .map(part => part.charAt(0).toUpperCase() + part.slice(1))
      // Concatenate
      .join('');

    // Globally register the component
    app.component(componentName, mod.default);
  });
}
    

Adding TailwindCSS

As you may have noticed, the components we just created utilize Tailwind classes.

        
      npm install -D tailwindcss
npx tailwindcss init
    

Then add the paths of your templates files.

tailwind.config.js
        
      /** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
    

Add the Tailwind directives to your CSS

src/styles/index.css
        
      @tailwind base;
@tailwind components;
@tailwind utilities;
    

Load content using the API

Now that we have our components ready, it’s time to get the Home story data. You can view the JSON structure of any story by clicking the Draft JSON button in the Visual Editor.

1
2

Get Draft JSON from Visual Editor

From the code side, first create a pages/Home.vue component that uses the useStoryblokApi from @storyblok/vue to fetch the content:

pages/Home.vue
        
      <script setup>
import { useStoryblok } from '@storyblok/vue';
const story = await useStoryblok('home', { version: 'draft' });
</script>

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

Now import it in your App.vue. Because we’re using top-level await in Home.vue, you need to use Suspense, a Vue 3 feature that allows you to have control over the asynchronous data flow.

App.vue
        
      <script setup>
import Home from "./pages/Home.vue";
</script>

<template>
  <Suspense>
    <template #default>
      <Home />
    </template>

    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>
    

If using vue-router, a RouterView component will be necessary to view all pages within the project. If that's the case, instead of the above code in App.vue you can wrap the router component with Suspense.

App.vue
        
      <script setup>
import { RouterLink, RouterView } from "vue-router";
</script>

<template>
  <header>
    <div class="wrapper">
      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
      </nav>
    </div>
  </header>

  <Suspense>
    <RouterView />
  </Suspense>
</template>
    

At this point, if you navigate to your Home page on Storyblok Visual Editor, you should see the components being rendered successfully.

Vue app integrated into Storyblok Visual Editor

Real-time editing with Storyblok Bridge

The power of Storyblok relies on its real-time editing experience. Play with changing the teaser headline or re-arranging the features and see the magic happen!

Luckily, @storyblok/vue makes it very easy for you. In order to make this real-time editing experience possible, your components have to be connected with Storyblok and listen to changes by its Visual Editor. Let's take a closer look at how this is achieved:

First, to link your Vue and Storyblok components together, @storyblok/vue automatically registers a v-editable directive. If you check Feature.vue, Grid.vue, and the rest under the components folder, you'll see you already have it there:

components/Feature.vue
        
      <div v-editable="blok" class="py-2">
  <!-- ... -->
</div>
    

Second, @storyblok/vue loads Storyblok Bridge under the hood and exposes a one-liner useStoryblokBridge function that you can use to listen for Storyblok Visual Editor changes and update your component state, accordingly.

In our example, we're using the short form syntax by importing useStoryblok in pages/Home.vue. This automatically loads Storyblok Bridge and enables it for you!

In case you would like to work with useStoryblokApi and useStoryblokBridge separately, you can learn how to accomplish that in our docs on GitHub. Alternatively, you can check out the long form in our live demo.

Real-time editing experience enabled

Wrapping Up

Congrats on reaching this point! You have a Vue 3 app fully connected to Storyblok using its API, bringing a true real-time editing experience.

Can it be even more simple? Yes. Check how to do it with Nuxt.js and find out how our @storyblok/nuxt module helps by doing some of these steps for you.

LIVE DEMO:

If you’re in a hurry, try yourself the live demo in Stackblitz!

WHAT'S NEXT?:

We (Storyblok) are huge fans of the Nuxt.js and the Vue.js universe. The Storyblok app is built in Vue.js and we are proud of it. As Nuxt.js is the all-in-one Vue.js meta-framework, we’ve built a Tech Hub to help you with the next steps on your Vue.js and Nuxt.js journey.

Author

Alex Jover Morales

Alex Jover Morales

Vue team member, passionate about communication and open source. Alex created vuedose.tips, the book "Testing Vue components with Jest" and several articles and courses on platforms like Egghead.io and Digital Ocean. He co-organizes Alicante Frontend and Vue Day Spain communities and created widely-used libraries such as tslint-config-prettier or v-lazy-image. In his free time, you'll find him traveling, enjoying outdoor activities, and having beers with friends 🌎🏋️‍♂️🍻