👨‍🏫 The Mastering Nuxt 3 course is now completed!

Discover the course

Storyblok Nuxt.js module

Storyblok Logo


Nuxt 3 module for the Storyblok, Headless CMS.

Storyblok JS Client npm

Follow @Storyblok
Follow @Storyblok

Live Demo

If you are in a hurry, check out our official live demo on Stackblitz.

🚀 Usage

Note: This module is for Nuxt 3. Check out @storyblok/nuxt-2 for Nuxt 2.

If you are first-time user of the Storyblok, read the Getting Started guide to get a project ready in less than 5 minutes.


Install @storyblok/nuxt:

npm install @storyblok/nuxt
# yarn add @storyblok/nuxt

Add following code to modules section of nuxt.config.js and replace the accessToken with API token from Storyblok space.

import { defineNuxtConfig } from "nuxt";
export default defineNuxtConfig({
modules: [
["@storyblok/nuxt", { accessToken: "<your-access-token>" }]
// ...

You can also use the storyblok config if you prefer:

import { defineNuxtConfig } from "nuxt";
export default defineNuxtConfig({
modules: ["@storyblok/nuxt"],
storyblok: {
accessToken: "<your-access-token>"

⚠️ This SDK uses the Fetch API under the hood. If your environment doesn't support it, you need to install a polyfill like isomorphic-fetch. More info on storyblok-js-client docs.


When you initialize the module, you can pass all @storyblok/vue options plus a bridge option explained in our JS SDK Storyblok bridge section.

Note: For spaces created in the United States, you have to set the region parameter accordingly { apiOptions: { region: 'us' } }.

// Defaults
["@storyblok/nuxt", {
accessToken: "<your-access-token>",
bridge: true,
apiOptions: {}, // storyblok-js-client options

Getting started

1. Creating and linking your components to Storyblok Visual Editor

To link your Vue components to the equivalent one in your Storyblok space:

  • First, you need to load them globally adding them to the ~/storyblok directory. It's important to name them with Pascal case in your code ExampleComponent.vue and with a hyphen inside your Storyblok space example-component, so they will be imported automatically.
    Otherwise, you can set another directory and load them manually (for example, by using a Nuxt plugin).

    Take into account that if you name a component inside the storyblok folder the same as another in the components folder, it won't work properly. Tip: Keep the components in your Nuxt project with different names.

  • For each components, use the v-editable directive on its root element, passing the blok property that they receive:
<div v-editable="blok" / >
  • Finally, use <StoryblokComponent> which is available globally in the Nuxt app:
<StoryblokComponent :blok="blok" />

The blok is the actual blok data coming from Storblok's Content Delivery API.

2. Getting Storyblok Stories and listen to Visual Editor events

Composition API

The simplest way is by using the useAsyncStoryblok one-liner composable (it's autoimported) and passing as a first parameter a name of your content page from Storyblok (in this case, our content page name is vue, by default you get a content page named home):

If you want to know more about versioning { version: "draft" /* or "publish" */ } then go to the section Working with preview and/or production environments

<script setup>
const story = await useAsyncStoryblok("vue", { version: "draft" });
<StoryblokComponent v-if="story" :blok="story.content" />

Which is the short-hand equivalent to using useStoryblokApi inside useAsyncData and useStoryblokBridge functions separately:

<script setup>
const story = ref(null);
const storyblokApi = useStoryblokApi();
const { data } = await useAsyncData(
async () => await storyblokApi.get(`cdn/stories/vue`, {
version: "draft"
story.value = data.value.data.story;
onMounted(() => {
useStoryblokBridge(story.value.id, (evStory) => (story.value = evStory));
<StoryblokComponent v-if="story" :blok="story.content" />

Using useAsyncData SSR and SSG capabilities are enabled.

Rendering Rich Text

You can easily render rich text by using the renderRichText function that comes with @storyblok/nuxt and a Vue computed property:

<div v-html="articleContent"></div>
<script setup>
const props = defineProps({ blok: Object });
const articleContent = computed(() => renderRichText(blok.articleContent));

You can also set a custom Schema and component resolver by passing the options as the second parameter of the renderRichText function:

<script setup>
import cloneDeep from "clone-deep";
const mySchema = cloneDeep(RichTextSchema); // you can make a copy of the default RichTextSchema
// ... and edit the nodes and marks, or add your own.
// Check the base RichTextSchema source here https://github.com/storyblok/storyblok-js-client/blob/v4/source/schema.js
const props = defineProps({ blok: Object });
const articleContent = computed(() =>
renderRichText(props.blok.articleContent, {
schema: mySchema,
resolver: (component, blok) => {
switch (component) {
case "my-custom-component":
return `<div class="my-component-class">${blok.text}</div>`;
return "Resolver not defined";

3. Working with preview and/or production environments

Remember that the bridge only works using version: 'draft' and the Preview Access Token.

For the production site, NOT used as a preview for content editors, version: 'published' and Public Access Token should be used.

If you're using production as a preview for marketeers and your public site, you will need a plugin to handle different .env variables, or versions using the Preview Access Token, checking if you are inside Storyblok or not. For example, something like if (window.location.search.includes(_storyblok_tk[token]=<YOUR_TOKEN>).

Check the official docs on how to access different content versions.


useAsyncStoryblok(slug, apiOptions, bridgeOptions)

(Recommended Option) Use useAsyncData and useState under the hood for generating SSR or SSG applications.

Check the available apiOptions (passed to storyblok-js-client) and bridgeOptions (passed to the Storyblok Bridge).

useStoryblok(slug, apiOptions, bridgeOptions)

It could be helpful to use useStoryblok instead of useAsyncStoryblok when we need to make client-side requests, for example, getting personalized data for a logged user.

Check the available apiOptions (passed to storyblok-js-client) and bridgeOptions (passed to the Storyblok Bridge).


Returns the instance of the storyblok-js-client.

useStoryblokBridge(storyId, callback, bridgeOptions)

Use this one-line function to cover the most common use case: updating the story when any kind of change happens on Storyblok Visual Editor.

ℹ️ More Resources



Please see our contributing guidelines and our code of conduct. This project use semantic-release for generate new versions by using commit messages and we use the Angular Convention to naming the commits. Check this question about it in semantic-release FAQ.