Nuxt.jsで多言語対応ブログを作成する

[The Complete Guide to Build a Full Blown Multilanguage Website with Nuxt.js 
](https://www.storyblok.com/tp/nuxt-js-multilanguage-website-tutorial)
上記のチュートリアルに従って、Nuxt.jsでブログを作成する過程を紹介します。自分用のメモなので英訳もしませんし、解説も少なめです。

##作業環境
- OS：Windows 10 HOME Edition（ver.2004）
- Node：v12.18.2
- NPM：v6.14.5
- NPX：v6.14.5

##Environment setup
###Requirements
- Basic understanding of Nuxt.js
- VueJs & their CLI
- NodeJS
- NPM
- NPX
- The CLI of now.sh for hosting
- An account on Storyblok.com to manage content

任意のフォルダにcdしてから、以下のコマンドを実行します。

```powershell:SHELL
$ npx create-nuxt-app mywebsite // yarn create nuxt-app mywebsite
$ cd mywebsite
$ npm run dev // yarn dev
```

`npx create-nuxt-app`を実行するといくつか質問があります。私は質問には以下のように答えました。

```
- Project name: mywebsite
- Programming language: JavaScript
- Package manager: Npm #package manager of your choice
- UI framework: None #no UI Framework
- Nuxt.js modules: #no modules needed
- Linting tools: ESLint
-
- Rendering mode: Universal (SSR / SSG) //choose Universal rendering mode (SSR)
- Deployment target: Static (Static/JAMStack hosting)
- Development tools: jsconfig.json (Recommended for VS Code)
```

ブラウザでhttp://localhost:3000 を開き、下記の画面が表示されればOKです。
![202007050535.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/45f8a904-fd3b-077a-7fa8-7fd65c611dd7.png)
sass-loaderをインストールします。また、Gitで管理をします。

```powershell:SHELL
$ npm install --save-dev sass-loader node-sass css-loader // yarn add -D sass-loader node-sass css-loader

// Initialize git 
$ git init && git add . && git commit -m 'init'
```

##Build a skeleton
###Global SCSS in Nuxt.js
以下のようなフォルダ構成にします。

```
assets/
--| scss/
-----| elements/
--------| body.scss
--------| ...
-----| components/
--------| util.scss
--------| ...
-----| styles.scss
```

`assets/scss/styles.scss`ファイルを作り、以下の内容を追加します。

```assets/scss/styles.scss
$brand-color: #357F8A;
$breakpoint-small: 480px;
$breakpoint-medium: 768px;
$breakpoint-large: 960px;
$breakpoint-xlarge: 1220px;
$breakpoint-mini-max: ($breakpoint-small - 1);
$breakpoint-small-max: ($breakpoint-medium - 1);
$breakpoint-medium-max: ($breakpoint-large - 1);
$breakpoint-large-max: ($breakpoint-xlarge - 1);

@import 'elements/body.scss';
@import 'components/util.scss';
```

`assets/scss/elements/body.scss`ファイルを作り、基本のフォントを定義します。

```assets/scss/elements/body.scss
body {
  font-family: 'Zilla Slab', Helvetica, sans-serif;
  line-height: 1;
  font-size: 18px;
  color: #000;
  margin: 0;
  padding: 0;
}
```

`assets/scss/components/util.scss`ファイルを作り、global utilityクラスを定義します。

```assets/scss/components/util.scss
.util__flex {
  display: flex;
}

.util__flex-col {
  flex: 0 0 auto;
}

.util__flex-eq {
  flex: 1;
}

.util__container {
  max-width: 75rem;
  margin-left: auto;
  margin-right: auto;
  padding-left: 20px;
  padding-right: 20px;
  box-sizing: border-box;
}
```

###Add a google font to Nuxt.js
'body.scss'でZilla Slabフォントを定義しましたが、有効化するために`nuxt.config.js`を開き、以下の内容を追記します。

```nuxt.config.js
head: {
    ...
    link: [
      ...
      {
        rel: 'stylesheet',
        href: 'https://fonts.googleapis.com/css?family=Zilla+Slab:400,700'
      }
    ]
},
...
```

###Define the default layout
`layouts/default.vue`ファイルを下記の内容に変更します。

```layouts/default.vue
<template>
  <div>
    <top-header/>
    <main id="main" role="main">
      <nuxt/>
    </main>
    <bottom-footer/>
  </div>
</template>

<script>
import TopHeader from '~/components/TopHeader.vue'
import BottomFooter from '~/components/BottomFooter.vue'

export default {
  components: {
    TopHeader,
    BottomFooter
  }
}
</script>

<style lang="scss">
@import '../assets/scss/styles.scss';
</style>
```

ここでヘッダーとフッターがないためエラーが起こるので、次の項目で作成していきます。

###Create the header component
`./components/TopHeader.vue`ファイルを下記の内容で作成します。

```./components/TopHeader.vue
<template>
  <header class="top-header util__flex util__container">
    <nav class="top-header__col">
      <ul class="nav">
        <li>
          <nuxt-link class="nav__item" to="/">Home</nuxt-link>
        </li>
        <li>
          <nuxt-link class="nav__item" to="/en/blog">Blog</nuxt-link>
        </li>
      </ul>
    </nav>
    <a href="/" class="top-header__col top-header__logo">
      <img src="http://a.storyblok.com/f/42016/1096x313/0353bf6654/logo2.png">
    </a>
    <nav class="top-header__col top-header__second-navi">
      <ul class="nav">
        <li>
          <nuxt-link class="nav__item" to="/en/blog">English</nuxt-link>
        </li>
        <li>
          <nuxt-link class="nav__item" to="/de/blog">German</nuxt-link>
        </li>
      </ul>
    </nav>
  </header>
</template>

<style lang="scss">
  .top-header {
    justify-content: space-between;
    padding-top: 30px;
    padding-bottom: 30px;
  }

  .top-header__logo {
    text-align: center;
    position: absolute;
    left: 50%;

    img {
      position: relative;
      max-height: 60px;
      left: -50%;
      top: -15px;
    }
  }

  .top-header__second-navi {
    text-align: right;
  }
</style>
```

###Create the footer component
`./components/BottomFooter.vue`ファイルを下記の内容で作成します。

```./components/BottomFooter.vue
<template>
  <footer class="bottom-footer">
    <div class="util__container">
      <nuxt-link class="bottom-footer__link" to="/en/sitemap">Sitemap</nuxt-link>
    </div>
  </footer>
</template>

<style lang="scss">
.bottom-footer {
  background: #e3f2ed;
  padding: 40px 0 120px 0;
  text-align: center;
}

.bottom-footer__link {
  color: #8ba19a;
  text-decoration: none;
}
</style>
```

これでエラーが解消されて、以下のように表示されます。
![202007050601.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/a766c621-35d3-bbda-9230-88b29d99ec43.png)
ここまでの内容をコミットします。

```powershell:SHELL
$ git add . && git commit -m 'creates the skeleton'
```

##Build a homepage
###Install the Storyblok Nuxt.js module
Storyblok moduleをインストールします。

```powershell:SHELL
$ npm install storyblok-nuxt --save // yarn add -D storyblok-nuxt
```

インストールが終わったら、https://app.storyblok.com/ に登録orログインして、新しいスペースを作成します。
初回登録したら「Create your first space」というボタンが表示されたので押します。
![202007050613.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/de8cd994-37f1-b897-1c44-6c718f1fde28.png)
適当に「Nuxt Blog」という名前をつけて作りました。
![202007050615.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/40169ae9-b85d-bda9-0436-fcd10488716e.png)
よく分からないのでスキップします。
![202007050617.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/1097bbb6-eb79-fd55-b3ba-fbffcffedade.png)
左側の「Settings」を選び、「API-Keys」のタブを押すと、PREVIEW_TOKENがあるのでコピーします。
![202007050621.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/72137f51-9011-77b4-458d-c9f6942c475b.png)
PREVIEW_TOKENをnuxt.config.jsファイルに追加します。

```nuxt.config.js
module.exports = {
  modules: [
    [
      'storyblok-nuxt',
      {
        accessToken: 'YOUR_PREVIEW_TOKEN',
        cacheProvider: 'memory'
      }
    ]
  ],
  ...
}
```

###Update the homepage component
`pages/index.vue`ファイルを下記の内容に変更します。

```pages/index.vue
<template>
  <section class="util__container">
    <component v-if="story.content.component" :key="story.content._uid" :blok="story.content" :is="story.content.component"></component>
  </section>
</template>

<script>

export default {
  data () {
    return {
      story: { content: {} }
    }
  },
  mounted () {
    // use the bridge to listen to events
    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()
        this.$nuxt.$router.go({
          path: this.$nuxt.$router.currentRoute,
          force: true,
        })
      }
    })
  },
  asyncData (context) {
    // Load the JSON from the API
    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>

```

すると、以下のようなエラーが出てきました。
![202007051437.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/a87d3130-5f69-aa78-6084-da0ddc4bfc67.png)
```
   3:5   warning  Require self-closing on Vue.js custom components (<component>)           vue/html-self-closing
   3:95  warning  Attribute ":is" should go before ":blok"                                 vue/attributes-order
  18:24  error    Expected '===' and instead saw '=='                                      eqeqeq
  26:22  error    Unexpected trailing comma                                                comma-dangle
  31:3   warning  The "asyncData" property should be above the "data" property on line 10  vue/order-in-components
  39:9   warning  Unexpected console statement                                             no-console
  42:9   warning  Unexpected console statement                                             no-console

✖ 7 problems (2 errors, 5 warnings)
  1 error and 3 warnings potentially fixable with the `--fix` option.
```

下記のようにエラーを修正しました。

```pages/index.vue
<template>
  <section class="util__container">
    <component v-if="story.content.component" :key="story.content._uid" :blok="story.content" :is="story.content.component"></component>
  </section>
</template>

<script>

export default {
  data () {
    return {
      story: { content: {} }
    }
  },
  mounted () {
    // use the bridge to listen to events
    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()
        this.$nuxt.$router.go({
          path: this.$nuxt.$router.currentRoute,
          force: true
        })
      }
    })
  },
  asyncData (context) {
    // Load the JSON from the API
    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>

```

これで下記のように表示されました。
![202007051445.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/155d320a-c61b-1f1d-5b40-ae5074a685fe.png)
###Creating the homepage components
`plugins`フォルダに`components.js`を追加します。

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

自動的には`components.js`を読み込まないので、`nuxt.config.js`に以下のように追記します。

```nuxt.config.js
module.exports = {
  plugins: [
    '~/plugins/components'
  ],
  ...
```

###Page.vue

```components/Page.vue
<template>
  <div v-editable="blok" 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>
```

###Teaser.vue

```components/Teaser.vue
<template>
  <div v-editable="blok">
    {{ blok.headline }}
  </div>
</template>

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

###Grid.vue

```components/Grid.vue
<template>
  <div v-editable="blok" class="util__flex">
    <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.vue

```components/Feature.vue
<template>
  <div v-editable="blok" class="util__flex-eq">
    <h1>{{ blok.name }}</h1>
  </div>
</template>

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

これで以下のように表示されます。
![202007051501.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/9a81b143-0a99-9a0e-7144-d3962f0cb09c.png)
`Vue.js devtools`を使用して、ソースコードで作成されたコンポーネントを確認できます。Google Choromeで拡張機能の[Vue.js devtools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) をインストールした後、右クリックをして「拡張機能を管理」を開きます。
![202007051509.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/3699df9b-63f8-1b39-c950-6fae603fa454.png)
「ファイルの URL へのアクセスを許可する」をOnにします。
![202007051513.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/74697bf0-d501-bfe1-c104-5960501f6152.png)
`http://localhost:3000/`をリロードしてからデベロッパーツールを起動すると`Vue`のタブが追加されています。
![202007051524.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/3ae8fc07-c9dd-b57b-16b3-3be93b9f1e59.png)

##Create your first block in Storyblok
###Setup of the Nuxt.js environment preview
https://app.storyblok.com/ にログインして、`Settings`を押して、'Location (default environment)'に「http://localhost:3000/ 」を入力してSaveボタンを押します。
![202007051931.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/1a967176-6f2f-6e38-b04a-447372de7d0c.png)
ダッシュボードに戻り、`Recent content changes`の下に出てきている項目を選びます。
![screencapture-app-storyblok-2020-07-05-20_05_58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/632e9e08-c834-8e06-bf59-e9c5fe8723f4.png)
以下のような画面になるので、`config`タブを選び、`Real Path`に`/`を入力して上の`Save`を押します。
![202007051945.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/1eae9660-eb62-5ddf-6fbe-78abb8c35fad.png)
すると下記のような画面に更新されます。
![202007052211.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/acbaac75-2a49-2884-3207-279344761150.png)
###Let's define the schema of a new slide block/component
下記の動画で新しいblockの作り方を学習します。
https://www.youtube.com/watch?v=NMM1qbGx9eo
画像やアイコンなどは以下のデモページよりダウンロードできます。
https://nuxtblok.now.sh/
以下の内容で`components/Slide.vue`を作成します。

```components/Slide.vue
<template>
  <div class="slide" v-editable="blok">
    <img :src="blok.image">
  </div>
</template>

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

<style lang="scss">
.slide img {
  width: 100%;
}
</style>
```

`component.js`に以下を追記します。

```plugins/components.js
import Vue from 'vue'
...
import Slide from '~/components/Slide.vue'

...
Vue.component('slide', Slide)
```

`Teaser.vue`を下記の内容に変更します。

```components/Teaser.vue
<template>
  <div v-editable="blok" class="teaser">
    <component v-if="slide" :blok="slide" :is="slide.component"></component>
    <div class="teaser__pag">
      <button @click="handleDotClick(index)"
              :key="index"
              v-for="(blok, index) in blok.body"
              :class="{'teaser__pag-dot--current': index == currentSlide}"
              class="teaser__pag-dot">Next</button>
    </div>
  </div>
</template>

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

  data () {
    return {
      currentSlide: 0
    }
  },

  computed: {
    slide () {
      let slides = this.blok.body.filter((slide, index) => {
        return this.currentSlide === index
      })
      if (slides.length) {
        return slides[0]
      }
      return null
    }
  },

  methods: {
    handleDotClick (index) {
      this.currentSlide = index
    }
  }
}
</script>

<style lang="scss">
.teaser__pag {
  width: 100%;
  text-align: center;
  margin: 30px 0;
}

.teaser__pag-dot {
  text-indent: -9999px;
  border: 0;
  border-radius: 50%;
  width: 17px;
  height: 17px;
  padding: 0;
  margin: 5px 6px;
  background-color: #ccc;
  -webkit-appearance: none;
  cursor: pointer;

  &--current {
    background-color: #000;
  }
}
</style>
```

またエラーが出てきました。
![202007052239.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/dad72ad1-2ee2-cbff-884c-15f3fde07f9c.png)

```
  4:5   warning  Require self-closing on Vue.js custom components (<component>)                    vue/html-self-closing
   4:43  warning  Attribute ":is" should go before ":blok"                                          vue/attributes-order
   6:15  error    '@click' should be on a new line                                                  vue/max-attributes-per-line
   7:15  warning  Attribute ":key" should go before "@click"                                        vue/attributes-order
   8:15  warning  Attribute "v-for" should go before "@click"                                       vue/attributes-order
   8:23  warning  Variable 'blok' is already declared in the upper scope                            vue/no-template-shadow
   9:15  warning  Attribute ":class" should go before "@click"                                      vue/attributes-order
  10:15  warning  Attribute "class" should go before "@click"                                       vue/attributes-order
  10:38  warning  Expected 1 line break before closing bracket, but no line breaks found            vue/html-closing-bracket-newline
  10:39  warning  Expected 1 line break after opening tag (`<button>`), but no line breaks found    vue/multiline-html-element-content-newline
  10:43  warning  Expected 1 line break before closing tag (`</button>`), but no line breaks found  vue/multiline-html-element-content-newline
  17:11  warning  Prop "blok" should define at least its type                                       vue/require-prop-types
  27:11  error    'slides' is never reassigned. Use 'const' instead                                 prefer-const

✖ 13 problems (2 errors, 11 warnings)
  2 errors and 9 warnings potentially fixable with the `--fix` option.
```

改行して@clickを新しい行に移行して、letをconstに変更しました。

```components/Teaser.vue
components/Teaser.vue
<template>
  <div v-editable="blok" class="teaser">
    <component v-if="slide" :blok="slide" :is="slide.component"></component>
    <div class="teaser__pag">
      <button
        @click="handleDotClick(index)"
        :key="index"
        v-for="(blok, index) in blok.body"
        :class="{'teaser__pag-dot--current': index == currentSlide}"
        class="teaser__pag-dot">Next</button>
    </div>
  </div>
</template>

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

  data () {
    return {
      currentSlide: 0
    }
  },

  computed: {
    slide () {
      const slides = this.blok.body.filter((slide, index) => {
        return this.currentSlide === index
      })
      if (slides.length) {
        return slides[0]
      }
      return null
    }
  },

  methods: {
    handleDotClick (index) {
      this.currentSlide = index
    }
  }
}
</script>

<style lang="scss">
.teaser__pag {
  width: 100%;
  text-align: center;
  margin: 30px 0;
}

.teaser__pag-dot {
  text-indent: -9999px;
  border: 0;
  border-radius: 50%;
  width: 17px;
  height: 17px;
  padding: 0;
  margin: 5px 6px;
  background-color: #ccc;
  -webkit-appearance: none;
  cursor: pointer;

  &--current {
    background-color: #000;
  }
}
</style>


```

これで下記のように表示されるようになりました。
![202007052246.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/4bd7cc71-94bc-da6e-0519-e789971a17cf.png)
###Extending the feature component
ブラウザからFeatureコンポーネントに`description`をtextareaとして、`icon`をimageとして追加します。
![202007052300.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/a4230a8e-173f-2e8c-fd3f-d4d8ce2d30a3.png)
Feature.vueを編集します。

```components/Feature.vue
<template>
  <div v-editable="blok" class="feature util__flex-eq">
    <img :src="resizedIcon" class="feature__icon">
    <h1>{{ blok.name }}</h1>
    <div class="feature__description">
      {{ blok.description }}
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    resizedIcon () {
      if (typeof this.blok.icon !== 'undefined') {
        return '//img2.storyblok.com/80x80' + this.blok.icon.replace('//a.storyblok.com', '')
      }
      return null
    }
  },
  props: ['blok']
}
</script>

<style lang="scss">
.feature {
  text-align: center;
  padding: 30px 10px 100px;
}

.feature__icon {
  max-width: 80px;
}
</style>
```

ブラウザからアイコンとテキストを追加して保存すると、以下のようになります。
![202007052305.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/369a2aef-d1a9-2142-f7e8-63b09649d1c7.png)
##Build a navigation menu
ダッシュボードに戻り`Contet`を開き`+Folder`ボタンを押すとポップアップが出るので、`en`と入力してSaveを押します。
![202007052300.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/dc7b4e27-507b-0e24-1afa-c1b226827cd3.png)
###Create global settings
作成した`en`フォルダを選ぶと画面が変わり、`+Entry`ボタンを押すとポップアップが出るので、下記のように`settings`と入力してSaveを押します。
![202007052312.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/0f655602-f5bd-dab7-b8aa-5f39f30ab332.png)
画面が自動的に切り替わるので`config`タブの`Real Path`に`/`と入力してSaveを押します。
`main_navi`という名前でblockを追加します。
![202007052315.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/75777782-fdc0-7b36-3bde-59a50ede9e7d.png)
`main_navi`のなかに`Nav Item`を作成し、`name`をtextとして、`link`をlinkとして追加し、下記のようにコピペして2個にしてSaveを押します。
![202007052320.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/44ea2ea9-712e-4f45-1b0a-dfef1769f22d.png)
###Getting global settings with the Vuex store
`store/index.js`を作成し、以下の内容をコピペします。
※スペースがないとエラーが出たので修正しています。

```store/index.js
export const state = () => ({
  cacheVersion: '',
  language: 'en',
  settings: {
    main_navi: []
  }
})

export const mutations = {
  setSettings (state, settings) {
    state.settings = settings
  },
  setLanguage (state, language) {
    state.language = language
  },
  setCacheVersion (state, version) {
    state.cacheVersion = version
  }
}

export const actions = {
  loadSettings ({ commit }, context) {
    return this.$storyapi.get(`cdn/stories/${context.language}/settings`, {
      version: context.version
    }).then((res) => {
      commit('setSettings', res.data.story.content)
    })
  }
}

```

###Add a middleware
`middleware/languageDetection.js`ファイルを以下の内容で作成します。
※エラーが出たので修正しています。

```middleware/languageDetection.js
export default function ({ app, isServer, route, store, isDev }) {
  const version = route.query._storyblok || isDev ? 'draft' : 'published'
  const language = route.params.language || 'en'

  if (isServer) {
    store.commit('setCacheVersion', app.$storyapi.cacheVersion)
  }

  if (!store.state.settings._uid || language !== store.state.language) {
    store.commit('setLanguage', language)

    return store.dispatch('loadSettings', { version, language })
  }
}

```

`nuxt.config.js`に設定を追記します。

```nuxt.config.js
module.exports = {
  ...
  router: {
    middleware: 'languageDetection'
  },
```

##Access the data in the TopHeader component
`TopHeader.vue`のtemplate部分を下記の内容に置き換えます。

```components/TopHeader.vue
<template>
  <header class="top-header util__flex util__container">
    <nav class="top-header__col">
      <ul class="top-header__nav">
        <li :key="index" v-for="(navitem, index) in $store.state.settings.main_navi">
          <nuxt-link class="top-header__link" :to="navitem.link.cached_url">
            {{ navitem.name }}
          </nuxt-link>
        </li>
      </ul>
    </nav>
    <a href="/" class="top-header__col top-header__logo">
      <img src="http://a.storyblok.com/f/42016/1096x313/0353bf6654/logo2.png">
    </a>
    <nav class="top-header__col top-header__second-navi">
      <ul class="top-header__nav top-header__nav--right">
        <li>
          <nuxt-link class="top-header__link" to="/en/blog">English</nuxt-link>
        </li>
        <li>
          <nuxt-link class="top-header__link" to="/de/blog">German</nuxt-link>
        </li>
      </ul>
    </nav>
  </header>
</template>

...
```

ブラウザから編集して下記のように変更します。
![202007052343.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/85fa0f0b-3faa-2c90-ebd1-cf01d5c48b8c.png)
##Build a blog section
以下のようなフォルダ構成にします。

```
pages/
--| _language/
-----| blog/
--------| _slug.vue
--------| index.vue
--| index.vue
```

###Add a blog detail page
markdown parserをインストールします。

```powershell:SHELL
$ npm install marked --save // yarn add -D marked
```

`pages/_language/blog/_slug.vue`を作成します。
※エラーが出たので修正しています。

```pages/_language/blog/_slug.vue
<template>
  <section class="util__container">
    <div v-editable="story.content" class="blog">
      <h1>{{ story.content.name }}</h1>
      <p><strong>{{ story.content.intro }}</strong></p>
      <div class="blog__body" v-html="body">
      </div>
    </div>
  </section>
</template>

<script>
import marked from 'marked'

export default {
  data () {
    return {
      story: { content: { body: '' } }
    }
  },
  computed: {
    body () {
      return marked(this.story.content.body)
    }
  },
  mounted () {
    // use the bridge to listen to events
    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()
        this.$nuxt.$router.go({
          path: this.$nuxt.$router.currentRoute,
          force: true
        })
      }
    })
  },
  asyncData (context) {
    // Load the JSON from the API
    const version = context.query._storyblok || context.isDev ? 'draft' : 'published'

    return context.app.$storyapi.get(`cdn/stories/${context.params.language}/blog/${context.params.slug}`, {
      version,
      cv: context.store.state.cacheVersion
    }).then((res) => {
      return res.data
    }).catch((res) => {
      context.error({ statusCode: res.response.status, message: res.response.data })
    })
  }
}
</script>

<style lang="scss">
.blog {
  padding: 0 20px;
  max-width: 600px;
  margin: 40px auto 100px;

  img {
    width: 100%;
    height: auto;
  }
}

.blog__body {
  line-height: 1.6;
}
</style>

```

###Create the overview page
`pages/_language/blog/index.vue`を作成します。
※エラーが出たので修正しています。

```pages/_language/blog/index.vue
<template>
  <section class="util__container">
    <div :key="blogPost.content._uid" v-for="blogPost in data.stories" class="blog__overview">
      <h2>
        <nuxt-link class="blog__detail-link" :to="'/' + blogPost.full_slug">
          {{ blogPost.content.name }}
        </nuxt-link>
      </h2>
      <small>
        {{ blogPost.published_at }}
      </small>
      <p>
        {{ blogPost.content.intro }}
      </p>
    </div>
  </section>
</template>

<script>
export default {
  data () {
    return { total: 0, data: { stories: [] } }
  },
  asyncData (context) {
    const version = context.query._storyblok || context.isDev ? 'draft' : 'published'

    return context.app.$storyapi.get('cdn/stories', {
      version,
      starts_with: `${context.store.state.language}/blog`,
      cv: context.store.state.cacheVersion
    }).then((res) => {
      return res
    }).catch((res) => {
      context.error({ statusCode: res.response.status, message: res.response.data })
    })
  }
}
</script>

<style lang="scss">
.blog__overview {
  padding: 0 20px;
  max-width: 600px;
  margin: 40px auto 60px;

  p {
    line-height: 1.6;
  }
}

.blog__detail-link {
  color: #000;
}
</style>

```

###Create the blog content in Storyblok
ブラウザから`en/blog`フォルダを作成します。
![202007052359.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/455d3345-9173-7d8b-c223-21f0e56fff90.png)
一つ記事を作った後に`blog`フォルダをデフォルトに設定します。
![202007060015.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/c5a8c16c-1b42-438f-2bf4-27f0b3fa3687.png)
###Create the blog article
ブラウザから記事を投稿します。`+Entry`を押し、以下のように入力します。
![202007060010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/9adb1ee0-60a5-c703-4186-d0a18873907d.png)
ブラウザから記事を作成します。
![202007060016.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/6c2f4b5a-6e50-d967-550f-ed7e5c1ee7a2.png)
http://localhost:3000/en/blog にアクセスすると、下記のように表示されます。
![202007060037.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/98e59eca-4cb3-9382-7cdc-281e72019298.png)
##Build a sitemap
`pages/_language/sitemap.vue`を作成します。
※エラーが出たので修正しています。

```pages/_language/sitemap.vue
<template>
  <section class="util__container">
    <div class="sitemap">
      <h1>Sitemap</h1>

      <div v-for="language in tree" :key="language.id">
        <ul>
          <sitemap-item
            v-show="item.item.name !== 'Settings'"
            :model="item"
            v-for="item in language.children"
            :key="item.id">
          </sitemap-item>
        </ul>
      </div>
    </div>
  </section>
</template>

<script>
export default {
  data () {
    return {
      links: {}
    }
  },
  computed: {
    tree () {
      const parentChilds = this.parentChildMap(this.links)

      return this.generateTree(0, parentChilds)
    }
  },
  asyncData (context) {
    const version = context.query._storyblok || context.isDev ? 'draft' : 'published'

    return context.app.$storyapi.get('cdn/links', {
      version,
      starts_with: context.store.state.language,
      cv: context.store.state.cacheVersion
    }).then((res) => {
      return res.data
    }).catch((res) => {
      context.error(res)
    })
  },
  methods: {
    parentChildMap (links) {
      const tree = {}
      const linksArray = Object.keys(links).map(e => links[e])

      linksArray.forEach((link) => {
        if (!tree[link.parent_id]) {
          tree[link.parent_id] = []
        }

        tree[link.parent_id].push(link)
      })

      return tree
    },
    generateTree (parent, items) {
      const tree = {}

      if (items[parent]) {
        const result = items[parent]

        result.forEach((cat) => {
          if (!tree[cat.id]) {
            tree[cat.id] = { item: {}, children: [] }
          }
          tree[cat.id].item = cat
          tree[cat.id].children = this.generateTree(cat.id, items)
        })
      }

      return Object.keys(tree).map(e => tree[e])
    }
  }
}
</script>

<style lang="scss">
.sitemap {
  max-width: 600px;
  margin: 20px auto 60px;
}
</style>

```

`components/SitemapItem.vue`を作成します。

```components/SitemapItem.vue
<template>
  <li class="sitemap-item">
    <nuxt-link :to="'/' + model.item.slug">
      {{model.item.name}}
    </nuxt-link>
    <ul v-if="model.children.length > 0">
      <sitemap-item
        :key="item.item.id"
        :model="item"
        v-for="item in model.children">
      </sitemap-item>
    </ul>
  </li>
</template>

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

<style lang="scss">
.sitemap-item {
  padding: 5px 0;

  a {
    color: #8ba19a;
  }

  ul {
    margin-top: 10px;
    margin-bottom: 10px;
  }
}
</style>

```

`plugins/components.js`に追記します。

```plugins/components.js
...
import SitemapItem from '~/components/SitemapItem.vue'

...
Vue.component('sitemap-item', SitemapItem)
```

http://localhost:3000/en/sitemap にアクセスすると、下記のように表示されます。
![202007060045.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/b2c8b898-2b90-8189-c400-e9aac296b94c.png)

今回はここまでです。以下は省略します。