最近Contentfulを触っていて、これがまた楽しい。

Contentfulはざっくりいうと管理画面だけ提供してくれるCMS。APIでコンテンツを取得するので、フロントエンドは好きな言語で構築できる。

自分はいまのところベーシックな構成であるContentful+Gatsby(React.jsベースの静的サイトジェネレーター)で進めていて、さらにNetlifyを組み合わせて完全無料で記事を公開するところまでできる。

ネット上で日本語記事も充実しているけど、簡単なGatsbyアプリをローカルで立ち上げるまでに、自分なりにつまづいたところなどを備忘録的にまとめておく。

前提

  • Macのターミナルが使える
  • node.jsがインストール済み
  • Contentfulにも登録済み

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

ターミナルで下記を実行。

$ npm install -g gatsby-cli

2. テストサイトを作成

$ gatsby new hello-world

これでカレントディレクトリ直下にhello-worldというフォルダが作られる。ここの名前はなんでもいい。

newは新しいGatsbyのアプリを作成するコマンドで、アプリのベースとなるコードをGithubリポジトリから引っ張ってくる引数を設定できる。

$ gatsby new [SITE_DIRECTORY_NAME] [URL_OF_STARTER_GITHUB_REPO]

二個目の引数[URL_OF_STARTER_GITHUB_REPO]に何も指定しない場合、default starter(デフォルトスターター)を元にサイトを構築してくれる。デフォルトスターターの中身は https://www.gatsbyjs.org/starters/?v=2 から確認が可能。

3. アプリのディレクトリに移動する

$ cd hello-world

4. まずは試しに起動してみる

$ gatsby develop

これでDEVサーバーが起動されるはず。

http://localhost:8000/ にアクセスする。

Screenshot-2020-05-28-at-00.49.58

成功! これでまずGatsbyのベースは作ることができた。

いったんCtrl+Cで終了する。

5. Contentfulとの連携準備

ここからContentfulとの連携を行う。

5-1. Contentful接続用のプラグインのInstall

まずはContentful連携を行うためのGatsbyプラグインをインストール。

$ npm install --save gatsby-source-contentful

5-2. config設定

GatsbyのConfigに接続情報を記載する。ルートディレクトリにあるgatsby-config.jsを使う。

gatsby-config.js

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

各Contentfulアカウントごとに発行されるSpace IDやAccess Tokenを書いておく必要があるけど、これ以降で書くように環境変数として別で管理するので、上記はそのままでOK。

5-3. 環境変数の設定

環境変数を管理できるプラグインdotenvをインストールする。

$ npm install dotenv --save

ルートディレクトリに .envファイルを作成して下記のように変数を書く。

.env

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

またgatsby-config.jsに戻って、一番上の行にrequire('dotenv').config();を書き足す。

require('dotenv').config();

module.exports = { ...

6. Contentful側の準備

記事タイトル、サムネイル画像、本文を投稿するだけの簡単なブログをContentfulで作る。

6-1. コンテンツモデルの作成

名前はなんでもいいけど、APIで取得する際は基本的にコンテンツモデルごとのクエリを書くので、コンテンツモデルを特定するAPI IDはblogArticleとしておく。

Screenshot-2020-05-28-at-23.26.19

6-2. フィールドの作成

次の4つのフィールドを作成する

  • Title(Field ID: title): 記事タイトル。Entry Titleとして定義が必要
  • Slug(Field ID: slug): 記事URLのSlug:
  • Thumbnail(Field ID: thumbnail): サムネイル画像
  • Content(Field ID: content): 本文

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

Screenshot-2020-05-29-at-00.23.09

6-3. テスト記事の作成

Contentfulの記事作成ページに移動して、テスト記事を作る。

Screenshot-2020-05-29-at-00.39.02

画像(メディア)にも公開ステータスがあり、忘れずにpublishしておくこと。

記事をPublishして準備完了。

次からはGatsby側でコンテンツを取得して表示する処理を書く

7. gatsby-node.jsの修正

ルートにある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);
    });
};

allContentfulBlogArticleは特定のコンテンツモデルのEntryを全て取得してくれる。

BlogArticleの部分の名前はContentfulで作成したCONTENT TYPE IDによって変わる。

Screenshot-2020-05-29-at-00.25.08

8. index.jsの編集

フロント側の準備。まずはTOPページを作る。

src/pages/index.js

