✒️

簡易ブログを作ってみた!パート1(with Next.js & contentful)

2021/04/16に公開

はじめに

Next.js 第二弾として Headless CMS の contnetful と連携し、簡単なブログを作成する手順をまとめました。

前回同様、認識が甘い点など各所で見受けられると思います。そのような点がございましたら、ご教授いただけると幸いです🙇‍♂️


本記事の目標

contentful の利用で記事に必要なデータはAPIをフロント側 (Next.js) で呼び出すだけに留め、Next.js のみコーディングすることで簡易ブログを実装します。


contentful でコンテントモデルを定義 & コンテント(記事)を作成

手順

  1. ナブメニューで Content Model に移動
  2. 上の写真ではいくつかモデルを定義した後ですが、今回は article と名付け、右側の Add field ボタンで必要な項目を定義
  3. モデルの定義にはバリデーションが用意されているので、これらも必要に応じてチェックを入れていく(以下のように)

コンテント(記事)の作成は簡単で、モデルの定義に従っていくつか作成していきます。


Next.js セットアップ

ターミナルに移動し、以下のコマンドを叩きます。

​yarn create next-app プロジェクト名

TypeScriptでコードを書くために最低限のパッケージが必要となるため、以下のコマンドを叩きます。

​yarn add -D typescript @types/node @types/react

コードを書く

contentful で API Key を取得 & .env.local ファイルに貼り付け

先程の画面より、(ナブメニューの)Setting → API Key をクリックするとこちらの画面になると思います。

Add API ボタンをクリックした後、Space ID と Access Key(Content Delivery API) をコピーし、エディタに .env.local ファイルを作成し貼り付けます。

.env.local
CONTENTFUL_SPACE_ID=         
CONTENTFUL_ACCESS_KEY=        

contentful の API を取得 & ページコンポーネントに渡す

src/pages/index.tsx
import React from 'react'
import Head from 'next/head'
import { Container } from '../styles/pages/Home'
import { createClient } from 'contentful'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import Link from 'next/link'

export const getStaticProps = async () => {
  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_KEY
  })

  const response: EntryCollection<IFields> = await client.getEntries({
    content_type: 'article'
  })


  return {
    props: {
      article: response.items
    }
  }
}

type Props = InferGetStaticPropsType<typeof getStaticProps>

const Home: NextPage<Props> = ({ article }) => {
  console.log(article)

  return (
    <Container>
      <Head>
        <title>Homepage</title>
      </Head>
    </Container>
  )
}

export default Home

getStaticProps 関数内では
データベースと接続を図るように、 createClient という関数が contentfulには用意されているので、 .env.local ファイルから API Key を付与して、 client と定めます。そして client をドットつなぎで getEntries({ content_type: 'article' })とすることで contentful との接続を可能にします。
content_type は Content Model で確認できます。

コンソール上で確認すると、先程 contentful で作成したデータが取得できていることが確認できます。

取得したデータを Link タグを用いてベージ遷移します。ページ遷移でのパスには contentful で作成した slug (ここでは日付とタイトルを紐付けた文字列)を動的なルーティングに渡します。

src/pages/index.tsx
const Home: NextPage<Props> = ({ article }) => {
  return (
    <Container>
      <Head>
        <title>Homepage</title>
      </Head>

      <ul>
        {article.map(item => (
          <li key={item.sys.id}>
            <Link href={'/article/' + item.fields.slug}>
              <a>{item.fields.title}</a>
            </Link>
          </li>
        ))}
      </ul>
    </Container>
  )
}

export default Home

動的なルーティングを実装

例えば /article/に続くパスを slug として扱いたい場合、 Next.js では [slug] のようにしてページ名に角括弧(ブラケット)を使うことで動的なルーティング(ダイナミックルーティング)を作成できます。
https://nextjs-ja-translation-docs.vercel.app/docs/routing/dynamic-routes

pages/article/[slug].tsx
import React from 'react'
import { createClient, EntryCollection } from 'contentful'
import { GetStaticPaths, GetStaticProps } from 'next'
import { IResponse, IArticle } from '../../components/Interfaces/IArticle'

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID,
  accessToken: process.env.CONTENTFUL_ACCESS_KEY
})

export const getStaticPaths: GetStaticPaths = async () => {
  const res: EntryCollection<IResponse> = await client.getEntries({
    content_type: 'article'
  })

  const paths = res.items.map(item => {
    return {
      params: { slug: item.fields.slug }
    }
  })

  return {
    paths,
    fallback: false
  }
}

export const getStaticProps = async ({ params }) => {
  const { items } = await client.getEntries<IFields>({
    content_type: 'article',
    'fields.slug': params.slug
  })

  return {
    props: { article: items[0] }
  }
}

