Nuxt.js+Contentful+Netlifyでブログサイトを作るために環境構築する

はじめまして、新卒１年目フロントエンドエンジニアのフジタです。
今回はNuxt.jsで個人的にブログサイトを作成しているのでそれについてまとめていこうと思います。

## Nuxt.jsとは

SSR(サーバーサイドレンダリング)に対応したVue.jsのフレームワークです。~~僕はSSRって聞くとなんだかワクワクします。~~

まだまだ勉強中なので詳しく知りたい方は[公式ドキュメント](https://ja.nuxtjs.org/)をご覧ください。


## SSR(サーバーサイドレンダリング)とは

SPA(シングルページアプリケーション)などでブラウザで行われていたJSの実行とHTML生成をサーバー側で行う技術です。
動作の流れとしては以下です。

1. クライアント側からHTTPリクエストが送られる(差分のみ)
2. サーバーが更新されたHTMLを生成してクライアント側に返す
3. ブラウザ側でHTMLを表示

差分のみのHTTPリクエストが送信されるのはSPAも同じです。SPAとの違いはHTMLの生成をブラウザ側で行わずにサーバー側で行う点にあります。
メリットとしてSEOの向上、ユーザーの通信環境に左右されにくいことが挙げられます。

> 検索エンジンのクローラが完全に描画されたページを直接解析するため、SEO が向上します。
> (中略)
> 特にインターネットの遅さや遅いデバイスでは、コンテンツの再生時間が短縮されます。サーバで描画されたマークアップは、すべての JavaScript がダウンロードされて表示されるまで待つ必要がないので、ユーザは完全に描画されたページをすぐに見ることができます。 

引用：[どうしてSSRなのか？ - Vue SSR ガイド](https://ssr.vuejs.org/ja/#%E3%82%B5%E3%83%BC%E3%83%90%E3%82%B5%E3%82%A4%E3%83%89%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0-ssr-%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B)

デメリットはサーバー側の負担が大きくなることやNode.jsが実行できるサーバーを用意する必要があることが挙げられます。

> - より複雑なセットアップと開発の要件を構築します。静的ファイルサーバに展開できる完全静的 SPA とは異なり、サーバで描画されたアプリケーションでは Node.js サーバを実行できる環境が必要になります。

> - サーバ側の負荷が増えます。 Node.js の完全なアプリケーションを描画することは、静的ファイルを提供するだけでなく、CPU を多用することになるので、トラフィックが多いことが見込まれる場合は、対応するサーバの負荷に備え、キャッシュの対策を賢明に行なってください。

引用：[どうしてSSRなのか？ - Vue SSR ガイド](https://ssr.vuejs.org/ja/#%E3%82%B5%E3%83%BC%E3%83%90%E3%82%B5%E3%82%A4%E3%83%89%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0-ssr-%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B)

## Nuxt.js以外に使うもの

- Netlify
    - 静的サイトをホスティングすることができるWebサービス。超便利
- Contentful
    - ヘッドレスCMS(ビューのないCMS)
    - ブログ記事のコンテンツ管理に使用します

### ヘッドレスCMSをふわっと説明

通常のCMS(WordPressとか)はWebページ(フロントエンド)と管理(バックエンド)が1つに統合されています。ヘッドレスCMSはその2つを分けようという構造から生まれたCMSです。

## つべこべ言わずに環境構築していく

### create-nuxt-app を使う

すぐ始めたいのでcreate-nuxt-appを使用します。
※コマンド入力した後に色々選択します。(今回は省略)

```console
$ npm -v
6.9.0

$ npm init nuxt-app sample-blog
```

### 起動できた

依存関係をインストール後、プロジェクトのフォルダにいき`npm run dev`をすると起動します。今回は試しにVuetifyを使ってみようと思い選択したので起動画面がいつもと違いました。びっくりした。
![スクリーンショット 2020-12-13 21.10.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409946/c93df218-770f-c22f-3347-23aad055fb1a.png)

## 仮でコンテンツの実装をする

一旦仮で記事コンテンツの実装をします

```components/card.vue
<template>
  <v-card class="card mb-5">
    <nuxt-link
      :to="{ name: 'blog-slug'}"
      class="wrapper"
    >
      <v-card-title>記事タイトル</v-card-title>
    <v-card-text>
      <v-row
        align="center"
        class="mx-0"
      >
      </v-row>
      <div>記事の概要</div>
    </v-card-text>
    </nuxt-link>
  </v-card>
</template>

<style lang="scss" scoped>
.wrapper {
  text-decoration: none;
}
</style>
```

```pages/index.vue
<template>
  <v-container class="index">
    <card
      v-for="i in 5"
      :key="i"
    />
  </v-container>
</template>

<script>
import Card from '~/components/card.vue'

export default {
  components: {
    Card
  }
}
</script>
```

```pages/blog/_slug.vue
<template>
  <v-container class="slug">
    <h1 class="slug_title">
      タイトル
    </h1>
    <p class="slug_date">2020/12/14</p>
    <div>
      記事の内容
    </div>
  </v-container>
</template>
```

`nuxt.config.js`でデフォルトでテーマが設定されているので背景が黒いですが、白だと同化してしまうのでそのままにしておきます。

![スクリーンショット 2020-12-13 22.49.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409946/ca993444-7ce5-26fa-ef12-5e399dd68e8f.png)

Vuetifyのおかげですでにそれっぽく見えますね＾＾

## contentfulの設定をする

### ログインする

[こちらから](https://www.contentful.com/)ログインできます。

### コンテンツを作成する

1. 上部の`Content model`から`Add content type`で作成
2. modalが出てくるので入力して作成

![スクリーンショット 2020-12-13 23.08.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409946/5083cc17-1000-4f59-53c4-731de2b72094.png)

![スクリーンショット 2020-12-13 23.05.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409946/498a08b7-71d4-6298-7f7d-f07a77bbca68.png)

### modelを使用してコンテンツを追加する

上部の`Content`から`Add [type名]`でコンテンツを何個か作成します。

![スクリーンショット 2020-12-13 23.11.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409946/5e957735-5404-4c61-66a1-606615370128.png)

![スクリーンショット 2020-12-13 23.20.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409946/7b2bd654-4e8c-e668-71f0-a1c30eea3518.png)

### Contentfulをnuxtプロジェクトに追加する

```console
$ npm add --dev contentful
```

以下、設定ファイルです。

```lib/config.js
require('dotenv').config()

function getValidConfig(configEnv, keys) {
  let { config, missingKeys } = keys.reduce(
    (acc, key) => {
      if (!configEnv[key]) {
        acc.missingKeys.push(key)
      } else {
        acc.config[key] = configEnv[key]
      }
      return acc
    },
    { config: {}, missingKeys: [] }
  )

  if (missingKeys.length) {
    throw new Error(`Contentful key is missing : ${missingKeys.join(', ')}`)
  }
  return config
}

module.exports = {
  getConfigForKeys(keys) {
    const configEnv = {
      CTF_BLOG_POST_TYPE_ID: process.env.CTF_BLOG_POST_TYPE_ID,
      CTF_SPACE_ID: process.env.CTF_SPACE_ID,
      CTF_CDA_ACCESS_TOKEN: process.env.CTF_CDA_ACCESS_TOKEN
    }
    return getValidConfig(configEnv, keys)
  }
}
```

```plugins/contentful.js
const contentful = require('contentful')
const defaultConfig = {
  CTF_SPACE_ID: process.env.CTF_SPACE_ID,
  CTF_CDA_ACCESS_TOKEN: process.env.CTF_CDA_ACCESS_TOKEN
}

module.exports = {
  createClient(config = defaultConfig) {
    return contentful.createClient({
      space: config.CTF_SPACE_ID,
      accessToken: config.CTF_CDA_ACCESS_TOKEN
    })
  }
}
```

```nuxt.config.js
import colors from 'vuetify/es5/util/colors'

const pkg = require('./package')
const { getConfigForKeys } = require('./lib/config.js')
const ctfConfig = getConfigForKeys([
  'CTF_BLOG_POST_TYPE_ID',
  'CTF_SPACE_ID',
  'CTF_CDA_ACCESS_TOKEN'
])

const { createClient } = require('./plugins/contentful')
const cdaClient = createClient(ctfConfig)

export default {
　// 省略
  generate: {
    routes() {
      return cdaClient
        .getEntries(ctfConfig.CTF_BLOG_POST_TYPE_ID)
        .then(entries => {
          return [...entries.items.map(entry => `/blog/${entry.fields.slug}`)]
        })
    }
  },
  env: {
    CTF_SPACE_ID: ctfConfig.CTF_SPACE_ID,
    CTF_CDA_ACCESS_TOKEN: ctfConfig.CTF_CDA_ACCESS_TOKEN,
    CTF_BLOG_POST_TYPE_ID: ctfConfig.CTF_BLOG_POST_TYPE_ID
  }
```

```.env
// .env
CTF_SPACE_ID=<Contentfulで記載されているspace_id>
CTF_CDA_ACCESS_TOKEN=<Contentfulで記載されているアクセストークン>
CTF_BLOG_POST_TYPE_ID=<contentfulで記載されているポストタイプ>
```

### Contentfulに対応したコンテンツの内容に書き換える

```components/card.vue
<template>
  <v-card class="card mb-5">
    <nuxt-link
      :to="{ name: 'blog-slug', params: {
        sys: id
      }}"
      class="wrapper"
    >
      <v-card-title>{{ title }}</v-card-title>
    <v-card-text>
      <v-row
        align="center"
        class="mx-0"
      >
      </v-row>
      <div>{{ date }}</div>
    </v-card-text>
    </nuxt-link>
  </v-card>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: ''
    },
    id: {
      type: String,
      default: ''
    },
    date: {
      type: String,
      default: ''
    }
  }
}
</script>

<style lang="scss" scoped>
.wrapper {
  text-decoration: none;
}
</style>
```

```pages/index.vue
<template>
  <v-container class="index">
    <card
      v-for="(post,i ) in posts"
      :key="i"
      :title="post.fields.title"
      :id="post.sys.id"
      :date="post.sys.updatedAt"
    />
  </v-container>
</template>

<script>
import Card from '~/components/card.vue'
import { createClient } from '~/plugins/contentful.js'

const client = createClient()
export default {
  transition: 'slide-left',
  components: {
    Card
  },
  asyncData({ env, params }) {
    return client
      .getEntries(env.CTF_BLOG_POST_TYPE_ID)
      .then(entries => {
        return {
          posts: entries.items
        }
      })
      .catch(console.error)
  }
}
</script>
```

```pages/blog/_slug.vue
<template>
  <v-container class="slug">
    <h1 class="slug_title">
      {{ article.fields.title }}
    </h1>
    <p class="slug_date">{{ article.sys.updatedAt }}</p>
    <div>
      {{ article.fields.body.content[0].content[0].value }}
    </div>
  </v-container>
</template>

<script>
import { createClient } from '~/plugins/contentful.js'

const client = createClient()
export default {
  props: {
    id: {
      type: String,
      default: ''
    }
  },
  transition: 'slide-right',
  async asyncData({ env, params }) {
    return await client
      .getEntry(params.sys)
      .then(entrie => {
        return {
          article: entrie
        }
      })
      .catch(console.error)
  }
}
</script>
```

### Contentfulのコンテンツを持ってこれた

![スクリーンショット 2020-12-13 23.51.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409946/b6c18f47-9683-98a0-768a-2766edcf00e7.png)

![スクリーンショット 2020-12-13 23.51.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409946/a609222c-ca57-4e84-16fb-2fddd3017372.png)

## Netlifyの設定をする

### デプロイ
1. GitHubでログインしておきます。
2. `New site from Git`を押す
3. Gitのプロバイダーを選択する(今回はGitHub)
4. Configure the Netlify app on GitHub.を押してGitHubでNetlifyと連携するリポジトリを追加する
5. `Build command`と`Publish directory(今回はdist)`を設定してデプロイする

### ContentfulとNetlifyのWebhook連携

先ほどの`.env`のキーを入力します。

![スクリーンショット 2020-12-14 0.21.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409946/a28046eb-070c-01af-fff6-64057623bfd6.png)

### 完成！

![スクリーンショット 2020-12-14 0.39.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409946/4594e779-867f-bd9c-2ff5-55458ab5c9b9.png)

## 最後に
以上、環境構築編でした。この後にマークダウンで記述するための設定やコーディングなどが控えているのですが、力尽きてしまったので今回はここまでになります。

次回 ~~気が向いたら~~マークダウン編です。