CircleCI × Contentful × S3で作るJamstackなブログ環境。

2020.12.13

こんにちは、ベルリンオフィスの小西です。

最近日本語の記事も充実してきたJamstack。説明はこちらの記事で書かれているので省略しますが、要はサイト表示が高速でセキュリティも強くてスケーラビリティもある環境構成のことです。

今回は爆速にページを表示でき、記事データの管理も楽で、かつセキュリティもあまり気にしなくていいブログサービスを構築したいと思います。

目次

記事が長めで初歩的なことも書いてたりするので、適宜必要な見出しまで飛んでってください。

ローカルでアプリ立ち上げたいだけなら1と2だけ、既にContentfulアプリがあってCircleCIと連携したい人は5だけ読んでいただければ。

環境構成

一つのサービスがボトルネックにならないよう、マイクロサービスの集合をイメージしています。

Contentful
ヘッドレスCMSとして、今回作るブログの記事を入稿/管理します。

※ヘッドレスCMSとは
管理画面だけを提供してくれるCMSで、APIでコンテンツを取得する前提で作られており、Jamstack構成と相性がいいです。

Gatsby.js
jamstack.orgでもフレームワークとして真っ先に挙げられている静的サイト生成フレームワーク。

CircleCI
Github上のソースコードの変更、もしくはContentful上での記事更新をトリガーとして自動でビルドとデプロイを行うようします。

S3
サイトのホスティングを行います。今回の構成ではここだけお金がかかります。

Github
説明不要。ソースコードの保管場所として利用します。

前提

  • node.jsをインストール済み
  • AWSアカウントを作成済み
  • Githubアカウントを作成済み

1. Contentfulの準備

1-1. www.contentful.com に登録する

ある程度の規模のブログであれば無料で運用できてしまうのがContentfulのいいところ。

1-2. Spaceを作成する

ContentfulはSpaceという単位でアプリを区切ります。

今回はTest AppというSpaceを作ってみました。

Contentful Screenshot

1-3. Content modelを作成する

Conetnt modelとは「記事に入力する項目の設定」です。

今回は、記事のタイトル、URLスラッグ、サムネイル画像、本文の4項目を投稿するだけの簡単なブログを作ってみます。

Contentfulの「Content model」から「Add content type」をクリックします。

APIで記事を取得する際、コンテンツモデルごとのクエリを書くので、コンテンツモデルを特定するAPI IDはblogArticleとしておきます。

Contentful Screenshot

1-4. 入力フィールドの作成

まだblogArticleの中身が空っぽなので、「Add field」から追加していきます。

Contentful Screenshot

次の4つのフィールドを作成します。

  1. Title(Field ID: title): 記事のタイトルです。「Text」タイプを選択し、作成時、「This field represents the Entry title」にチェックを入れ、かつValidationから「Required field」にチェックを入れてください。
  2. Slug(Field ID: slug): 記事URLのSlugです。「Text」タイプを選択し、作成時、Validationから「Required field」「Unique field」にチェックを入れ、Appearanceから「URL」を選択します。
  3. Thumbnail(Field ID: thumbnail): サムネイル画像です。「Media」タイプを選択します。
  4. Content(Field ID: content): 記事の本文です。「Rich Text」タイプを選択します。

最終的にこんなフィールドができていればOK。

Contentful Screenshot

これで記事を投稿する準備ができましたので、保存します。

1-4. テスト記事の投稿

ヘッダーの「Content」に移動して、新規で記事を作成します。

さきほどContent modelとして設定した項目に入力し、テスト記事を公開しましょう。

Contentful Screenshot

注意点として、ここで記事がDraftになっていたりすると、アプリ側で記事を取得できないので、確実に最低一記事は公開しておきましょう。

1-5. APIキーの取得

アプリ側からAPIで取得するための鍵情報を取得します。

ヘッダー「Settings」から「API keys」に進み、「Add API key」をクリックします。

名前はなんでもいいのですが、このページでSpace IDContent Delivery API - access tokenをメモしておいてください。

2. Gatsbyアプリの立ち上げ

今回アプリはGatsby.jsで立ち上げます。すでにContentfulと連携済みのgatsbyアプリがある場合はこのセクションは飛ばしてください。

2-1. Gatsbyのコマンドラインツールをインストール

$ npm install -g gatsby-cli

