2020年2月13日に投稿

【自作ブログ】Contentfulから記事を取得しよう

連載第4回目です。
今回はいよいよContentfulから記事を取得するところまで進めていきます。
今まで下準備してきたNetlify、Contentful、そしてNuxt.jsが全て繋がります。
Nuxtの説明も交えて進めていきますので、少しテンポが悪くなりますがご了承ください。

今回の作業の進め方

今回は以下の手順で進めていきます。

  1. 簡単なレイアウトを整える(ナビゲーションバーを追加)
  2. 記事を表示するコンポーネントを作る
  3. Contentfulの記事を取得する準備
  4. Contentfulの記事を表示する
  5. Netlifyにデプロイする

簡単なレイアウトを整える

さすがに何も無いのは寂し過ぎるので、ナビゲーションバーくらいは作っておくことにします。
必須ではないので不要の方はスキップしても問題ありません。

ナビゲーションバーを追加する

はじめにナビゲーションバーのコンポーネントを作っていきます。
Nuxtのコンポーネント(画面上の部品)はcomponents/配下に置くルールになっているので、components/Navbar.vueを新規追加してナビゲーションバーコンポーネントを作ります。

ナビゲーションバーのコンポーネントを追加

$ touch components/Navbar.vue

components/Navbar.vue

<template>
  <b-navbar class="is-info">
    <template slot="brand">
      <b-navbar-item :to="{ path: '/' }" tag="router-link">
        <strong class=" is-size-3-desktop"><i>Sample Blog</i></strong>
      </b-navbar-item>
    </template>
  </b-navbar>
</template>

<script>
export default {

}
</script>

<style>

</style>

<b-navbar>はBuefyで使えるタグになります。
詳しくはこちらに記載されていますが、レスポンシブなナビゲーションバーを表示することができます。

デフォルトレイアウトを変更

次にレイアウトファイルを修正してナビゲーションバーのコンポーネントを表示できるようにします。
layouts/配下にあるvueファイルは画面を描画する際のレイアウトとして認識されます。
中でもlayouts/default.vueは特別で、ビュー側のテンプレート(pages/配下のvueファイル)でlayoutを指定しない場合は、自動的にlayouts/default.vueのレイアウトが適用されるようになっています。


layouts/default.vue

<template>
  <div>
    <navbar />
    <section class="container">
      <div class="columns is-mobile">
        <div class="column main-page-contents">
          <nuxt />
        </div>
      </div>
    </section>
  </div>
</template>

<script>
import Navbar from '@/components/Navbar.vue'

export default {
  components: {
    Navbar
  }
}
</script>

<style scoped>
#body-contents {
  margin-top: 2em;
}
</style>


先ほど作ったナビゲーションバーをimportで読み込んでcomponentsに登録しています。ちなみに@/components/Navbar.vue~/components/Navbar.vueでも大丈夫です。
あとはテンプレート内でナビゲーションバーコンポーネントを<navbar />で表示するようにしています。
画面で確認すると以下のように表示されます。
navbar

記事を表示するカードを作る

いつまでも表示されているHello worldの代わりに記事を表示するカード形式のコンポーネントを作っておきます。
components/Card.vueというファイルがすでに存在していると思いますので、それを編集していきます。(無い場合は追加してください)


components/Card.vue

<template>
  <div class="box is-radiusless">
    <article class="media">
      <figure class="media-left">
        <div v-if="post.fields.headerImage">
          <p class="image is-128x128">
            <img :src="post.fields.headerImage.fields.file.url" alt="thumbnail">
          </p>
        </div>
        <div v-else>
          <p class="image is-128x128">
            <img src="https://bulma.io/images/placeholders/128x128.png" alt="thumbnail">
          </p>
        </div>
      </figure>
      <figure class="media-content">
        <div class="content">
          <div class="is-size-4">
            {{ post.fields.title }}
          </div>
          <div class="has-text-right">
            <small>{{ getFormattedDate(post.fields.publishedAt) }}</small>
          </div>
        </div>
      </figure>
    </article>
  </div>
</template>

<script>
export default {
  props: {
    post: {
      type: Object,
      reqire: true,
      default: () => {
        return {
          fields: {
            title: 'sample',
            publishedAt: new Date(),
            headerImage: null
          }
        }
      }
    }
  },
  methods: {
    getFormattedDate (date) {
      const originDate = new Date(date)
      const year = originDate.getFullYear()
      const month = originDate.getMonth() + 1
      const day = originDate.getDate()
      return `${year}${month}${day}日`
    }
  }
}
</script>

<style scoped>
.box {
  box-shadow: 0.5em;
  margin-bottom: 10px;
}
</style>

propsにて親コンポーネントから表示内容を受け取るように実装しています。
次にHello worldの代わりに上記のコンポーネントを表示するようにpages/index.vueを修正します。


pages/index.vue

<template>
  <div>
    <card v-for="(post, index) in posts" :key="index" :post="post" />
  </div>
</template>

<script>
import Card from '@/components/Card.vue'

export default {
  components: {
    Card
  },
  data () {
    return {
      posts: [
        {
          // 1件目
          fields: {
            title: 'これはテストです',
            publishedAt: new Date()
          }
        },
        {
          // 2件目
          fields: {
            title: 'これはテスト2です',
            publishedAt: new Date()
          }
        }
      ]
    }
  }
}
</script>


テスト用のデータを2件用意しておきました。画面で確認してみましょう。

mock_article_list

