執筆者: どあんりょ (旧:sasigume)
最終更新: 2/13/2022
microCMSで、記事タイトル・スラッグ・サムネイル・本文を取得し、NextjsでISR(Incremental Static Rendering)します。その際、エディタでPropsを全部自動補完できるようにします。
microCMS側の仕様変更や画面デザインの変更があるかもしれないのでご注意ください。
npx create-next-app@latest --ts
Googleのサジェストでcreate-nuxt-appばっかり出てきて困りますね。Next使いはこれぐらい検索しないので出ないんでしょう(偏見)。
{
"name": "nextjs-microcms-practice",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "12.0.10",
"react": "17.0.2",
"react-dom": "17.0.2"
},
"devDependencies": {
"@types/node": "17.0.17",
"@types/react": "17.0.39",
"eslint": "8.9.0",
"eslint-config-next": "12.0.10",
"typescript": "4.5.5"
}
}
▲昔と違ってeslint-config-next
が入っています。今回はこれ以上lintはいじりません。
/api/v1/article
というルートのエンドポイントを作りました。
ブログ的なサイトを作るので、リスト形式にします。
以下のようにフィールドを設定してください。
title
/ タイトル / テキストフィールド / 必須slug
/ スラッグ / テキストフィールド / 必須 / ユニーク / 後述の正規表現を適用thumbnail
/ サムネイル / 画像 / 任意body
/ 本文 / リッチエディタ / 必須コンテンツIDをスラッグとして使うことも可能ですが、非エンジニアに触らせるのは危険すぎるので、別でユニークなスラッグフィールドを作りましょう。
ユニークの設定は「詳細設定」のところにあります。
「特定のパターンのみ入力を許可する」をONにしてください。パターンは^[a-z0-9]+(?:-[a-z0-9]+)*$
でフラッグはなしです。
article-1
はOKですが、-artielce-1
みたいな先頭のハイフンはNGで、article--1
みたいな2回スラッグもNG、article-1-
みたいな末尾のスラッグもNGになります。
お前こんなのどこで見つけたんだよって感じですが、
https://ihateregex.io/expr/url-slug/
i Hate Regex(正規表現なんて大っ嫌い)という神サイトがあってですね、ここに詳細な解説と一緒に例が載っています。
適当な記事を書いて公開してください。プレースホルダに『ポラーノの広場』を使うとなんかデザイナー感出る。フォントに詳しそう。
npm i microcms-js-sdk
ライブラリを追加。
APIキーをコピーします。
echo MICROCMS_SERVICE_DOMAIN=あなたのmicroCMSサービス名 >> .env.local
echo MICROCMS_API_KEY=$(pbpaste) >> .env.local
環境変数を書き出し。Linuxだとpbpasteじゃなくてxclip -selection clipboard -o
を使えばいいそうです(ソース)
あと、クライアントサイドで使わないのでconfigで環境変数を登録する必要はありません。
mkdir lib && touch $_/microcms.ts
microcms.ts
を作ります。
import { createClient } from 'microcms-js-sdk'
export const client = createClient({
serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN!,
apiKey: process.env.MICROCMS_API_KEY!,
})
これでデータ取得の用意が整いました。
で、ここでReact 18からSSRでもSuspenseが...とか語ってたらキリがないので、普通にSSRして、静的にページを書き出せる形で作ります。
(React世界にはいろいろなデータフェッチ方法があるよ!調べてみよう!)
microcms-typescript
っていうライブラリが新しいAPIに対応していなかったので、公式のSDKを使います。
API_NAME=article
mkdir types && touch $_/article.ts
API_NAME
はあとでも使います。
import type { MicroCMSDate, MicroCMSImage, MicroCMSListResponse } from 'microcms-js-sdk'
export type ArticleType = {
id: string
title: string
slug: string
thumbnail?: MicroCMSImage
body: string
} & MicroCMSDate
export type ArticleListType = MicroCMSListResponse<Article>
MicroCMSListResponse
でtotalCount
などを付けます。それ以外の部分を自動で書いてくれるライブラリを見つけられませんでした。(aspidaでもここは手動)
ORGANISMS_DIR=./components/organisms
mkdir -p $ORGANISMS_DIR && touch "_/$(echo "$API_NAME" | sed 's/.*/\u&/').tsx"
Atomic Designガン無視で突然「Organisms(生体)」が登場しましたね。宇宙の 法則が 乱れる!
あと、イキりすぎて全部コマンドで済ませようとしていますね、何をしているかというとcomponents/organisms/(API名の先頭だけ大文字).tsx
ってファイルを作っているだけです。大文字にする方法は色々あるそうなんですが、bashのバージョン云々が面倒なので置換してしまいました。
import { ArticleType } from '../../types/article'
export default function Article(props: ArticleType) {
return (
<article>
<h1>{props.title}</h1>
{props.thumbnail && <img src={props.thumbnail.url} alt={`${props.title}のサムネイル`} />}
<div dangerouslySetInnerHTML={{ __html: props.body }}></div>
</article>
)
}
記事コンポーネントです。Atomic Designガン無視で生体の中身を書いてしまいましたが、説明のためなので見逃してください。
touch "$ORGANISMS_DIR/$(echo "$API_NAME" | sed 's/.*/\u&/')List.tsx"
ArticleList.tsx
を作ります。
import { ArticleListType } from '../../types/article'
export default function ArticleList(props: ArticleListType) {
return (
<div>
<ul>
{props.contents?.map(
(p) =>
p && (
<li key={p?.id}>
<a href={`/articles/${p.slug}`}>{p.title}</a>
</li>
)
)}
</ul>
</div>
)
}
最低限の記事リストです。
それではNext.jsの真骨頂、SSRにおける型の推測をお見せしましょう。
import type { InferGetStaticPropsType, NextPage } from 'next'
import Head from 'next/head'
import ArticleList from '../components/organisms/ArticleList'
import { client } from '../lib/microcms'
import { ArticleType } from '../types/article'
export const getStaticProps = async () => {
const articlesData = await client.getList<ArticleType>({
endpoint: 'article',
queries: {
limit: 10,
orders: 'publishedAt',
},
})
return {
props: { articlesData: articlesData ?? null },
revalidate: 10,
}
}
type Props = InferGetStaticPropsType<typeof getStaticProps>
const Home: NextPage<Props> = (props) => {
return (
<div>
<Head>
<title>記事は{props.articlesData.totalCount}あります</title>
</Head>
<main>{props.articlesData ? <ArticleList {...props.articlesData} /> : <div>記事を取得できませんでした。</div>}</main>
</div>
)
}
export default Home
InferGetStaticPropsType<typeof getStaticProps>
で、getStaticProps
の返り値の方をReactのchildren
と混ぜます。
するとHome
のprops
にprops.articlesData
が追加され、サーバーサイドの型をわざわざコンポーネント用に再度書く必要がなくなります。
こんな感じにタイトルに反映されます。
また、revalidate: 10
でISRが有効になり、適切なデプロイ環境なら10秒ごとにデータのキャッシュが切れます(10秒ごとにサーバーが動くわけではない。ここ重要)。
mkdir -p pages/${API_NAME}s && touch $_/\[slug\].tsx
[slug].tsx
はparams.slug
を変数として取ります。
import type { GetStaticPaths, GetStaticPropsContext, InferGetStaticPropsType, NextPage } from 'next'
import Head from 'next/head'
import Article from '../../components/organisms/Article'
import { client } from '../../lib/microcms'
import { ArticleType } from '../../types/article'
export const getStaticPaths: GetStaticPaths = async () => {
const allArticles = await client.getList<ArticleType>({
endpoint: 'article',
queries: {
limit: 10,
fields: ['slug'],
},
})
const paths = allArticles.contents.map((c) => {
return {
params: {
slug: c.slug,
},
}
})
return { paths, fallback: true }
}
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
const articlesMatched = await client.getList<ArticleType>({
endpoint: 'article',
queries: {
limit: 1,
filters: 'slug[equals]' + params?.slug,
},
})
if (articlesMatched.contents[0]) {
return {
props: { articleData: articlesMatched.contents[0] ?? null },
revalidate: 10,
}
}
return {
props: {
articleData: null,
},
notFound: true,
revalidate: 10,
}
}
type Props = InferGetStaticPropsType<typeof getStaticProps>
const ArticlePage: NextPage<Props> = (props) => {
if (props.articleData) {
const { title } = props.articleData
return (
<div>
<Head>
<title>{title}</title>
</Head>
<main>
<Article {...props.articleData} />
</main>
</div>
)
}
return <div>記事を取得できませんでした。</div>
}
export default ArticlePage
長いので分けて説明。
export const getStaticPaths: GetStaticPaths = async () => {
const allArticles = await client.getList<ArticleType>({
endpoint: 'article',
queries: {
limit: 10,
fields: ['slug'],
},
})
const paths = allArticles.contents.map((c) => {
return {
params: {
slug: c.slug,
},
}
})
return { paths, fallback: true }
}
Next.jsのgetStaticPropsで、動的にURLを生成するための関数です。client.getList<ArticleType>
で一覧からslugだけを取り出し、それらを[{params: slug: "スラッグ"}]
といった配列に収めてpaths
に返します。fallback: true
は、初回生成時にないページに訪れても、データを取得する設定です。正直ブログだと必要ないのですが、公開した記事を即見れるようにしたい場合に便利です。
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
const articlesMatched = await client.getList<ArticleType>({
endpoint: 'article',
queries: {
limit: 1,
filters: 'slug[equals]' + params?.slug,
},
})
if (articlesMatched.contents[0]) {
return {
props: { articleData: articlesMatched.contents[0] ?? null },
revalidate: 10,
}
}
return {
props: {
articleData: null,
},
notFound: true,
revalidate: 10,
}
}
microCMSには2022年2月13日現在ユニークなフィールドからコンテンツ1つを返すAPIが用意されていません。そのため、filters: 'slug[equals]' + params?.slug
で絞り込んで長さ0~1の配列をもらい、記事の有無を見ています。Strapiとか使えば自前でAPI作れるから楽しいんですけど、microCMSでは2022年2月13日現在こうするしかありません。
あと、params.slugの型は本来string | string[] | undefined
ですから、文字列じゃない可能性を考慮するべきですが、文字列とプラスして文字列にしてしまえばMicroCMSQueries.filters?: string | undefined
とたまたま合致し、型チェックが通ります。
しかもURL構造上配列になりえない(クエリパラメータじゃないのでカンマ書いても配列にならない)ので、これで動作に支障はきたしません。notFound: true
を返せば404になります。もちろん404ページはカスタマイズ可能です。
type Props = InferGetStaticPropsType<typeof getStaticProps>
const ArticlePage: NextPage<Props> = (props) => {
if (props.articleData) {
const { title } = props.articleData
return (
<div>
<Head>
<title>{title}</title>
</Head>
<main>
<Article {...props.articleData} />
</main>
</div>
)
}
return <div>記事を取得できませんでした。</div>
}
export default ArticlePage
title
だけはページで使いたいので取り出します。ページ以外でheadをいじるのは人間のやることではない。
Do not use <img>. Use Image from 'next/image' instead. See: https://nextjs.org/docs/messages/no-img-element
next/core-web-vitals
っていうeslintプラグインが追加されていて、普通のimgタグだとwarnレベルの警告が出ます。このままでもビルドはできますが、練習がてらNext/Imageを使ってみましょう。
images: {
domains: ['images.microcms-assets.io'],
},
以上の設定を加えてください。microCMS以外の画像を使う場合は、適宜追加してください。
mkdir ./components/atoms && touch $_/MicroCMSImage.tsx
やっと原子を作ります。順番がおかしい。
import type { MicroCMSImage } from 'microcms-js-sdk'
import Image from 'next/image'
export default function MicroCMSImage(props: { image: MicroCMSImage; alt: string }) {
const { width, height, url } = props.image
return <Image width={width} height={height} src={url} alt={props.alt} />
}
next/image用にコンポーネントを分けるのは、サイズの取得が面倒だからです。
import { ArticleType } from '../../types/article'
import MicroCMSImage from '../atoms/MicroCMSImage'
export default function Article(props: ArticleType) {
return (
<article>
<h1>{props.title}</h1>
{props.thumbnail && <MicroCMSImage image={props.thumbnail} alt={`${props.title}のサムネイル`} />}
<div dangerouslySetInnerHTML={{ __html: props.body }}></div>
</article>
)
}
これで画像部分を最適化できました。
HTMLがすごいことになってたら成功です。
この人が書いた記事