簡易ブログを作ってみた!パート1(with Next.js & contentful)
はじめに
Next.js 第二弾として Headless CMS の contnetful
と連携し、簡単なブログを作成する手順をまとめました。
前回同様、認識が甘い点など各所で見受けられると思います。そのような点がございましたら、ご教授いただけると幸いです🙇♂️
本記事の目標
contentful
の利用で記事に必要なデータはAPIをフロント側 (Next.js) で呼び出すだけに留め、Next.js のみコーディングすることで簡易ブログを実装します。
contentful でコンテントモデルを定義 & コンテント(記事)を作成
手順
- ナブメニューで Content Model に移動
- 上の写真ではいくつかモデルを定義した後ですが、今回は article と名付け、右側の Add field ボタンで必要な項目を定義
- モデルの定義にはバリデーションが用意されているので、これらも必要に応じてチェックを入れていく(以下のように)
コンテント(記事)の作成は簡単で、モデルの定義に従っていくつか作成していきます。
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
ファイルを作成し貼り付けます。
CONTENTFUL_SPACE_ID=
CONTENTFUL_ACCESS_KEY=
contentful の API を取得 & ページコンポーネントに渡す
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 (ここでは日付とタイトルを紐付けた文字列)を動的なルーティングに渡します。
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] のようにしてページ名に角括弧(ブラケット)を使うことで動的なルーティング(ダイナミックルーティング)を作成できます。
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
を使っています。
基本データはドットつなぎで画面に表示できるのですが、contentful
のリッチテキストはそうはいきません。リッチテキストというのは、これです。
というのも、リッチテキストは (今回の例でいうと) article.fields.content にデータが格納されている訳ですが、コンソールで確認して見ると、
このように配列で格納されています。これらのデータを配列処理でひとつひとつ画面に表示することはできなくもなさそうですが、かなり手間がかかりそうです。またスタイルも崩れそうです。
そこで contentful
にはリッチテキスト用の便利な関数が用意されています。
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>
)
}
このようにページコンポーネントでは一行追記するだけで、リッチテキスト通り画面に表示することができます。
ただし、リッチテキストで設定していた画像が取得できていません。
画像を画面に表示するためには、もうひと手間必要です。
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
実際に自分でも手を動かしながらやってみました!
スムーズに最後まで実装できました。
読みやすくて、実装コードも丁寧に書かれていたので、とても参考になりました。
ありがとうございます!!
追記: 以前、自分で調べながらやったときは、、解説されていたrich-text-react-rendererの画像の部分で詰まってしまいました。。確かに日本語の情報少なかったかなと思いましたね。
嬉しいコメントありがとうございます!
自分ごとですが、最近は忙しくて記事投稿できていないのですが、、
落ち着いたら、情報発信頑張っていきます!!