2-2. アプリを作成するディレクトリに移動して、テストサイトを作成

$ gatsby new hello-world

newは新しいGatsbyのアプリを作成するコマンドで、これでカレントディレクトリ直下にhello-worldというフォルダが作られます。

2-3. まずはアプリを起動してみる

$ cd hello-world
$ gatsby develop

これでDEVサーバーが起動されるので、http://localhost:8000/ にアクセスしてみます。

Gatsby top

成功しました、これでまずGatsbyのベースは作ることができたのでいったんCtrl+Cで終了します。

2-4. 必要なプラグインをインストール

$ npm install --save gatsby-source-contentful@3.1.1 dotenv gatsby-plugin-s3 @contentful/rich-text-react-renderer
  • gatsby-source-contentful...Contentfulのデータを扱う。後述のリッチテキスト出力のためにバージョン3をインストール
  • dotenv...環境変数を扱えるようになる
  • gatsby-plugin-s3...s3にビルド&デプロイする
  • @contentful/rich-text-react-renderer...Contentfulのリッチテキストを出力する

次にContentfulとの連携を行います。

2-5. 必要なプラグインをインストール

gatsby-config.js

plugins: [
    ...
    {
      resolve: `gatsby-source-contentful`,
      options: {
        spaceId: process.env.CONTENTFUL_SPACE_ID,
        accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
      },
    },
],

Contentfulで発行されるSpace IDやAccess Tokenを書いておく必要がありますが、次に書くように環境変数として管理します。

2-6. 環境変数の設定

ルートディレクトリに.envファイルを作成し、先ほどメモしたContentfulの鍵情報を記載します。

.env

CONTENTFUL_SPACE_ID=123456789(自分のSpace ID)
CONTENTFUL_ACCESS_TOKEN=abcdefghi(自分のToken)

またgatsby-config.jsに戻って、一番上の行に書き足します。

gatsby-config.js

require('dotenv').config();