いいですね。これでContentfulから取得したデータを表示できそうです。

Contentfulからデータを取得する

それでは今回のメインです。
まず公式に記載されている方法でJavascript用のSDKをインストールします。

$ npm install --save contentful

Contentfulのアクセスキーを環境変数に設定する

続いてContentfulへの接続に必要なアクセスキーを環境変数に設定します。
まずはContentfulのマイページ上部メニューの「Settings」の「API Key」を選択します。
「Example Key 1」というキーがプリセットされているはずなので、今回はこれを使います。
「Example Key 1」をクリックすると以下のアクセスキーが表示されるので、この値を.envファイルに定義していきます。

  • Space ID
  • Content Delivery API
  • Content Preview API

プロジェクトの直下に.envファイルを作成します。

$touch .env

そして先ほど確認したContentfulのアクセスキーを登録します。
公式では.contentful.jsonを使った方法がとられていますが、ここでは.envを使います。
なお、.envファイルはGitHubなどのリポジトリにpushする際に無視されるようになっていますので、ご安心を。


.env

CTF_SPACE_ID=<Space ID>
CTF_CDA_ACCESS_TOKEN=<Content Delivery API>
CTF_PREVIEW_ACCESS_TOKEN=<Content Preview API>

.envに値を設定する場合はダブルクォート(")で囲む必要はありません。
続いてnuxt.config.jsenvプロパティを追加して、.envに定義した環境変数を読み込めるようにします。


nuxt.config.js

require('dotenv').config()
export default {
  ~中略~
  env: {
    CTF_SPACE_ID: process.env.CTF_SPACE_ID,
    CTF_CDA_ACCESS_TOKEN: process.env.CTF_CDA_ACCESS_TOKEN,
    CTF_PREVIEW_ACCESS_TOKEN: process.env.CTF_PREVIEW_ACCESS_TOKEN
  },
  ~中略~
}

Contentful SDKクライアントを生成するpluginを作る

次にNuxtのプラグイン機能(共通の処理などを定義したJSファイルを読み込む)を使ってSDKクライアントを生成する処理を実装します。公式のチュートリアルに倣ってplugins/contentful.jsを追加して編集します。

$ touch plugins/contentful.js

plugins/contentful.js

const contentful = require('contentful')

const defaultConfig = {
  space: process.env.CTF_SPACE_ID,
  accessToken: process.env.CTF_CDA_ACCESS_TOKEN
}
const sdkClient = contentful.createClient(defaultConfig)

export default sdkClient

これでContentfulからデータを取得するためのSDKクライアントを生成する準備は整いました。
さっそくpluginを利用して記事のデータを取得してみましょう。

pages/index.vue

<template>
  <div>
    <card v-for="(post, index) in posts" :key="index" :post="post" />
  </div>
</template>

<script>
import Card from '@/components/Card.vue'
import sdkClient from '@/plugins/contentful.js'

export default {
  components: {
    Card
  },
  async asyncData ({ env }) {
    let posts = []
    await sdkClient.getEntries({
      content_type: 'blogPost',
      order: '-fields.publishedAt'
    }).then((res) => {
      posts = res.items
    }).catch(console.error)
    return { posts }
  }
}
</script>

pluginをインポートして取得したSDKクライアントを用いてasyncDataメソッド内でContentfulからデータを取得しています。
asyncDataメソッドはページコンポーネントがロードされる前に実行されます。
実際にContentfulに問合せている処理はsdkClient.getEntriesメソッドになります。

await sdkClient.getEntries({
  content_type: 'blogPost',
  order: '-fields.publishedAt'
})

引数に渡しているオブジェクトのcontent_typeには前回作成したContent modelのContent type IDである「blogPost」を指定します。
orderはその名の通りソートを指定できますが、頭に-を付けることで逆順(DESC)にすることができます。そのため、今回は「公開日が新しい順」にデータを取得しています。

では最後にちゃんとデータが取れているか画面でも確認してみましょう。

get_data_from_contentful

やりましたね。これで自由自在にContentfulからデータを取得できるようになりました。

Netlifyにデプロイする

最後にここまでの成果をNetlifyにデプロイしておきます。

GitHubにpush

まずはGitHubにpushしていきます。

$ git add -A
$ git commit -m "Contentfulからデータを取得する処理を実装"
$ git checkout master
$ git push origin master

Netlifyに環境変数をセットする

ここで忘れてはいけないのは.envファイルはGitHubにpushされないことです。
.env ファイルで設定した内容と同じものをNetlifyの環境変数にも設定していきます。

Netlifyの管理ページ上部のメニューの「Settings」から「Build & Deploy」 → 「Environment」を選択します。そして「Edit variables」をクリックします。

netlify_environments


キーと値のカラムが表示されるので、必要な値を設定して「Save」しましょう。

set_env

あとは環境変数を反映させるためにもう一度デプロイする必要があるので、
ページ上部のメニューの「Deploys」から「Trigger deploy」→「Deploy site」を選択して手動でデプロイを実行します。

deloy_immediately

Netlifyのビルドが終わったのを確認してサイトのURLにアクセスして確認してみましょう。先ほどローカルで確認したのと同じようにContentfulの記事の内容が表示されていればバッチリです。

終わりに

お疲れ様でした。ついにNetlify、Contentful、Nuxt.jsが繋がりましたね。
次回は記事の詳細画面を作っていきたいと思います。
更新の告知はTwitterで行っていますので、ぜひフォローもお願いします!
それではまた次回。