JavaScript
vue.js
nuxt.js
contentful

Contentfulの料金と使い方を整理しつつ、Nuxt.jsと組み合わせてブログを作る

Contentfulを使ってみたくなったので、印象に残った邦画を書き残すブログを作りました。ブログを作って満足してしまったので、記事の中身は(仮)です。Contentfulの考え方に馴染むのに少し時間がかかったので、そこのところの整理がメインの記事です。

demo.gif

https://blog.houga.cc

ヘッドレスCMSというもの

CMSというと、WordPressMovable Typeぐらいしか知らなかったのですが、最近ヘッドレスCMSという言葉を耳にするようになりました。ヘッドレスCMSは、WordPressのようにテンプレートを表示する部分を持たず、APIでデータを提供することに特化している点が特徴です。

ヘッドレスCMSの選択肢は、こちらのサイトにたくさんありましたが、そのなかで、ContentfulというCMSのサービスが気になったので、Nuxt.jsと組み合わせて簡素なブログを作りました。

headlessCMS | A List of Content Management Systems for JAMstack Sites

Contentful

contentful_web.jpg

Contentful is not a CMS, Contentful is content infrastructure.

Webサイトがシンプルで見やすい。
ものは言いような気もするけれど、CMSではなくContent Infrastructureらしい。
ドキュメントも見やすくて素敵。

https://www.contentful.com

無料で使える範囲と制限

pricing.png
https://www.contentful.com/pricing/

アカウントの作成時に、MICRO SPACEというものをひとつ割り当ててもらえます。個人で使う分にはMICRO SPACEで事足りそうなので、かなり気前がいい印象です。

  • 24個のコンテンツタイプ
  • 5000個のレコード
  • 10人のユーザー
  • 1つのロール(ユーザー権限)
  • 2つの環境(master/stagingとか)
  • 2つのロケール(多言語対応)

※ 2018年11月4日時点の内容です

レコード数は、エントリーとアセットを合わせた数になるようで、たとえばブログエントリーが10件あって、5つの画像を使用している場合は、15レコードになります。

もっとスペースを使いたい場合は、MICRO SPACEで$39/月、より大きいLARGE SPACEで$879/月、その先は要お問い合わせですが、ENTERPRISE-GRADE SPACESが用意されています。

また、Fair Use PolicyでAPIの呼び出し数や、アセットの帯域の制限が決められています。画像などの数が多く、かなりのアクセスが見込まれる場合は、アセットの帯域が上限に達するかもしれません。

  • APIの呼び出し - 200万/月
  • アセットの帯域 - 0.75TB/月

Extra API calls will be billed at \$5/1,000,000 API calls/month. Extra asset bandwidth consumption will be billed at \$65/1 TB/month.

超過分はこんな感じで料金が発生します。
https://www.contentful.com/r/knowledgebase/fair-use/

引っかかることはないと思いますが、Technical Limitsもあるので参考までに。
https://www.contentful.com/developers/docs/technical-limits/

気になる部分だと思うので、できる限り細かく書いていますが、プラン内容が変更されることもあると思うので、実際に使う前には必ず公式サイトを確認してください!

https://www.contentful.com/pricing/

サインアップから利用開始まで

サインアップから指示通りに進んでいけばスムーズにできたので、この手順は省略します。
https://www.contentful.com/sign-up/

スペース

プロジェクトごとに割り当てる領域のことです。
このスペース上に、コンテンツモデルを定義していきます。

コンテンツモデル 

content_model.png

コンテンツモデルは、すべてのコンテンツを束ねる概念です。
そのモデルのなかに、「Post」や「Tag」など、コンテンツの雛形となるコンテンツタイプを作成し、そこにフィールドを追加していく流れになります。

今回は、印象に残った邦画を書き残すブログを作りたいので、「Post」という名前のコンテンツタイプを作成し、映画のタイトルや公開日、感想などのフィールドを追加しました。データの種類やバリデーションなどを直感的に選べるので、説明をあまり読まなくてもサラッと進められて素敵です。