import React from "react";
import { Link } from "gatsby";
import Layout from "../components/layout";
import Image from "../components/image";
import SEO from "../components/seo";
//import "./index.css";
const IndexPage = () => (
  <Layout>
    <SEO title="Home" keywords={[`gatsby`, `application`, `react`]} />
    <div className="home">
      <h1>Hello There</h1>
      <p>Welcome my awesome blog</p>
      <div>
        <div
          style={{
            maxWidth: `300px`,
            margin: "0 auto 1.45rem"
          }}
        >
          <Image />
        </div>
      </div>
      <Link to="/posts/">View all posts</Link>
    </div>
  </Layout>
);

export default IndexPage;

8. ブログ一覧ページの作成

次に、ブログ一覧ページのために下記のファイルを作成する。

src/pages/posts.js

import React from "react";
import { Link, graphql } from "gatsby";
import Layout from "../components/layout";
import SEO from "../components/seo";
const BlogArticles = ({ data }) => {
  const blogPosts = data.allContentfulBlogArticle.edges;
  return (
    <Layout>
      <SEO title="Blog Article" />
      <h1>{"Here's a list of all posts!"}</h1>
      <div className="posts">
        {blogPosts.map(({ node: post }) => (
          <div key={post.id}>
            <Link to={`/post/${post.slug}`}>{post.title}</Link>
          </div>
        ))}
        <span className="mgBtm__24" />
        <Link to="/">Go back to the homepage</Link>
      </div>
    </Layout>
  );
};
export default BlogArticles;
export const query = graphql`
  query BlogArticlePageQuery {
    allContentfulBlogArticle(filter: {node_locale: {eq: "ja-JP"}}) {
      edges {
        node {
          id
          title
          slug
          content {
            content
          }
        }
      }
    }
  }
`;

注意点(個人的ハマりポイント)

僕の場合はContentful側でLocaleを日本語に設定している関係で、記事を取得する際に(filter: {node_locale: {eq: "ja-JP"}})というフィルター処理を挟んでいる。
この処理がなくても全件取得はできるんだけど、問題になるのが複数LocaleがContentful側で設定されている時。allContentfulXXXXのクエリではLocaleをまたいで記事を全件取得してくれてしまうので、上記の記述をしておかないとEntry(記事)が重複して取得されてしまうことがある。もちろん人によって日本語以外の言語に書き換えてOK。

僕はこれで2時間くらいハマり、最終的に下記の公式の記述に辿りついて助かった。

https://www.gatsbyjs.org/packages/gatsby-source-contentful/

9. 記事詳細ページ

各記事の詳細ページを作成する。

src直下にtemplatesというフォルダを作り、post.jsというファイルを作成して下記を記述する。

import React from "react";
import { Link, graphql } from "gatsby";
import Layout from "../components/layout";
import SEO from "../components/seo";
const BlogArticle = ({ data }) => {
  const { title, content, thumbnail } = data.contentfulBlogArticle;
  return (
<Layout>
  <SEO title={title} />
  <div className="post">
    <h1>{title}</h1>
    <img alt={title} src={thumbnail.file.url} />
    <p className="body-text">{content.content}</p>
    <Link to="/posts">View more posts</Link>
    <Link to="/">Back to Home</Link>
  </div>
</Layout>
  );
};
export default BlogArticle;
export const pageQuery = graphql`
  query($slug: String!) {
contentfulBlogArticle(slug: { eq: $slug }) {
  id
  title
  slug
  content {
    content
  }
  thumbnail {
    file {
      url
    }
  }
}
  }
`;

10. アプリを立ち上げてみる

これでTOPページ、記事一覧ページ、記事詳細ページからなるブログアプリが整ったはずなので、再度アプリを立ち上げてみる。

$ gatsby develop

http://localhost:8000/ にアクセスし、TOPから記事一覧、記事詳細ページまで閲覧できれば完了。

scr3

ここまでつまづきながらも半日程度で構築できた。

数ページ程度のポートフォリオとかには最適な構成ではなかろうか。

おまけ

GraphQLのクエリをテストしたい場合、立ち上げたアプリの下記URLからデータの確認、クエリのテストができる。どんなフィルターが使えるか、どんな値が返ってくるかブラウザ上でチェックできるので、開発には必須になってくる。

http://localhost:8000/___graphQL

Contentful+Gatsby構成は立ち上げも簡単だし、公式ドキュメントも充実しているし、表示も爆速だけど、僕のJavascriptフレームワーク知識不足からか、例えば人気記事の表示など現時点でのベストプラクティスがわかってなくて、カスタマイズは色々と勉強していかないといけないなぁ。

あとWordpressのように管理画面やサーバー側のセキュリティを気にしなくていいというのは気が楽。記事のデータ構造に直接手を入れられない分工夫が必要だけど。

あとは本格運用しようとすると課金タイミングが割と早く来るというのはある。

それでもしばらくはいじり倒してみよう。