Add a headless CMS to VueJS in 5 minutes
Storyblok is the first headless CMS that works for developers & marketers alike.
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 (opens in a new window) .
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 (opens in a new window) .
Following the Vue.js official installation guide (opens in a new window) , 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}.
)
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
Understand what represents Story in our Storyblok Guide.
Enabling the Visual Editor
To see your website in Storyblok Visual Editor (opens in a new window) 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 (opens in a new window) {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 (opens in a new window)
npm install @vitejs/plugin-basic-ssl -D
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.
)
Overriding the real path for the Home story
Connecting Vue to Storyblok
First of all, let’s install @storyblok/vue (opens in a new window) , 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:
)
Where to get the preview access token of your Storyblok space.
Then add the preview API token in a .env
file:
VITE_STORYBLOK_PREVIEW_TOKEN=your_preview_access_token_goes_here
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 EUus
: For spaces created in the USap
: For spaces created in Australiaca
: For spaces created in Canadacn
: For spaces created in China
Here's an example of a space created in the US:
app.use(StoryblokVue, {
// Other options
apiOptions: {
region: "us",
},
});
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.
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.
<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>
<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>
<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>
<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:
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.
/** @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
@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.
)
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:
<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 (opens in a new window) in Home.vue, you need to use Suspense, a Vue 3 feature that allows you to have control over the asynchronous data flow.
<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.
<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:
<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 (opens in a new window) . Alternatively, you can check out the long form in our live demo (opens in a new window) .
)
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.
If you’re in a hurry, try yourself the live demo in Stackblitz!
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.
Resource | Link |
---|---|
Vue | https://vuejs.org/ |
Storyblok | https://www.storyblok.com/ |
Live Demo | https://stackblitz.com/edit/vue-5-minutes |
Storyblok Vue | https://github.com/storyblok/storyblok-vue |
Storyblok APIs | https://www.storyblok.com/docs/api |
Storyblok Nuxt Technology Hub | https://www.storyblok.com/tc/nuxtjs |