タグを設定したい

How do I categorize my content?

In Contentful there are two ways to create categories, either using short text fields or using Reference fields to other entries.

Basics - FAQ

Contentfulはブログサービスではないので、デフォルトではカテゴリやタグが用意されていませんが、コンテンツタイプとして「Tag」を作成して、それを「Post」から参照することで実現できます。この柔軟さは魅力的です。

Content trees, tags, and facets in Contentful

複数の環境やプレビューのAPIなど

今回は試せていませんが、master以外の環境を作れたり、ドラフトの記事を取得できるプレビューのAPIがあったりで、色々と他にも使えそうな機能があったので、また使う機会があったら試してみたい。

APIの準備

use_api.jpg

「Use the API」からアクセストークンの設定ページに移動して作成します。
この画面にある、Space IDContent Delivery API - access tokenは、のちほど必要になります。

Nuxt.js

Contentfulの設定が一通り終わったので、フロントエンドの実装をしていきます。

アプリケーションの作成

ドキュメントに従ってインストールします。
この記事を書いている時点で、Nuxt.jsのバージョンは 2.2 です。

$ npx create-nuxt-app <project-name>

インストール - Nuxt.js

開発を始める

nuxt_install.jpg

作成したプロジェクトに移動して、開発サーバーを起動します。

$ cd <my-project>
$ npm run dev

APIで記事を取得してみる

index01.png

JavaScript用のSDKが用意されているので、それを使って記事を取得してみます。

APIに関する処理はそれなりに増えそうだったので、/apiディレクトリを作成して処理をまとめました。spaceaccessTokenには、先ほど準備したものを指定しますが、@nuxtjs/dotenvあたりを使って、環境変数に設定して使用するのが良いと思います。

一覧の取得はできましたが、「Post」と「Tag」のコンテンツタイプが一緒に取得されてしまっているので残念な感じです。

# SDKの追加
$ npm install --save contentful
/api/index.js
import { createClient } from 'contentful'

const client = createClient({
  space: process.env.SPACE_ID,
  accessToken: process.env.ACCESS_TOKEN
})

export const fetchEntries = () => client.getEntries()
/pages/index.vue
<template>
  <ul>
    <li v-for="item in items" :key="item.sys.id">{{ item.fields.title }}</li>
  </ul>
</template>

<script>
import { fetchEntries } from '@/api'

export default {
  async asyncData() {
    return await fetchEntries()
  }
}
</script>

コンテンツタイプを指定して取得する

index02.png

ドキュメントのSearch parametersを参考に、content_typeを指定して絞り込むと、思い通りの表示になりました。

/api/index.js
export const fetchEntries = content_type => client.getEntries({ content_type })
/pages/index.vue
<script>
export default {
  async asyncData() {
    return await fetchEntries('post')
  }
}
</script>

タグのIDで絞り込んで、公開日でORDER BY DESC

参照型の「Tag」のidで絞り込んで、releaseDateのフィールドで降順の並び替えをする例です。ネストしている場合も直感的に指定できてすごい。降順(DESC)にしたい場合は、orderの値に-を付けるといけます。APIの設計する人が大変そう。

Filter API results with relational queries
Reverse order

export const fetchPostsByTagId = id =>
  client.getEntries({
    content_type: 'post',
    'fields.tags.sys.id': id,
    order: '-fields.releaseDate'
  })

ページネーション

skipにオフセット値を指定して、limitに件数を指定すると、ページに分けてデータを取得できます。

const POSTS_PER_PAGE = 30

export const fetchPosts = page =>
  client.getEntries({
    content_type: 'post',
    order: '-fields.releaseDate',
    skip: (page - 1) * POSTS_PER_PAGE,
    limit: POSTS_PER_PAGE
  })

見た目を整える

demo.gif

あとはひたらすらページのコーディングをしていきます。今回のブログは、とにかく簡素にしたかったので、表示項目を最小限に絞りました。見た目に関しては特に書くことがないので省略しますが、コードをGitHubにアップしています。

https://github.com/noplan1989/houga-blog

