Contents

Add a headless CMS to NuxtJs

The main goal of NuxtJs is to create a framework flexible enough that you can use it as a main project base or in addition to your current project based on Node.js. So in this quick walkthrough, we will have a look at how we can use the data from the Storyblok API with a NuxtJs project to create a landing page. At the end of this article, you will have a NuxtJs project which renders components filled with data from the Storyblok API.

Nuxt JS Logo

We’re about to use Storyblok as headless CMS - so what is it?

Let me start with a short explanation of Storyblok: It is a hosted headless CMS in which you can create nested components per content entry. The author of one content entry, therefore, can create components that act as Content-type like articles or products but also easily can create nestable components to create landing pages - but would allow you to add Storyblok to existing solutions to enrich your current content as well.

Storyblok Explained

What are we going to build?

During this article, we will use the default component page which acts as layout/content-type and the nested components teaser, grid, and feature to create a sample layout for landing pages. You will receive this setup during this tutorial, however, if you’ve already created a space that structure is already available in the content entry with the name “home”.

Basic Setup Storyblok Nuxt

Let’s start with NuxtJS

Requirements

Installation

To get started quickly, the Nuxt.js team has created a starter template, which we will extend in this tutorial to load data from the Storyblok API.

npx create-nuxt-app mynuxt

After that we need to navigate to our newly created project folder and execute:

cd mynuxt && npm build

to finally launch your nuxtJs project simply write:

npm run dev

You should now be able to visit http://localhost:3000/ already.

nuxt default

Let’s create a Storyblok space

If you’ve not registered already simply sign up using the web-interface. You will be guided through the creating of your first space.

Create Space

The default set of components and a content entry called “home” will be generated for you. You will also see the on-boarding with some code examples in different programming languages by clicking on that “home” content-entry. It also includes a client-side VueJs example.

Storyblok On boarding

You will also see the preview token (sometimes called private token) which allows us to load draft versions of your content, you can copy that from one of the code examples or from your space dashboard. We will need this token in the next steps because we’re about to load that!

Install storyblok-nuxt to allow access to the Storyblok API

Storyblok provides a NuxtJS Module which is a thin wrapper for the api (available at this.$storyapi) and the Storyblok Bridge (available at this.$storyblok). So what we will have to do is simply install this dependency and configure it in our nuxt.config.js.

npm install storyblok-nuxt --save

Configure the module in your nuxt.config.js. Make sure to exchange this accessToken with your own as otherwise you will receive a 404 or 401 since you won’t have the permission to edit that tutorial space later on. You will receive your own preview token during the onboarding in your space.

...
  modules: [
    ['storyblok-nuxt', {
      accessToken: 'YOUR_PREVIEW_TOKEN',
      cacheProvider: 'memory'
    }]
  ]
...

Create a dynamic route in Nuxt

To allow a generation of pages according to your content in Storyblok, we will add dynamic routes to nuxt. We’re going to call the file _slug.vue in the /pages since the paths of your content entries in Storyblok also can be found by their slug (path).

We’ve named the file pages/_slug.vue, but you can name it as you want.

<template>
  <section>
    {{story.name}}
  </section>
</template>

<script>
export default {
  data () {
    return { story: { content: {} } }
  },
  asyncData (context) {
    // Check if we are in the editor mode
    let version = context.query._storyblok || context.isDev ? 'draft' : 'published'

    // Load the JSON from the API
    return context.app.$storyapi.get(`cdn/stories/${context.params.slug}`, {
      version: version
    }).then((res) => {
      return res.data
    }).catch((res) => {
      context.error({ statusCode: res.response.status, message: res.response.data })
    })
  }
}
</script>

You can see that we’re using context.app.$storyapi which will be registered by the storyblok-nuxt module.

Our first component

Since we don’t only want to display the name of those content entries from Storyblok - we need to access the actual content. You can see that we’re already loading content from the Content Delivery API Storyblok according to the route parameters. By default, you will be able to access the home slug.

The first component in the JSON result the Storyblok API returns is called page and acts as a content type. That component only contains one field called body which is a simple array of other components. We’re going to use VueJS dynamic components to allow dynamic importing of those nested components, since there can be different content-type we will include that base component as shown next.

Simply replace the template part of the pages/_slug.vue with the following content:

<template>
  <section>
    <component v-if="story.content.component" :key="story.content._uid" :blok="story.content" :is="story.content.component"></component>
  </section>
</template>

This allows us to create different content types without changing that pages/_slug.vue at all. It will try to include our first component which is called page so let’s create that Page.vue in the components folder.

The Page component

Let’s create the file components/Page.vue with the content below:

<template>
  <div class="page">
    <component :key="blok._uid" v-for="blok in blok.body" :blok="blok" :is="blok.component"></component>
  </div>
</template>

<script>
export default {
  props: ['blok']
}
</script>

You can see that the only script which is necessary to access the data is the property blok which was passed in the include by _slug.vue. Since this component only contains that body field - we’re going to include all nested components the same way as before by using dynamic components.

We would have to register our component now in the _slug.vue - but since we don’t want to end up with so many import statements, let’s create a quick plugin that allows us to import all components we need at one point.

Add the file plugins/component.js (name it as you want) with the following content:

import Vue from 'vue'
import Page from '@/components/Page.vue'

Vue.component('page', Page)

and extend your nuxt.config.js by the following line:

plugins: ['~/plugins/components.js']