module.exports = {
...

2-7. ビルド時のコンテンツ取得設定

ルートにあるgatsby-node.jsの修正して、Contentfulから記事を取得してアプリをビルドできるようにします。

gatsby-node.js

const path = require(`path`);
const slash = require(`slash`);
exports.createPages = ({ graphql, actions }) => {
  const { createPage } = actions;
  // we use the provided allContentfulBlogArticle query to fetch the data from Contentful
  return graphql(
    `
      {
        allContentfulBlogArticle {
          edges {
            node {
              id
              slug
            }
          }
        }
      }
    `
  ).then(result => {
      if (result.errors) {
        console.log("Error retrieving contentful data",      result.errors);
      }
      // Resolve the paths to our template
      const blogArticleTemplate = path.resolve("./src/templates/post.js");
      // Then for each result we create a page.
      result.data.allContentfulBlogArticle.edges.forEach(edge => {
        createPage({
          path: `/post/${edge.node.slug}/`,
          component: slash(blogArticleTemplate),
          context: {
            slug: edge.node.slug,
            id: edge.node.id
          }
        });
      });
    })
    .catch(error => {
      console.log("Error retrieving contentful data", error);
    });
};

ContentfulはGraphQLをサポートしていますが、

allContentful{コンテンツモデル名}に対してクエリを書くと、特定のコンテンツモデルの記事を全て取得してくれます。

BlogArticleの部分の名前はContentfulで作成したCONTENT TYPE IDによって変わりますが、GraphQL投げる際は最初も大文字にする点にご注意ください。

2-8. index.jsの編集

GatsbyアプリのTOPページで記事の一覧を表示するようにします。

src/pages/index.js

import React from "react"
import { Link } from "gatsby"
import Layout from "../components/layout"
import Img from "gatsby-image";
import SEO from "../components/seo"

const IndexPage = ({ data, location }) => {
  const blogArticles = data.allContentfulBlogArticle.edges;

  return (
  <Layout>
    <SEO title="Home" />
    <div className="bg-gray">
      <div className="ast-container ast-container-top">
        <h2>Blog</h2>
        {blogArticles && blogArticles.map(({ node: post }) => {
          return (
            <Link to={`/post/${post.slug}/`} className="post-basic-item flex-column">
              <div className="flex-column-3">
                {post.thumbnail && //もしサムネイル画像をもっていれば
                  <Img
                    fluid={post.thumbnail.fluid}
                    className="thumbnail"
                  />
                }
              </div>
              <div className="flex-column-9">
                <h3>{post.title}</h3>
                <div className="post-basic-postedat">Posted on {post.createdAt}</div>
              </div>
            </Link>
          )
        })}
      </div>
    </div>
  </Layout>
  );
};
export default IndexPage

export const query = graphql`
  query QueryTop {
    allContentfulBlogArticle: allContentfulBlogArticle( sort: {fields: createdAt, order: DESC}) {
      edges {
        node {
          title
          slug
          thumbnail {
            fluid(maxWidth : 600) {
              ...GatsbyContentfulFluid_withWebp
            }
          }
          createdAt(formatString: "YYYY-MM-DD")
        }
      }
    }
  }
`;

2-9. 記事詳細ページのテンプレート作成

srcの直下にtemplatesというフォルダを作り、その中でpost.jsを作成します。

src/templates/post.js

import React from "react";
import { Link, graphql } from "gatsby";
import Layout from "../components/layout";
import SEO from "../components/seo";
import { BLOCKS } from "@contentful/rich-text-types"
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import Img from "gatsby-image";

const options = {
    renderText: text => {
        return text.split('\n').reduce((children, textSegment, index) => {
            return [...children, index > 0 && <br key={index} />, textSegment];
        }, []);
    },
    renderNode: {
        [BLOCKS.EMBEDDED_ASSET]: (node) => (
            <img
                src={node.data.target.fields.file["en-US"].url}
            />
        )
    },
};
 
const blogArticle = ({ data, location }) => {
    const { title, content, createdAt, thumbnail } = data.contentfulBlogArticle;
    return (
        <Layout>
            <SEO title="post" />
            <div className="ast-container ast-container-post">
                <div className="main">
                    <div className="post">
                        <h1>{title}</h1>
                        <p className="post__date">Posted on {createdAt}</p>
                        <div>
                            {thumbnail && //もしサムネイル画像をもっていれば
                                <Img
                                    fluid={thumbnail.fluid}
                                    className="thumbnail"
                                />
                            }
                        </div>
                        <div className="body-text">
                            {documentToReactComponents(content.json, options)}
                        </div>
                        <p className="post__date">Posted on {createdAt}</p>
                    </div>
                </div>
            </div>
        </Layout>
    );
};
export default blogArticle;
export const pageQuery = graphql`
    query( $slug: String) {
        contentfulBlogArticle(slug: { eq: $slug }) {
            title
            content{
                json
            }
            thumbnail {
                fluid(maxWidth : 600) {
                    ...GatsbyContentfulFluid_withWebp
                }
            }
            createdAt(formatString: "YYYY-MM-DD")
        }
    }
`;

2-10. アプリの確認

これでローカルブログアプリができたはずなので、再度$ gatsby developして http://localhost:8000/ にアクセスします。

Gatsby.js top page

Styleは全く当てていませんが、Contentfulと連結した簡単なブログがローカルに立ち上がりました!

3. S3へのアップロード

先ほどローカルに立ち上げたGatsbyアプリを、AWSのS3バケットでホストしたいと思います。これについては既に記事をアップしているので下記をご参照ください。

Gatsby.jsで作ったサイトをさくっとS3でホスティングする

上記が完了すると、S3のパブリックなURLで記事を確認できるようになります。

Gatsby.js top page

4. CircleCIのセットアップ

CircleCIを使って、上記のビルド&デプロイ処理を自動化したいと思います。

※事前にGithubにリポジトリを作り、先ほどのローカルアプリをmasterブランチにpushしておいてください。

4-1. CircleCIに登録

circleci.com

登録し、Githubとの連携を済ませます。

4-2. プロジェクトのセットアップ

左サイドバーからProject一覧に進み、「Set Up Project」をクリック

CircleCI Screenshot

「Add config」をクリック

CircleCI Screenshot

こんな画面に進みます。一回Statusが「Failed」になりますが気にしなくてOKです。

CircleCI Screenshot

4-3. CircleCI用の環境変数セットアップ

CircleCIにビルド&デプロイを行ってもらうため、必要な環境変数を設定します。

「Project Settings」をクリック

CircleCI Screenshot

環境変数を追加します。

CircleCI Screenshot

下記4つの変数を追加してください。

  1. AWS_ACCESS_KEY_ID
  2. AWS_SECRET_ACCESS_KEY
  3. CONTENTFUL_ACCESS_TOKEN
  4. CONTENTFUL_SPACE_ID

こんな感じになればOK。

CircleCI Screenshot

最初の2つは「3. S3へのアップロード」で利用したAWSアカウントと同じものを利用します。3と4は先ほど.envで設定した値を同様のものを入力します。

4-4. CircleCI用のconfigファイル作成

ローカルのアプリのルートディレクトリに .circleci/config.yml を作成します。

.circleci/config.yml

version: 2.1
jobs:
  setup:
    docker:
      - image: circleci/node:10.15.3
    working_directory: ~/application
    steps:
      - checkout
      - run:
          name: Update npm
          command: "sudo npm install -g npm@latest"
      - restore_cache:
          key: dependency-cache-{{ checksum "package.json" }}
      - run:
          name: Install npm wee
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package.json" }}
          paths:
            - node_modules
      - save_cache:
          key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
          paths:
            - ~/application
  build:
    docker:
      - image: circleci/node:10.15.3
    working_directory: ~/application
    steps:
      - restore_cache:
          key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
      - restore_cache:
          key: dependency-cache-{{ checksum "package.json" }}
      - run:
          name: Deploy
          command: |
            npm run build && npm run deploy
workflows:
  version: 2.1
  build:
    jobs:
      - setup
      - build:
          requires:
            - setup
          filters:
            branches:
              only:
                - master

4-5. デプロイ

再度リモートのmasterブランチにpushしてみます。

CircleCIではビルド&デプロイ状況がリアルタイムで確認できます。

setupとbuildが成功していればOKです。

CircleCI Screenshot

先ほど立ち上げたS3のURLにアクセスすると、更新されたサイトが確認できるかと思います。

これでGithub上のmasterブランチに変更があった場合に、自動でビルド&デプロイが行われるようになりました。

5. ContentfulとCircleCIの連携

最後に、コンテンツ管理を行うContentfulでの変更もトリガーにします。

5-1. ContentfulのAPIキーの変数をCircleCIに登録

.envの環境変数をCircleCIから使えるように、.circleci/config.ymlを修正して、ビルド処理の直前に変数を取得する処理を追加します。

.circleci/config.yml

...
- run:
    name: Deploy
    command: |
      echo CONTENTFUL_SPACE_ID=${CONTENTFUL_SPACE_ID} > ~/.env
      echo CONTENTFUL_ACCESS_TOKEN=${CONTENTFUL_ACCESS_TOKEN} >> ~/.env
      npm run build && npm run deploy
...

5-2.ContentfulでCircleCIプラグインをインストール

https://www.contentful.com/marketplace/webhook/circle-ci/ にアクセスしてインストールします。

Contentful CircleCI

基本的にはContentfulのインストラクションに沿ってインストールするだけです。

先ほどContentfulで作成したSpaceを選択するようご注意ください。

Contentful CircleCI

Github情報を入力します。

※Personal API TokenはCircleCIのダッシュボードから取得します

Contentful CircleCI

ContentfulのWebhook設定ページにリダイレクトするので、設定を若干修正してみます。

先ほどContentfulで作成した「Blog Article」のみを対象にしたのと、記事が保存された際もビルド処理が走るようにします。

Contentful Webhook

リポジトリがpublicの場合、これで準備は完了です。

適当に記事を更新すると、CircleCIのビルドが走るチェックしてみてください。

先程と同様に「Success」になっていれば大丈夫です。

CircleCI

さいごに

今回の記事で継続的な開発環境の基本は抑えていると思いますが、次のステップとしては下記でしょうか。

  • CloudinaryをContentfulに連携し、画像管理&配信を外部化する
  • Cloudfrontを導入してより高速なサイト表示

ビルド&デプロイ&ホスティングを行うNetlifyなどのサービスもありますが、月ごとのビルド処理が一定時間を超えると課金されてしまいますし、Basic認証の利用が難しかったり日本にエッジポイントがなかったりするので、自分で開発環境を構築しておいたほうが後々柔軟に対応できると思います。

CircleCI、Contentfulのご利用をお考えの方がいましたら、ぜひお気軽に弊社にお問い合わせください。

参考資料

https://boredhacking.com/deploy-gatsby-s3-circleci/