動的なルートの明示

generateコマンドでは 動的なルーティング は無視されます。

generateプロパティ

Nuxt.jsで静的サイトを生成する際に、/entry/_idのような動的なルートは、設定ファイルで明示してあげる必要があります。Nuxt.jsから見たら、ビルド時にどんなIDのエントリーがあるかは知る由もないので、APIで取得してルートを網羅して返すようにします。

ルート生成時にpayloadを渡して高速化する方法もあるようなので、参考までに。
payloadによる動的ルーティング生成の高速化

nuxt.config.js
import { createClient } from 'contentful'

require('dotenv').config()

export default {
  generate: {
    routes: async function () {
      const client = createClient({
        space: process.env.SPACE_ID,
        accessToken: process.env.ACCESS_TOKEN
      })

      const { items } = await client.getEntries({
        content_type: 'post',
        limit: 1000 // 最大
      })

      return items.map(item => `/entry/${item.sys.id}`)
    }
  }
}

静的サイト生成

設定が終わったら、コマンドを実行して静的サイトを出力し、表示を確認します。

$ npm run generate

デプロイ

あとは好きなところへホスティング。

CircleCIでビルドしてAWSのS3へアップ

CloudFront+S3の環境が好きなので、CircleCI経由でデプロイするように設定しました。ここは、人によってホスティング先が違うと思うので省略しますが、AWSを使いたい方は別の記事に書いているので、参考までに。

Nuxt.js+CircleCIで静的ページをAWSのS3へデプロイする

Contentfulの記事更新をきっかけにCircleCIを起こす

webook.png

Contentfulの管理画面のSettings → Webhooksから設定できます。
CircleCIのテンプレートが用意されていたので、CircleCIの管理画面からAPIのトークンを取得して、画面の指示通りに設定していけば無事に連携できました。どのコンテンツタイプが変更されたらフックするかなどを細かく設定できて便利です。

Webhooks | Contentful
Managing API Tokens | CircleCI

アクセストークンとブラウザで再会

Nuxt.jsのgenerateで静的サイトを生成した場合、ページ遷移時はSPAのようにふるまうので、APIの通信が発生します。そのため、ブラウザを少しのぞくだけで、環境変数に設定したSPACE_IDACCESS_TOKENが確認できてしまいます。

The Content Delivery API (CDA), available at cdn.contentful.com, is a read-only API for delivering content from Contentful to apps, websites and other media.

Content Delivery API

幸いContent Delivery APIはread-onlyなので、不正なコンテンツの投稿や改ざんをされる心配はありませんが、どうも気持ちが悪いです。アクセストークンなどが絡む処理はサーバーサイドで実装するのが筋だとは思いますが・・・面倒になってしまったので、ブログはこのままにしました。

どうせPublicなコンテンツなので、データが欲しければアクセストークンを自由に使ってくれというスタンスで。今回の場合は、ただのブログなので誰に見られても問題のないコンテンツですが、会員サイトのコンテンツを扱う場合は、Nuxt.jsにアクセストークンを設定してしまうと、漏れた時にコンテンツが垂れ流しになってしまうのでマズいです。

そもそも会員サイトならサーバー側で認証が入るはずなので、Nuxt.jsにアクセストークンを置くようなことはしないと思いますが。

WordPress vs Contentful ではない

Contentfulを使ってみて、その使いやすさや柔軟性には感動しましたが、「WordPressはもう古い!これからの時代はContentful!」などと言うつもりはサラサラなく、ブログをサクッと作りたいときには、これからもWordPressを使うと思います。プラグインが豊富だったり、運用担当の人が操作に慣れていたりで、なんだかんだ融通が効くので。一方、個人的にWordPressのコードを書いている時間はあまり楽しくないので、仕事以外では別のCMSを使ってバランスをとりたいというのが、正直なところです。

ブログを作って満足してしまって、記事がまだ(仮)なので、そのうち書きたい。

ブログ
https://blog.houga.cc

ソースコード
https://github.com/noplan1989/houga-blog