getStaticPaths という非同期関数を使って、動的ルーティングによりページを静的に生成することを可能にします。この関数の中では、slug としてとりうるビルド時にレンダリングする必要のあるパスのリストを生成します。

また getStaticProps では getStaticPaths から受け取った slug に基づいて contentful で作成したデータを取得します。

最後に getStaticProps から取得したデータをページコンポーネントに渡します。

import React from 'react'
import { createClient, EntryCollection } from 'contentful'
import { Container } from '../../styles/pages/Home'
import Layout from '../../components/Layout'
import { GetStaticPaths, GetStaticProps } from 'next'
import moment from 'moment'
import styled from 'styled-components'
import { IResponse, IArticle } from '../../components/Interfaces/IArticle'

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID,
  accessToken: process.env.CONTENTFUL_ACCESS_KEY
})

export const getStaticPaths: GetStaticPaths = async () => {
// 一つ前のコードブロック参照
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
// 一つ前のコードブロック参照
}

type Props = InferGetStaticPropsType<typeof getStaticProps>

const Article: NextPage<Props> = ({ article }) => {
  return (
    <Container>
      <Layout title={article.fields.title}></Layout>
      <div>
        <Flex>
          <h1>{article.fields.title}</h1>
          <h3>{moment(article.fields.date).utc().format('YYYY/MM/DD')}</h3>
        </Flex>
      </div>
    </Container>
  )
}

export default Article

const Flex = styled.div`
  display: flex;
  justify-content: space-between;
`

日付の取得には react-moment を使っています。
https://www.npmjs.com/package/react-moment

基本データはドットつなぎで画面に表示できるのですが、contentful のリッチテキストはそうはいきません。リッチテキストというのは、これです。

というのも、リッチテキストは (今回の例でいうと) article.fields.content にデータが格納されている訳ですが、コンソールで確認して見ると、

このように配列で格納されています。これらのデータを配列処理でひとつひとつ画面に表示することはできなくもなさそうですが、かなり手間がかかりそうです。またスタイルも崩れそうです。

そこで contentful にはリッチテキスト用の便利な関数が用意されています。
https://www.npmjs.com/package/@contentful/rich-text-react-renderer

pages/article/[slug].tsx
import React from 'react'
・・・・・・・・・・・
+import { documentToReactComponents } from '@contentful/rich-text-react-renderer'

​const Article: NextPage<Props> = ({ article }) => {
  return (
    <Container>
      <Layout title={article.fields.title}></Layout>
      <div>
        <Flex>
          <h1>{article.fields.title}</h1>
          <h3>{moment(article.fields.date).utc().format('YYYY/MM/DD')}</h3>
        </Flex>
+        <div>{documentToReactComponents(article.fields.content)}</div>
      </div>
    </Container>
  )
}

このようにページコンポーネントでは一行追記するだけで、リッチテキスト通り画面に表示することができます。

ただし、リッチテキストで設定していた画像が取得できていません。
画像を画面に表示するためには、もうひと手間必要です。

pages/article/[slug].tsx
import React from 'react'
・・・・・・・・・・・
import { documentToReactComponents } from '@contentful/rich-text-react-renderer'
+ import { BLOCKS } from '@contentful/rich-text-types'


​const Article: NextPage<Props> = ({ article }) => {
  return (
    <Container>
      <Layout title={article.fields.title}></Layout>
      <div>
        <Flex>
          <h1>{article.fields.title}</h1>
          <h3>{moment(article.fields.date).utc().format('YYYY/MM/DD')}</h3>
        </Flex>
        <div>
          {documentToReactComponents(article.fields.content, {
+           renderNode: {
+             // eslint-disable-next-line react/display-name
+             [BLOCKS.EMBEDDED_ASSET]: node => (
+               <img
+                 src={'https:' + node.data.target.fields.file.url}
+                 width={400}
+                 height={300}
+               />
              )
            }
          })}
        </div>
      </div>
    </Container>
  )
}

export default Article

documentToReactComponents の第2引数に追記することで画像を取得できます。日本語の情報があまりない?ので理解が浅いのですが、
とにかく、 renderNode 内、画像表示用のコンポーネント(EMBEDDED_ASSET)にアクセスし,先程、article.fields.content をコンソール上で確認した配列の中の一つであった画像データが取得できます。
コンソール上で node という引数を確認してみると、
↓上が article.fields.content(配列) 下が node

以上の変更により、contentful で作成したブログデータをすべて反映できました。

以上になります。ここまで読んでいただきありがとうございました!!

Discussion