前回作ったブログの記事をCMSで更新出来るようにContentful
を導入してみました。
完成(demo)
https://next-typescript-blog-with-search.vercel.app/repo
https://github.com/chanfuku/next-contentful-typescript-blogContentfulに登録しSpaceを作成する
Contentful
に登録完了後、spaceを作成します。
私はSampleという名前でSpaceを作成しました。※freeプランだと一つしかspaceが作れないようです。
Content Modelを作成する
quick startの中でテンプレート選択のようなメニューが出てくるので、「Blog」を選択します。すると、下記の様なContent Modelが自動的に作成されます。
Blog Postはこんな感じ↓
記事を作成してみる
Content > Add Blog Postで記事を作成してみます。既に3つの記事が自動的に作成されてました。「Test Title」というタイトルの記事を追加で作成しました。
Tagを追加する
Settings > Tags > Create TagsでTagを追加します。
tag1,tag2,tag3を追加しました。tag2, tag3のみpublicにしたので、postに設定できます。
publicは公開用のtagなのでdelivery apiで使う、private は管理用なのでmanagement apiで使う、という仕様みたいです。
Post(記事)にTagを設定する
Content > Post > Tags と進むとtag設定画面が表示されます。
Clientの型を自動生成する
TypescriptでAPIのレスポンスのを型定義をするのが若干大変なので contentful-typescript-codegen を使って、型を自動生成したいと思います。 他にも必要なライブラリがいくつかあるのでまとめてinstallします。
npm install contentful contentful-management dotenv contentful-typescript-codegen
getContentfulEnvironment.jsを作成する
// ./getContentfulEnvironment.js
require('dotenv').config()
const contentfulManagement = require("contentful-management")
module.exports = function() {
const contentfulClient = contentfulManagement.createClient({
accessToken: process.env.CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN,
})
return contentfulClient
.getSpace(process.env.CONTENTFUL_SPACE_ID)
.then(space => space.getEnvironment(process.env.CONTENTFUL_ENVIRONMENT))
}
.envを作成する
.env.localに記載した環境変数が上記のgetContentfulEnvironment.jsでは読み込めなかったので, ファイル名を.envに変更します。
mv .env.local .env
.envの中身↓ …の部分はContentfulのsetting > API Keysに記載されている値を貼り付けてください
CONTENTFUL_SPACE_ID=...
CONTENTFUL_ACCESS_TOKEN=...
CONTENTFUL_PREVIEW_ACCESS_TOKEN=...
CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN=...
CONTENTFUL_ENVIRONMENT=master
package.jsonにコード自動生成コマンド追加
"scripts": {
"contentful-typescript-codegen": "contentful-typescript-codegen --output @types/generated/contentful.d.ts"
これで、コード自動生成の準備は整ったので、下記のコマンドを実行するとclientコードが自動生成されます
npm run contentful-typescript-codegen
utils/client.tsを作成する
// utils/client.ts
import { createClient } from 'contentful'
const config = {
space: process.env.CONTENTFUL_SPACE_ID || '',
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN || '',
environment: 'master'
}
export const client = createClient(config)
lib/api.tsを作成する
記事(Post)を取得するgetAllPosts
とタグ(Tag)を取得するgetAllTags
を定義しました。
// lib/api.ts
import { client } from '../utils/client'
import { IBlogPostFields } from '../@types/generated/contentful'
import { Entry, Tag } from 'contentful'
export async function getAllPosts(params: {}): Promise<Entry<IBlogPostFields>[]> {
const { items } = await client.getEntries<IBlogPostFields>(params)
return items
}
export async function getAllTags(): Promise<Tag[]> {
const { items } = await client.getTags()
return items
}
pages/index.tsのgetStaticProps修正
getStaticPropsで記事一覧とタグ一覧を取得するようにします。
// pages/index.ts
export const getStaticProps = async () => {
const [allPosts, allTags] = await Promise.all([
getAllPosts({ content_type: 'blogPost' }),
getAllTags(),
])
return {
props: { allPosts, allTags },
}
}
pages/posts/[slug].tsxのgetStaticPaths・getStaticProps修正
// pages/posts/[slug].tsx
export async function getStaticProps({ params }: Params) {
const allPosts = await getAllPosts({
content_type: 'blogPost',
'fields.slug': params.slug
})
const post = allPosts.length ? allPosts[0] : undefined
const content = post
? await markdownToHtml(post.fields.body || '')
: undefined
return {
props: {
post,
content,
},
}
}
export async function getStaticPaths() {
const allPosts = await getAllPosts({ content_type: 'blogPost' })
return {
paths: allPosts.map((post: Entry<IBlogPostFields>) => {
return {
params: {
slug: post.fields.slug,
},
}
}),
fallback: false,
}
}
pages/index.tsxに検索機能を追加
頻繁に記事が更新される訳でもないのでAPIで取得するのではなく、取得済のPostsをシンプルにjavascriptでfilterするようにしました。
// pages/index.tsx
const search = ({ keyword, selectedTags }: SearchType) => {
if (!keyword && !selectedTags.length) {
setPosts(allPosts)
return
}
const filtered = allPosts.filter((post: Entry<IBlogPostFields>) => {
const keywordFound = keyword.length && (post.fields.title.includes(keyword) || post.fields.slug.includes(keyword) || post.fields.body.includes(keyword))
if (keywordFound) return true
return selectedTags.some((tag: string) => post.metadata.tags.map(v => v.sys.id).includes(tag))
})
setPosts(filtered)
}
今回はConetent Delivery APIしか使いませんでしたが、他にもContent Management API, Content Preview API, Images API, GraphQL Content API, User Management API等あるようです。