The complete guide to building a multilanguage website with Nuxt & Storyblok
Storyblok is the first headless CMS that works for developers & marketers alike.
This guide is for beginners and professionals who want to build a full-blown multilanguage website using Nuxt.js. With this step by step guide you will get a dynamic Nuxt.js website running on a Vercel (previously ZEIT now) server, using an API for the multilanguage content.
If you are in a hurry you can download the whole source code of the project at Github https://github.com/storyblok/nuxtjs-multilanguage-website or jump into one of the following chapters.
- Introduction
- Setting up the environment
- Building a skeleton
- Creating the content structure
- Connecting Storyblok with Nuxt.js
- Setting up a real-time Visual Editor
- Extending schema of bloks in Storyblok
- Creating article content type
- Showing Featured Articles on the homepage
- Creating articles overview page
- Adding another language
- Deploying to Vercel
The project in this article was developed using the following versions of these technologies:
- Nuxt.js v2.14.0
- Nodejs v12.3.1
- Npm v6.9.0
Keep in mind that these versions may be slightly behind the latest ones.
Initialization of the Nuxt.js App/Website
We will start by initializing the project with the Nuxt.js create app template. Navigate through the guide and choose the following options:
- JavaScript as the programming language
- Package manager of your choice (I used Yarn)
- TailwindCSS as UI Framework
- Axios module
- Linting tools of your choice (I used none in the sample project)
- Universal (SSR / SSG) rendering mode
- Static/JAMStack hosting as a deployment target
- Development tools of your choice (I used VS Code)
yarn create nuxt-app mywebsite // npx create-nuxt-app mywebsite cd mywebsite yarn dev // npm run dev
Nuxt.js starts its server on port 3000 by default so after running yarn dev (npm run dev) open your browser at http://localhost:3000. You should see a welcome screen of Nuxt.js.
)
Building a Skeleton
Since we are using TailwindCSS we don't need to define any special folder structure for our style. We can jump directly into the structuring and styling of our pages/components.
Defining the Default Layout
First, we will design the default layout of our website. Copy & paste the following code into the file layouts/default.vue
.
layouts/default.vue
<template> <div> <Header /> <Nuxt /> <Footer /> </div> </template> <script> import Header from '~/components/Header.vue' import Footer from '~/components/Footer.vue' export default { components: { Header, Footer } } </script>
Our default layout wants to use components named Header and Footer. We didn't create these components, so we will get the Nuxt.js error. Let's create them as the next step.
Creating a Header & Footer
Create two new files named ./components/Header.vue
and ./components/Footer.vue
with the following source code. At this moment we are only defining the layout of our page with the mock data.
./components/Header.vue
<template> <header class="max-w-5xl mx-auto py-8 flex"> <nav> <ul> <li> <nuxt-link class="text-teal-600 hover:underline" to="/"> Home </nuxt-link> </li> <li> <nuxt-link class="text-teal-600 hover:underline" to="/en/blog"> Blog </nuxt-link> </li> </ul> </nav> <div class="flex-1"> <a href="/" class="block w-56 mx-auto"> <img src="http://a.storyblok.com/f/42016/1096x313/0353bf6654/logo2.png"> </a> </div> <nav> <ul> <li> <nuxt-link class="text-teal-600 hover:underline" to="/en/blog"> English </nuxt-link> </li> <li> <nuxt-link class="text-teal-600 hover:underline" to="/de/blog"> German </nuxt-link> </li> </ul> </nav> </header> </template>
./components/Footer.vue
<template> <footer class="py-10 bg-teal-100"> <div class="max-w-5xl mx-auto text-center"> <nuxt-link class="text-gray-500 hover:text-gray-800 hover:underline" to="/en/sitemap"> Sitemap </nuxt-link> </div> </footer> </template>
The result you will see on localhost 3000 should be similar to the following image.
)
You can check the current progress in this GitHub commit.
Creating the Content Structure
Up to this point, we only used hard-coded mockup data in our Nuxt.js project. Before we connect Storyblok with Nuxt.js and render our first data onto the screen, we must define the structure of our data in Storyblok. A good thing about this process is that we don't need to set up the connection and you can even prepare the data before you choose the front-end framework.
To better understand the content structure, we strongly recommend reading the Structures of Content chapter of the developer guide.
First signup or login at app.storyblok.com and create a new space. You can name it anything you like. The first screen you see should be similar to the following image:
)
As you can see here, Storyblok creates your default content. We will use this content and these components in our project. Feel free to delete this content and defined components in real-world projects. If you navigate in the Components section in the left navigation, you will see the following screen with already defined components (page. teaser, feature, grid). These components are ready to be used to create new stories in Storyblok.
)
As you can see in the image, the type of the Page component is Content Type. This means that the Page represents the highest level of components and all the other components will live inside of it. In this case, our Page will be the page of our website. Feature, Grid and Teaser are Nestable, which means they can be used inside any component as a reusable blok. This way you can create an almost infinite number of combinations with your components, with multiple levels of nesting.
Read more about components and the difference between Content Types and Bloks (nestable components) in an essential part of the developer guide.
Now we have components defined in Storyblok, but we don't have the implementation of these components defined in the Nuxt.js project. Let's create them using the following source code.
Page
./components/Page.vue
<template> <div v-editable="blok" class="px-6"> <component v-for="blok in blok.body" :key="blok._uid" :blok="blok" :is="blok.component" /> </div> </template> <script> export default { props: { blok: { type: Object, required: true } } } </script>
Grid
./components/Grid.vue
<template> <ul v-editable="blok" class="flex py-8 mb-6"> <li :key="blok._uid" v-for="blok in blok.columns" class="flex-auto px-6"> <component :blok="blok" :is="blok.component" /> </li> </ul> </template> <script> export default { props: { blok: { type: Object, required: true } } } </script>
Feature
./components/Feature.vue
<template> <div v-editable="blok" class="py-2"> <h1 class="text-lg">{{ blok.name }}</h1> </div> </template> <script> export default { props: { blok: { type: Object, required: true } } } </script>
Teaser
./components/Teaser.vue
<template> <div v-editable="blok" class="py-8 mb-6 text-5xl font-bold text-center"> {{ blok.headline }} </div> </template> <script> export default { props: { blok: { type: Object, required: true } } } </script>
Explanation of Blok Prop
You probably noticed the blok
prop in all of the components created by us. We are using this prop to pass the data into each of the components. There are various solutions for this challenge and you don't have to call this prop blok
as we did. Keep in mind that you need to pass the data down to the nested components to render them.
In the case of the Page component, we are using the content from the blok
prop to decide which component should be dynamically rendered on the screen. It is done by the dynamic <component>
element (check the docs of it) and it's special is
attribute, which defines what component should be rendered. Which component should be rendered is defined in the blok.component
and can be used as this :is="blok.component"
.
You can check the current progress in this Github commit.
Connecting Storyblok with Nuxt.js
So far we have created the skeleton of our website and prepared the default components in Nuxt.js. Before we start with the design of the pages, we need to set up the connection between Nuxt.js and Storyblok to get the data for the components. To do that, we can use the storyblok-nuxt module created by the Storyblok team. Install it as shown in the following example:
bash
yarn add storyblok-nuxt // npm install storyblok-nuxt --save
Then open the nuxt.config.js file and set up the storyblok-nuxt module in the modules section. Replace the STORYBLOK_SPACE_TOKEN with a preview access token of your space. You can find all tokens of your space in Settings under API-Key tab.
./nuxt.config.js
export default { . . . /* ** Nuxt.js modules */ modules: [ [ 'storyblok-nuxt', { accessToken: 'STORYBLOK_SPACE_TOKEN', cacheProvider: 'memory' } ], ], . . . }
Check the following image if you struggle to find the preview access token.
)
If you run yarn dev
in your terminal after these steps, you should see the same screen as before.
You should save your access tokens and other sensitive data in .env and not directly in the nuxt.config.js as we did.
Updating the Index Page & Requesting First Data
Replace the source code of the pages/index.vue
with the following code.
./pages/index.vue
<template> <section> <component v-if="story.content.component" :key="story.content._uid" :blok="story.content" :is="story.content.component" /> </section> </template> <script> export default { data () { return { story: { content: {} } } }, mounted () { this.$storybridge(() => { const storyblokInstance = new StoryblokBridge() // Use the input event for instant update of content storyblokInstance.on('input', (event) => { console.log(this.story.content) if (event.story.id === this.story.id) { this.story.content = event.story.content } }) // Use the bridge to listen the events storyblokInstance.on(['published', 'change'], (event) => { // window.location.reload() this.$nuxt.$router.go({ path: this.$nuxt.$router.currentRoute, force: true, }) }) }) }, asyncData (context) { // // This what would we do in real project // const version = context.query._storyblok || context.isDev ? 'draft' : 'published' // const fullSlug = (context.route.path == '/' || context.route.path == '') ? 'home' : context.route.path // Load the JSON from the API - loadig the home content (index page) return context.app.$storyapi.get('cdn/stories/home', { version: 'draft' }).then((res) => { return res.data }).catch((res) => { if (!res.response) { console.error(res) context.error({ statusCode: 404, message: 'Failed to receive content form api' }) } else { console.error(res.response.data) context.error({ statusCode: res.response.status, message: res.response.data }) } }) } } </script>
You should get an error now because we didn't register our component in Nuxt.js. So let's do it and quickly create a simple Nuxt.js plugin for it. Create a file plugins/components.js
with the following code.
./plugins/components.js
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)
And register the plugin in nuxt.config.js.
./nuxt.config.js
export default { . . . /* ** Plugins to load before mounting the App ** https://nuxtjs.org/guide/plugins */ plugins: [ '~/plugins/components' ], . . . }
Your localhost:3000 should look similar to the following image.
)
You can check the current progress in this Github commit.
Setting Up a Real-Time Visual Editor
Our website is running on the localhost:3000 and there we can preview the content of the Home story. The better way to preview your content is by using the real-time preview in Visual Editor, where you and your editors can see the changes instantly in real-time.
If you open the Home story it should look like this:
)
You will see Visual Editor without the Preview setup (like on the next image). You may edit your content, or even save it and publish it. You will still need to go to another browser tab and open the localhost:3000 to see the result, so let's change it.
)
Go into the Settings and in the General tab to change the value of the Location (default environment) to http://localhost:3000/
. You can also create a special preview URL for different environments like the preview from Vercel, Netlify, or other hosting platforms. This can be done by clicking on "Add preview url" button on the screen.
)
If you open your Home story using the Visual Editor right now, you will see a Nuxt.js error message. This is good and it means that we connected Nuxt.js with Storyblok. Nuxt.js tells us that we are trying to reach the non-existent URL of http://localhost:3000/home
as the slug of the Home story is home
.
Let's fix this by overriding the Real Path property of the Home story to /
as this story represents the homepage (index.vue) and shouldn't have any slug. We cannot remove the slug in Storyblok and our API needs to somehow reach this story.
)
Don't forget to save the changes. You should see the update of the preview after the save and at this moment your real-time preview is up and running. Try to edit the content of the story and see the instant change on the left-hand side.
)
Understanding the Structure of the Components
Since we already have a working Visual Editor with a preview function, we can click on different highlighted areas and the content editor will open in the right-hand side of the Visual Editor. You can also open and collapse these components (bloks) in the right-hand side editor without using the preview.
You can see the opened Grid component and its content in the next image. The Grid in this case consists only of an array of other bloks (nested components) and in this particular case is made up of three Feature components. Feel free to add more or remove one of them.
)
If you inspect the source code using the Vue.js devtools you will see the same structure of components that we created in Storyblok.
)
Extending the Schema of Bloks in Storyblok
We have loaded the demo content of Storyblok (added by default at the creation of the new space) and set up the real-time visual editor. In this section we will extend existing bloks (nested components) with new fields and adjust their render templates in Nuxt.js.
Update of the Teaser
First, we will add an image asset to the Teaser component. Go to the Components section of Storyblok UI and click on the Teaser component. Create a new field named image in the opened overlay.
)
Click on the created image field and define its type to Asset and as field-type allow only Images. Save the changes.
)
Open Home story in the Visual Editor and click on the Teaser. You should see the option to add an image to the Teaser. Add one now.
)
We have to update our nuxt component ./components/teaser.vue
with the following code to render the image.
./components/teaser.vue
<template> <div v-editable="blok" class="pb-8 mb-6 font-bold text-center"> <img class="h-48 w-full mb-4 object-cover" :src="blok.image.filename" /> <h3 class="text-5xl">{{ blok.headline }}</h3> </div> </template> <script> export default { props: { blok: { type: Object, required: true } } } </script>
Right now you should see a result similar to this:
)
Extending the Feature Component
Every feature component currently only has one title. Let's extend the Feature blok (nested component), including a description text and an icon.
Open up the schema of the component directly from the Visual Editor. Click on one of the Features components and in the right-hand corner you should see the Define Schema button. Click on it and the Schema overlay will open.
)
Now we need to define two new fields in the Feature component. Add a field named description of the type textarea
, and a field named icon of the type image
.
)
Open the Feature component in Nuxt.js components/Feature.vue
and extend it with the new fields as well.
./components/Feature.vue
<template> <div v-editable="blok" class="py-2 text-center"> <img class="mx-auto" :src="blok.icon"> <h1 class="text-lg">{{ blok.name }}</h1> <p class="text-gray-600"> {{ blok.description }} </p> </div> </template> <script> export default { props: { blok: { type: Object, required: true } } } </script>
Finally, add new content to your features and you should see results similar to the following image:
)
You can check the current progress in this Github commit.
Creating the Article Content Type
Next we are going to create our first Content Type called Article. It will hold the very simple content of our blogpost. Go to the Components section of the Storyblok UI and click on New in the top right-hand corner. Add the name article
to the new content type and set Act as content type (eg. blog-post) to true.
)
Define in the schema of Article the fields name
(Text), intro
(Textarea), and body
(Richtext).
)
Create a folder named articles in the root of Content section in Storyblok UI.
)
Create a Article.vue
component with the following source code in Nuxt.js project.
./components/Article.vue
<template> <div v-editable="blok" class="prose my-24 mx-auto"> <h1>{{ blok.name }}</h1> <p>{{ blok.intro }}</p> <rich-text-renderer :document="blok.body" /> </div> </template> <script> export default { props: { blok: { type: Object, required: true } } } </script>
As you can see here, we used <rich-text-renderer>
to render our Richtext content and class named prose
to style it. To make this component work correctly we need to install @marvr/storyblok-rich-text-vue-renderer from Marvin Rudolph, one of our ambassadors, and @tailwindcss/typography plugin for the styling of the article text.
Bash
yarn add @vue/composition-api @marvr/storyblok-rich-text-vue-renderer
To make it work, you have to create two more plugins, composition-api.js and storyblok-rich-text-renderer.js, with the following code.
./plugins/composition-api.js
import Vue from 'vue' import VueCompositionApi from '@vue/composition-api' Vue.use(VueCompositionApi)
./plugins/storyblok-rich-text-renderer.js
import Vue from 'vue' import VueRichTextRenderer from '@marvr/storyblok-rich-text-vue-renderer' // Simple ... Vue.use(VueRichTextRenderer)
And don't forget to register them in the nuxt.config.js. The order of the plugins is important and the composition-api needs to be registered before the renderer.
./nuxt.config.js
export default { ... /* ** Plugins to load before mounting the App ** https://nuxtjs.org/guide/plugins */ plugins: [ '~/plugins/components', '~/plugins/composition-api.js', '~/plugins/storyblok-rich-text-renderer.js' ], ... }
To enable the .prose
class of @tailwindcss/typography plugin, we need to install it.
Bash
yarn add @tailwindcss/typography
Additionally to that adjust your ./tailwind.config.js
to the following code, where we just register the typography plugin.
./tailwind.config.js
/* ** TailwindCSS Configuration File ** ** Docs: https://tailwindcss.com/docs/configuration ** Default: https://github.com/tailwindcss/tailwindcss/blob/master/stubs/defaultConfig.stub.js */ module.exports = { theme: {}, variants: {}, plugins: [ require('@tailwindcss/typography') ], purge: { // Learn more on https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css enabled: process.env.NODE_ENV === 'production', content: [ 'components/**/*.vue', 'layouts/**/*.vue', 'pages/**/*.vue', 'plugins/**/*.js', 'nuxt.config.js' ] } }
For the last step before rendering the article, we need to configure the dynamic routing in the Nuxt.js. In this case, we just need to create a folder named articles in the pages folder. Add the _slug.vue
file into the articles folder with the following code.
./pages/articles/_slug.vue
<template> <section> <Article :blok="story.content"/> </section> </template> <script> import Article from '~/components/Article.vue' export default { components: { Article }, data () { return { story: { content: {} } } }, mounted () { this.$storybridge(() => { const storyblokInstance = new StoryblokBridge() // Use the input event for instant update of content storyblokInstance.on('input', (event) => { console.log(this.story.content) if (event.story.id === this.story.id) { this.story.content = event.story.content } }) // Use the bridge to listen the events storyblokInstance.on(['published', 'change'], (event) => { // window.location.reload() this.$nuxt.$router.go({ path: this.$nuxt.$router.currentRoute, force: true, }) }) }) }, asyncData (context) { // Load the JSON from the API let version = context.query._storyblok || context.isDev ? 'draft' : 'published' return context.app.$storyapi.get(`cdn/stories/articles/${context.params.slug}`, { version: version }).then((res) => { return res.data }).catch((res) => { if (!res.response) { console.error(res) context.error({ statusCode: 404, message: 'Failed to receive content form api' }) } else { console.error(res.response.data) context.error({ statusCode: res.response.status, message: res.response.data }) } }) } } </script>
You can see in the code that we are using the slug
param to decide which content of the article we should request. As soon as we get the data we will render the Article component with the data provided through the blok
prop. The source code of this sample could be a lot shorter if we extract the mounted function and asyncData function into the abstract component.
Now create your first article in the article folder, the result in Visual Editor should look like the next image. Don't forget to play a little bit with the content and the real-time visual editor.
)
You can check the current progress in this Github commit.
Showing Featured Articles on the Homepage
At this point in our project, we are missing any listing showing the articles. To do this, we'll create a Featured Articles section on the homepage.
Preparing Storyblok's Structure
We will create a new blok (nested component) called featured-articles
containing the field named title
of the type Text and the field named articles
of the type Multi-option. Set the Source of articles field to Stories
and in the field named Path to folder of stories write articles/
.
)
You can now use the Featured Articles component in the Home story to select a few articles. I strongly recommend creating multiple articles in the articles folder. Use the Add block button in the Home story and select the Featured Articles.
)
)
)
Like before, we are not rendering any result we didn't define for the components in the Nuxt.js. Let's create the following files.
./components/FeaturedArticles.vue
<template> <div v-editable="blok"> <h2 class="pt-2 pl-6 text-lg text-gray-700 italic">{{ blok.title }}</h2> <ul class="flex py-6 mb-6"> <li v-for="article in sortedArticles" :key="article._uid" class="flex-auto px-6" style="min-width: 33%"> <article-teaser v-if="article.content" :article-link="article.full_slug" :article-content="article.content"/> <p v-else class="px-4 py-2 text-white bg-red-700 text-center rounded">This content loads on save. <strong>Save the entry & reload.</strong></p> </li> </ul> </div> </template> <script> export default { props: { blok: { type: Object, required: true } }, computed: { sortedArticles() { // Load reference data/content from store const featuredArticles = this.$store.state.articles.articles.filter((article) => { return this.blok.articles.includes(article.uuid) }) // Enable the ordering of the article previews featuredArticles.sort((a, b) => { return this.blok.articles.indexOf(a.uuid) - this.blok.articles.indexOf(b.uuid); }) return featuredArticles } } } </script>
./components/ArticlesTeaser.vue
<template> <nuxt-link :to="articleLink" class="article-teaser block py-4 px-6 border rounded border-gray-500"> <h2 class="pt-2 pb-4 text-2xl font-bold"> {{ articleContent.name }} </h2> <p class="pb-6 leading-relaxed"> {{ articleContent.intro }} </p> </nuxt-link> </template> <script> export default { props: { articleContent: { type: Object, required: true }, articleLink: { type: String, required: true } } } </script> <style> .article-teaser:hover { box-shadow: 0px 0px 15px 0px rgba(0,0,0,0.75); } </style>
./store/articles.js
export const state = () => ({ articles: [], loaded: '0', }) export const mutations = { setArticles (state, entries) { state.articles = entries }, setLoaded (state, loaded) { state.loaded = loaded } }
Replace the content of components.js with the following source code:
./plugins/components.js
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' import FeaturedArticles from '~/components/FeaturedArticles.vue' import ArticleTeaser from '~/components/ArticleTeaser.vue' Vue.component('page', Page) Vue.component('teaser', Teaser) Vue.component('grid', Grid) Vue.component('feature', Feature) Vue.component('featured-articles', FeaturedArticles) Vue.component('article-teaser', ArticleTeaser)
Restart the Nuxt.js to initialize the Store yarn dev
and update the ./pages/index.vue
with the fetch function to fill the store.
./pages/index.vue
... <script> export default { ... async fetch(context) { // Loading reference data - Articles in our case if(context.store.state.articles.loaded !== '1') { let articlesRefRes = await context.app.$storyapi.get(`cdn/stories/`, { starts_with: 'articles/', version: 'draft' }) context.store.commit('articles/setArticles', articlesRefRes.data.stories) context.store.commit('articles/setLoaded', '1') } }, ... } </script>
After all of these updates are complete, you should see the following preview of the Home story in the Visual Editor. You can even change the order of the teaser by clicking the Show only selected button and drag & drop the articles.
)
You can check the current progress in this Github commit.
Creating an Articles Overview Page
Let's quickly create an overview page of our articles before we implement another language. Create index.vue
on the path ./pages/articles with the following source code.
./pages/articles/index.vue
<template> <section> <h2 class="py-10 text-center font-bold text-4xl">Articles Overview</h2> {{ articles }} <ul class="flex py-6 mb-6"> <li v-for="article in stories" :key="article._uid" class="flex-auto px-6" style="min-width: 33%"> <article-teaser v-if="article.content" :article-link="article.full_slug" :article-content="article.content"/> <p v-else class="px-4 py-2 text-white bg-red-700 text-center rounded">This content loads on save. <strong>Save the entry & reload.</strong></p> </li> </ul> </section> </template> <script> export default { data () { return { stories: [] } }, asyncData (context) { return context.app.$storyapi.get('cdn/stories', { starts_with: 'articles/', version: 'draft' }).then((res) => { return res.data }).catch((res) => { if (!res.response) { console.error(res) context.error({ statusCode: 404, message: 'Failed to receive content form api' }) } else { console.error(res.response.data) context.error({ statusCode: res.response.status, message: res.response.data }) } }) } } </script>
We need to also update the Header.vue to point to this path. Replace the source code of Header.vue with the following lines.
./components/Header.vue
<template> <header class="max-w-5xl mx-auto py-8 flex"> <nav> <ul> <li> <nuxt-link class="text-teal-600 hover:underline" to="/"> Home </nuxt-link> </li> <li> <nuxt-link class="text-teal-600 hover:underline" to="/articles"> Blog </nuxt-link> </li> </ul> </nav> <div class="flex-1"> <a href="/" class="block w-56 mx-auto"> <img src="http://a.storyblok.com/f/42016/1096x313/0353bf6654/logo2.png"> </a> </div> <nav> <ul> <li> <nuxt-link class="text-teal-600 hover:underline" to="/"> English </nuxt-link> </li> <li> <nuxt-link class="text-teal-600 hover:underline" to="/de"> German </nuxt-link> </li> </ul> </nav> </header> </template>
Our navigation should work now and therefore we may implement another language.
You can check the current progress in this Github commit.
Adding Another Language
We can implement internationalization with two different approaches in Storyblok. It strongly depends on your use case, which you should use. Read more about the Internationalization in docs. We are going to use Field Level Translation.
All you need to do on the Storyblok side is go to the Settings of your space and define a new language in the Languages tab. Let's add German as a language and set English as the default language.
)
If you open any story in the Visual Editor now, you will see the language dropdown in the header.
)
If you choose German language right now, you won't see any changes because we didn't set any of the fields in our components as translatable. To do that name the field headline
of the Teaser as translatable.
)
Change the language to German and you will see a Translate checkbox next to the translatable field. If you set the checkbox to true you are able to translate the value and you will see a real-time preview in the Preview. If the checkbox stays false, the default value will be used.
)
The Storyblok Visual Editor works, but the Nuxt.js routing needs a little adjustment to make it work correctly. I will keep it simple for the purpose of this tutorial and we will create a de
folder in the pages folder of the Nuxt.js. Create following files in this folder with the following code.
./pages/de/index.vue
<template> <section> <component v-if="story.content.component" :key="story.content._uid" :blok="story.content" :is="story.content.component" /> </section> </template> <script> export default { data () { return { story: { content: {} } } }, mounted () { this.$storybridge(() => { const storyblokInstance = new StoryblokBridge() // Use the input event for instant update of content storyblokInstance.on('input', (event) => { console.log(this.story.content) if (event.story.id === this.story.id) { this.story.content = event.story.content } }) // Use the bridge to listen the events storyblokInstance.on(['published', 'change'], (event) => { // window.location.reload() this.$nuxt.$router.go({ path: this.$nuxt.$router.currentRoute, force: true, }) }) }) }, async fetch(context) { // Loading reference data - Articles in our case if(context.store.state.articles.loaded !== '1') { let articlesRefRes = await context.app.$storyapi.get(`cdn/stories/`, { starts_with: 'de/articles/', version: 'draft' }) context.store.commit('articles/setArticles', articlesRefRes.data.stories) context.store.commit('articles/setLoaded', '1') } }, asyncData (context) { // // This what would we do in real project // const version = context.query._storyblok || context.isDev ? 'draft' : 'published' // const fullSlug = (context.route.path == '/' || context.route.path == '') ? 'home' : context.route.path // Load the JSON from the API - loadig the home content (index page) return context.app.$storyapi.get('cdn/stories/de/home', { version: 'draft' }).then((res) => { return res.data }).catch((res) => { if (!res.response) { console.error(res) context.error({ statusCode: 404, message: 'Failed to receive content form api' }) } else { console.error(res.response.data) context.error({ statusCode: res.response.status, message: res.response.data }) } }) } } </script>
./pages/de/articles/index.vue
<template> <section> <h2 class="py-10 text-center font-bold text-4xl">Articles Overview</h2> {{ articles }} <ul class="flex py-6 mb-6"> <li v-for="article in stories" :key="article._uid" class="flex-auto px-6" style="min-width: 33%"> <article-teaser v-if="article.content" :article-link="article.full_slug" :article-content="article.content"/> </li> </ul> </section> </template> <script> export default { data () { return { stories: [] } }, asyncData (context) { return context.app.$storyapi.get('cdn/stories', { starts_with: 'de/articles/', version: 'draft' }).then((res) => { return res.data }).catch((res) => { if (!res.response) { console.error(res) context.error({ statusCode: 404, message: 'Failed to receive content form api' }) } else { console.error(res.response.data) context.error({ statusCode: res.response.status, message: res.response.data }) } }) } } </script>
./pages/de/articles/_slug.vue
<template> <section> <Article :blok="story.content"/> </section> </template> <script> import Article from '~/components/Article.vue' export default { components: { Article }, data () { return { story: { content: {} } } }, mounted () { this.$storybridge(() => { const storyblokInstance = new StoryblokBridge() // Use the input event for instant update of content storyblokInstance.on('input', (event) => { console.log(this.story.content) if (event.story.id === this.story.id) { this.story.content = event.story.content } }) // Use the bridge to listen the events storyblokInstance.on(['published', 'change'], (event) => { // window.location.reload() this.$nuxt.$router.go({ path: this.$nuxt.$router.currentRoute, force: true, }) }) }) }, asyncData (context) { // Load the JSON from the API let version = context.query._storyblok || context.isDev ? 'draft' : 'published' return context.app.$storyapi.get(`cdn/stories/de/articles/${context.params.slug}`, { version: version }).then((res) => { return res.data }).catch((res) => { if (!res.response) { console.error(res) context.error({ statusCode: 404, message: 'Failed to receive content form api' }) } else { console.error(res.response.data) context.error({ statusCode: res.response.status, message: res.response.data }) } }) } } </script>
Now that all the routes are working, the last piece of code we need to update is to make the Header navigation dynamic, but it is up to you. You can visit http://localhost:3000/de/articles to browse your articles in the german language.
You can check the current progress in this Github commit.
Deploying to Vercel
You have multiple options for the deployment of your website/application to go live or to preview the environment. One of the easiest ways is to use Vercel (previously Now.sh) and deploy using the command line.
First, create an account on Vercel and install their CLI application. Then, create a vercel.json file with the following content.
./vercel.json
{ "builds": [ { "src": "nuxt.config.js", "use": "@nuxtjs/vercel-builder", "config": {} } ] }
Deploy your website by running teh vercel in your console.
bash
vercel
You can check the current progress in this Github commit.
Conclusion
It's incredibly easy to build a full-blown website with Nuxt.js and it comes with a great ecosystem. I really like the way Nuxt.js abstracts common tasks you normally do in the Webpack configuration. It feels a little bit like Ruby on Rails where conventions go over configuration. For big projects, these conventions make it easy to onboard new team members and make the projects a lot more maintainable.
Resource | Link |
---|---|
Github repository of this Tutorial | github.com/storyblok/nuxtjs-multilanguage-website |
Vercel | vercel.com |
Nuxt.js | nuxtjs.org |
Vue.js | vuejs.org |
Storyblok App | app.storyblok.com |