Now we’re already able to include the Page.vue. The page component of the home content entry has two nested components. The first one is called teaser and the second would be grid.

Create our content components.

The page component before defines the content-type and of course could already contain fields. Think of a component called post which instead of using a field called body has a field title, text and image and you would already be able to create a blog. But let’s focus on the components already generated.

Simply create components/Teaser.vue, components/Grid.vue and extend the plugins/components.js as shown below.

The Teaser component

The teaser component contains one field called headline so we’re going to use that like:

<template>
  <div class="teaser">
    <h1>{{blok.headline}}</h1>
  </div>
</template>

<script>
export default {
  props: ['blok']
}
</script>

you can see that the Page.vue also passes a property called blok with the information of that teaser component, so we’re able to directly access the content of that component using that blok property.

The Grid component

The grid component mainly looks the same as the teaser component above, the main difference is that it (similar to the page component) only contains an array field called columns to allow even deeper nesting.

Let’s create the components/Grid.vue with the following content:

<template>
  <div class="grid">
    <component :key="blok._uid" v-for="blok in blok.columns" :blok="blok" :is="blok.component"></component>
  </div>
</template>

<script>
export default {
  props: ['blok']
}
</script>

The last component we need to create will be the Feature.vue, which is currently only used as a nested component of the Grid.vue.

The Feature component

The feature component only contains a field called name at the moment, so the only task to be done is to output that field as we did already in the teaser above.

<template>
  <div class="column feature">
    {{blok.name}}
  </div>
</template>

<script>
export default {
  props: ['blok']
}
</script>

The plugins/component.js

The final step - before anything is rendered is to add the reference to those components in our plugins/components.js. So let’s add those components there as well.

import Vue from 'vue'
import Page from '@/components/Page.vue'
import Teaser from '@/components/Teaser.vue'
import Grid from '@/components/Grid.vue'
import Feature from '@/components/Feature.vue'

Vue.component('page', Page)
Vue.component('teaser', Teaser)
Vue.component('grid', Grid)
Vue.component('feature', Feature)

You should now be able to see the basic structure already, sadly without any styles. I’ve added some simple styles to the components to make it look better than plain HTML.

Well done so far! Let’s prepare the editing!

Now you should have seen the key concept of Storyblok and those nested components if you only need flat structures simply create a new component as content-type and add your required fields directly.

The next thing is the way you and your content creators will be able to edit the content of such components. For this purpose, we will use the Storyblok JavaScript Bridge which will be added by the storyblok-nuxt module. It’s actually a simply client side script tag.

To allow that editing we will have to update our components, in the draft version of the content from the Storyblok API each component ships with the _editable property - containing an HTML comment, which should be added in front of the components you want to edit, since the single file components (.vue) only allow one root tag, we’ve created a Vue Directive to still be able to use the information which is needed.

Storyblok editable directive

The before installed storyblok-nuxt module ships that v-editable directive. The only step we need to do ourself is to use that directive as we did in the Teaser.vue below in all .vue components we’ve created above.

<template>
  <div v-editable="blok" class="teaser">
    <h1>{{blok.headline}}</h1>
  </div>
</template>

<script>
export default {
  props: ['blok']
}
</script>

Simply add the attribute v-editable="blok" to all your components (currently page, teaser, grid, and feature).

Make sure you’ve replaced the accessToken in the nuxt.config.js with your own previewToken.

Rebuild on content change

Since we don’t want to reload the whole frame by our own since we can simply subscribe to an event the Storyblok JavaScript Bridge provides us. Since those events are only available on the client side, we will have to subscribe to that event as the component is mounted.

Simply update pages/_slug.vue by adding:

mounted () {
  this.$storybridge.on(['input', 'published', 'change'], (event) => {
    if (event.action == 'input') {
      if (event.story.id === this.story.id) {
        this.story.content = event.story.content
      }
    } else {
      window.location.reload()
    }
  })
},

Which will reload as the save or publish button in the Storyblok editor was hit.

Embed your local environment as preview source

The last step in the onboarding (and to finally allow you to edit your components) simply enter localhost:3000/ in the input at the bottom of the onboarding screen in Storyblok. It will switch to your local address directly - you can change that later in your space settings of course.

Nuxt embedded in Storyblok

Summary

Using Storyblok as your CMS with NuxtJS is, as you can see, simply loading JSON from an API. Since the content provided is structured in components - you can directly use the setup prepared by Nuxt and Vue. The visual editor can be plugged-in pretty straightforward and only needed for that instant preview and not for production. I’ve really enjoyed using NuxtJs because it uses VueJs everywhere I looked - and as you may have noticed - Storyblok is also made completely with VueJs. I would love to receive and read your feedback and maybe have a look at some of your implementations. As always you can download source code from Github of our Nuxt.js boilerplate and comment below.

ResourceDescription
NuxtJsUniversal Vue.js Applications
https://nuxtjs.org/
VueJsThe Progressive JavaScript Framework
https://vuejs.org/
StoryblokComponent and Headless CMS as a Service
https://www.storyblok.com/
AxiosPromise based HTTP client for the browser and node.js
https://github.com/axios/axios
GithubA project with NuxtJs and Storyblok
https://github.com/storyblok/vue-nuxt-boilerplate

More to read...

About the author

Dominik Angerer

Dominik Angerer

A web performance specialist and perfectionist. After working for big agencies as a full stack developer he founded Storyblok. He is also an active contributor to the open source community and one of the organizers of Stahlstadt.js.