⚠️Draft Content

Storyblok Raises $80M Series C - Read News

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

Skip to main content

How to build a multilanguage website with Gridsome

Try Storyblok

Storyblok is the first headless CMS that works for developers & marketers alike.

In this tutorial we will show you how to create a Gridsome project with Storyblok by using the plugin gridsome-source-storyblok. Step by step we will build some components, using the live preview functionality and add translations. The final result will be a multi language website with some example content:

You can clone the boilerplate that of this tutorial at: https://github.com/storyblok/gridsome-source-storyblok

Environment Setup

Requirements

To follow this tutorial there are following requirements:

  • Understanding of Gridsome and Vue.js
  • Node, yarn (or npm) and npx installed
  • An account on Storyblok.com to manage content

Setup the Project

Install the Gridsome boilerplate with the initial files:

        
      npx gridsome create mygridsome
    

Install Storyblok's Gridsome source plugin. Under the hood, this plugin will inject Storyblok's content items to Gridsome's GraphQL Data Layer and initialise the Storyblok plugin for Vue.js.

        
      yarn add gridsome-source-storyblok
    

Edit the gridsome.config.js file to use the plugin and set your token:

        
      module.exports = {
  siteName: 'MyGridsome',
  plugins: [
    {
      use: 'gridsome-source-storyblok',
      options: {
        client: {
          accessToken: '<YOUR_ACCESS_TOKEN>' // you must be replace with your token
        }
      }
    }
  ]
}
    

Run the Gridsome development server with:

        
      yarn develop # or npm run develop
    

Open your browser, go to url http://localhost:8080 and you will see the following page:

Load Data to the GraphQL Data Layer

Edit the file gridsome.server.js to load the data from Storyblok's API and create the pages using the full_slug attribute.

        
      module.exports = function (api) {
  api.loadSource(({ addCollection }) => {
    // Use the Data Store API here: https://gridsome.org/docs/data-store-api/
  })

  api.createPages(async ({ graphql, createPage }) => {
    // load data from Storyblok API
    const { data } = await graphql(`{
      allStoryblokEntry {
        edges {
          node {
            id
            full_slug
          }
        }
      }
    }`)

    // for each content found create a page
    data.allStoryblokEntry.edges.forEach(({ node }) => {
      createPage({
        path: `/${node.full_slug}`,
        component: './src/templates/StoryblokEntry.vue',
        context: {
          id: node.id
        }
      })
    })
  })
}
    

Define the path for the homepage

By default is the content of the homepage covered by Home content entry in Storyblok. This means that previous code would not generate index.html in the root of your website, but in the folder home like this home/index.html. To override this, you just need to update the code of the function data.allStoryblokEntry.edges.forEach(({ node }) to the following code.

        
      data.allStoryblokEntry.edges.forEach(({ node }) => {
  if (node.full_slug === 'home') {
    createPage({
      path: '/',
      component: './src/templates/StoryblokEntry.vue',
      context: {
        id: node.id
      }
    })
  }
  createPage({
    path: `/${node.full_slug}`,
    component: './src/templates/StoryblokEntry.vue',
    context: {
      id: node.id
    }
  })
})
    

Create the StoryblokEntry.vue Template File

How will Gridsome know how to render data from the Storyblok API? Using Template Files. So lets create a file called StoryblokEntry.vue in src/templates:

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

<script>
export default {
  name: 'StoryblokEntryTemplate',
  computed: {
    story () {
      return this.$page.storyblokEntry
    }
  }
}
</script>

<page-query>
query StoryblokEntry ($id: ID) {
  storyblokEntry (id: $id) {
    id
    slug
    content
  }
}
</page-query>
    

Setup Components

When you create a space in Storyblok you will see a default page called Home. This page contains multiple components, like Page, Teaser, Feature and Grid. So we will create this components and register them globally as Vue.js components in src/components

Page Component

Create a file called Page.vue in src/components:

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

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

Teaser Component

Create a file called Teaser.vue in src/components:

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

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

Grid Component

Create a file called Grid.vue in src/components:

        
      <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>
    

Feature Component

Create a file called Feature.vue in src/components:

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

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

Register the Components

Now register the components as global in src/main.js:

        
      // This is the main.js file. Import global CSS and scripts here.
// The Client API can be used here. Learn more: gridsome.org/docs/client-api

import DefaultLayout from '~/layouts/Default.vue'
import Page from '~/components/Page.vue'
import Teaser from '~/components/Teaser.vue'
import Feature from '~/components/Feature.vue'
import Grid from '~/components/Grid.vue'

export default function (Vue, { router, head, isClient }) {
  // Set default layout as a global component
  Vue.component('Layout', DefaultLayout)

  // register components
  Vue.component('Page', Page)
  Vue.component('Teaser', Teaser)
  Vue.component('Feature', Feature)
  Vue.component('Grid', Grid)
}
    

Open your browser, go to http://localhost:8080/home and you will see the following page:

Adding some Styles

The last step is to add some example css styles in the layout file. Create the file index.html in the folder src and add the following content:

        
      <!DOCTYPE html>
<html ${htmlAttrs}>

<head>
  ${head}
  <link rel="stylesheet"
    href="https://rawgit.com/DominikAngerer/486c4f34c35d514e64e3891b737770f4/raw/db3b490ee3eec14a2171ee175b2ee24aede8bea5/sample-stylings.css">
</head>

<body ${bodyAttrs}>
  ${app}
  ${scripts}
</body>

</html>
    

Now restart your server and open the page again. It's better, isn't it?

Live Preview

To use Storyblok's live preview feature it is necessary to do the following changes:

  1. Add a page for the Editor
  2. Add v-editable directives in the components

Add the Editor Page

The Editor page will be a page that syncs the changes from Storyblok. Create the file Editor.vue in src/pages with the following content:

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

<script>
const getParam = function(val) {
  var result = '',
      tmp = []
  location.search
    .substr(1)
    .split('&')
    .forEach(function (item) {
      tmp = item.split('=')
      if (tmp[0] === val) result = decodeURIComponent(tmp[1])
  })
  return result
}

const loadStoryblokBridge = function(cb) {
  let script = document.createElement('script')
  script.type = 'text/javascript'
  script.src = `//app.storyblok.com/f/storyblok-latest.js?t=${getParam('token')}`
  script.onload = cb
  document.getElementsByTagName('head')[0].appendChild(script)
}

export default {
  name: 'EditorPage',
  data() {
    return {
      story: {content: {}},
      oldPath: ''
    }
  },
  computed: {
    globalData () {
      return this.$page.global.edges[0].node
    }
  },
  mounted() {
    loadStoryblokBridge(() => { this.initStoryblokEvents() })
  },
  methods: {
    loadStory() {
      const path = getParam('path')

      if (path !== '') {
        this.oldPath = path
      }

      window.storyblok.get({
        slug: path || this.oldPath,
        version: 'draft'
      }, (data) => {
        this.story = data.story
      })
    },
    initStoryblokEvents() {
      this.loadStory()
      let sb = window.storyblok
      sb.on(['change', 'published'], (payload) => {
        this.loadStory()
      })
      sb.on('input', (payload) => {
        if (this.story && payload.story.id === this.story.id) {
          payload.story.content = sb.addComments(payload.story.content, payload.story.id)
          this.story = payload.story
        }
      })
      sb.pingEditor(() => {
        if (sb.inEditor) {
          sb.enterEditmode()
        }
      })
    }
  }
}
</script>
    

Add the v-editable Directive

Now add the v-editable directive in the components Teaser, Feature and Grid like this:

        
      <template>
  <!-- FEATURE COMPONENT -->
  <div class="column feature"
       v-editable="blok">
    {{ blok.name }}
  </div>
</template>

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

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

As last step open your Storyblok space and set the Location setting to http://localhost:8080/editor/?token=<YOUR_TOKEN>&path=:

When you access the Home page in your space it will open the Live Preview and show your project inside an iframe:

Now you are able to edit components, change the text and add other components and the live preview will be automatically sync the changes that you made.

Setup Multilanguage Capabilities

In Storyblok you have two options for internationalisation:

  1. Field level translation: Each field marked by translatable will have a translation and you have a single content item for all translations.
  2. Multi-tree translation: Each translation will have it's own content item and the translations are saved in different folders.

For this tutorial, we use the first option.

Add the new language in Space Settings->Languages like in the following screenshot:

Storyblok's source plugin already will automatically load all content for each language. Try it out and translate some fields:

In the last step we will edit the component src/layouts/Default.vue to add a language switch:

        
      <template>
  <div class="layout">
    <header class="header">
      <strong>
        <g-link to="/">{{ $static.metadata.siteName }}</g-link>
      </strong>
      <nav class="nav">
        <!-- Changes are here -->
        <g-link class="nav__link" to="/home">English</g-link>
        <g-link class="nav__link" to="/pt/home">Portuguese</g-link>
      </nav>
    </header>
    <slot/>
  </div>
</template>
    

Restart your server and open http://localhost:8080/home. If all went well you can change the translation by clicking on the links in the upper right corner of the page.

Conclusion

Using the Gridsome source plugin we built a MultiLanguage website with live preview, learned how to navigate between different translations and load pages from the GraphQL Data Layer. You can clone this tutorial code in https://github.com/storyblok/storyblok-gridsome-boilerplate.

ResourceLink
Github repository of this tutorialhttps://github.com/storyblok/storyblok-gridsome-boilerplate
GridsomeGridsome
Vue.jsVue.js
Storyblok AppStoryblok