はじめに
最近JAMstackで開発している人をたくさん見るようになり、Headless CMSがちらほらでてきたり、フレームワークがより開発しやすくなるように整備を始めたりしていて、大規模以外の案件等ではJAMstackを選択することが増えてくるのかな、と思っています。
Next.js も9.3からSSG用にgetStaticProps
とgetStaticPaths
が導入されたり、プレビュー機能が導入されたりと、よりJAMstackで開発しやすくなるようになってきているようです。
今回だいたい最小構成を作ったので解説です。
preview機能だけ書こうかと思いましたが、getStaticProps
等を使っているリファレンスがあまりなかったので、動的ルーティングからの手順を書いています。
TypeScript覚えたてなので、型の指定の仕方が間違っていたりしたらすみません。
ーーーー05/04追記ーーー
Twitterで、SSGでもapi routeが使えるとのご指摘をいただきまして内容を多少修正しました。
柴田さん、ありがとうございます。
対象読者
- microCMSでアカウントを作ってちょっと触ったことがある
- Next.jsを使ったことがある
今回TypeScriptで開発しましたが、使ってない人は適宜型などを削除して読んでいただければと思います。
環境
- Windows 10 pro
- node.js 12.14.1
リポジトリ
Githubリポジトリはこちらです。
なお、こちらのリポジトリはqiita用に環境変数を削除して公開しているものなので、
環境変数を正しくいれるとうごきます。(microCMSの情報)
全体の流れ
- microCMSでAPIを作る
- Next.jsでmicroCMS連携、dynamic routeのページを作る
- Nowでデプロイする、microCMSでwebhookの設定をする
- Next.jsでプレビュー用のapi routeを作る
- microCMSから画面プレビューする
1. microCMSでAPIを作る
API名:ブログ(違っても可)
エンドポイント:blogs
ここで、以下の情報をメモしてください。
「APIリファレンス」タブのエンドポイントのX-API-KEY
「試してみる」ボタンを押してでてくるhttps://~~~~~~/api/v1/
までのURL(v1でない場合もあり)
2. Next.jsでmicroCMS連携、dynamic routeのページを作る
環境構築
こちらの記事等を参考にして、Next.jsの環境を作ってください。
axiosとdotenvを使うので、モジュールを追加してください。
yarn add axios dotenv
next 9.1からsrc
にpages
をいれることができるようになったので、root/src/pages/
の形にします。
next.config.js
を作り、以下のように記述します。
const { resolve } = require('path')
require('dotenv').config()
const nextConfig = {
webpack: (config) => {
config.resolve.alias['~'] = resolve(__dirname, 'src')
return config
},
exportTrailingSlash: true,
env: {
API_KEY: process.env.API_KEY,
SECRET_KEY: process.env.SECRET_KEY,
END_POINT: process.env.END_POINT
}
}
module.exports = nextConfig
コンポーネント等をimportするときにsrcからのパスで記述できるようにする設定と、環境変数の設定です。
ルートに.env
ファイルを作り、先ほどメモした情報をコピーします。
API_KEY=xxxx
END_POINT=xxxx
ページを作成
先ほど作ったAPIスキーマに合わせて、interfaceを定義するファイルを作ります。
export interface Blogs {
id: string
createdAt: string
title: string
label: string
description: string
}
pages
ディレクトリにblogs
ディレクトリを作り、[id].tsx
ファイルを作成、以下のように入力します。
データの取得等はこのあと追加するので、とりあえずページを表示する部分です。
import { Blogs } from '~/interfaces'
import { NextPage, GetStaticPaths, GetStaticProps } from 'next'
import Head from 'next/head'
import axios from 'axios'
import Link from 'next/link'
interface Props {
blog: Blogs
errors?: string
}
const BlogDetail: NextPage<Props> = (props) => {
return (
<>
<Head>
<title>ブログ詳細</title>
</Head>
<h1 className="title">ブログ詳細</h1>
<Link href="/blogs/">
<a className="link">ブログトップへ</a>
</Link>
<div className="item">
<h2 className="item__title">{props.blog.title}</h2>
<p className="item__label">{props.blog.label}</p>
<p className="item__description">{props.blog.description}</p>
</div>
</>
)
}
export default BlogDetail
getStaticProps
とgetStaticPaths
はnext 9.3で導入された新APIで、SSGのために作られたものです。
これまではgetInitialProps
をつかっていましたが、こちらを使うことにより完全に静的化されたファイルが生成されます。
getServerSideProps
も用意されており、SSGするとき以外ではこちらを使うことになりそうです。
これらはpages
内にあるファイルでしか使用できません。
dynamic routeのパス名を指定する
先ほどのファイルに以下を追加します。
getStaticPaths
は、dynamic routeで生成されるパス名を指定するためのものです。
microCMSからデータを取得してきて、id名をパス名として返しています。
GetStaticPaths
の型の中身を見るとわかりますが、文字列の配列を返す必要があります。
microCMSはデフォルトで返ってくるデータの数は10個になっているので、パラメータとして?limit=9999
を指定して全てのデータをとってこれるようにしています。
export const getStaticPaths: GetStaticPaths = async () => {
const key = {
headers: { 'X-API-KEY': process.env.API_KEY }
}
const res = await axios.get(process.env.END_POINT + 'blogs/?limit=9999', key)
const data: Array<Blogs> = await res.data.contents
const paths = data.map((item) => ({
params: { id: item.id.toString() }
}))
return { paths, fallback: false }
}
データの受け渡しをする
先ほどのファイルに以下を追加します。
getStaticProps
はサーバ上でデータを取得し、ページに渡すためのAPIです。
getServerSideProps
ではなくこちらを使うことにより、最適な静的サイトを生成することができるようです。
params
として、このページのPath名を取得できます。
パス名はid名にしているので、microCMSにそのid名のエンドポイントを指定し、一件だけのデータを取得してページに返すようにしています。
export const getStaticProps: GetStaticProps = async ({ params }) => {
const key = {
headers: { 'X-API-KEY': process.env.API_KEY }
}
const res = await axios.get(
process.env.END_POINT + 'blogs/' + params?.id,
key
)
const data: Blogs = await res.data
return {
props: { blog: data }
}
}
これで[id].tsxは完成です。
それぞれのページへのリンクを持ったブログのトップを作る
blogsのindexを作ります。以下のファイルを作成してください。
import Head from 'next/head'
import { Blogs } from '~/interfaces'
import axios from 'axios'
import { NextPage, GetStaticProps } from 'next'
import Link from 'next/link'
interface Props {
blogs: Array<Blogs>
}
const BlogHome: NextPage<Props> = ({ blogs }) => (
<>
<Head>
<title>blogs</title>
</Head>
<h1 className="title">ブログトップ</h1>
<Link href="/">
<a className="link">ホームへ</a>
</Link>
<div>
{blogs.map((blog, index) => (
<div className="item" key={index}>
<h2 className="item__title">{blog.title}</h2>
<p className="item__label">{blog.label}</p>
<Link href="/blogs/[id]" as={`/blogs/${blog.id}`}>
<a className="item__link">詳細へ</a>
</Link>
</div>
))}
</div>
</>
)
export const getStaticProps: GetStaticProps = async (): Promise<{
props: Props
}> => {
const key = {
headers: { 'X-API-KEY': process.env.API_KEY }
}
const res = await axios.get(process.env.END_POINT + 'blogs/?limit=9999', key)
const data: Array<Blogs> = await res.data.contents
return {
props: {
blogs: data
}
}
}
export default BlogHome
3. Nowでデプロイする、microCMSでwebhookの設定をする
Now(現Vercel)では.envファイルが自動的に除外されるので、nowでの環境変数を宣言するファイルを作ります。
ルートに、now.json
を作り、.envファイルと同じ情報を入力します。
{
"build": {
"env": {
"API_KEY": "xxxx",
"END_POINT": "xxxx"
}
}
}
githubとnowを連携して、nowで新しいプロジェクトを作ります。
https://vercel.com/login
こちらからgithubのアカウントでログインし、「Import Project」を選択します。
「From Git Repository」があるので、こちらから先ほど作成したgithubのリポジトリを選択します。
ビルドのコマンドは、「yarn build」にしてください。
自動的にビルドされURLが表示されるので、そちらにアクセスしちゃんと表示できていれば完了です。
microCMSでwebhookを設定する
静的に生成されたアプリ(getInitialProps
かgetServerSideProps
がないページは自動で静的に生成される)は、build時にmicroCMSからデータをとってきてページを生成しているので、
microCMSが変更されたら再ビルドするようにmicroCMS側にwebhookを設定する必要があります。(アクセス時にmicroCMSからデータとってこないので)
nowのProject→Settings→Git Integration に「Deploy Hooks」があるので、
- Hook Name:なんでもよい
- Git Branch Name: 連携しているGithub のブランチ名(ブランチ切ってないならmaster)
を入力して「Create Hook」してください。
URLが生成されるので、それをコピーします。
microCMSの管理画面で、「API設定」タブの「Webhook」で
「カスタム通知」を選択し、先ほどのURLを貼り付けます。
「コンテンツの公開時」「コンテンツの削除時」にチェックをつけ、追加します。
4. Next.jsでプレビュー用のapi routeを作る
先ほどのpreview-modeのドキュメントを見ると、secret keyを設定してapi側で判定するように書いてあるので、環境変数に追加します。
なんでも好きな文字列で結構です。
now.jsonも同じように変更するのを忘れないでください。
SECRET_KEY=xxxxxxx
pages/api/preview.ts
を作成し、以下のように入力します。
import { NextApiHandler } from 'next'
import axios from 'axios'
const preview: NextApiHandler = async (req, res) => {
// クエリの確認
if (
req.query.secret !== process.env.SECRET_KEY ||
!req.query.id ||
!req.query.draftKey
) {
return res
.status(401)
.json({ message: `Invalid query, ${process.env.SECRET_KEY}` })
}
// 下書きのデータを取得
const key = {
headers: { 'X-API-KEY': process.env.API_KEY }
}
const url =
'https://next-test.microcms.io/api/v1/blogs/' +
req.query.id +
`?draftKey=${req.query.draftKey}`
const post = await axios.get(url, key)
// エラー処理
if (!post) {
return res.status(401).json({ message: 'Invalid draft key' })
}
// プレビューデータを格納
res.setPreviewData({
draftKey: req.query.draftKey,
id: req.query.id
})
// 詳細ページへリダイレクト
res.writeHead(307, { Location: `/blogs/${req.query.id}` })
res.end('Preview mode enabled')
}
export default preview
res.setPreviewData
を設定すると、プレビューモードとなり、アプリ内のページから context.preview
が true
として判別できるようになります。
また、オブジェクトとして値を渡すと、context.previewData
としてアプリ内のページからアクセスできるようになります。
値をセットしたら、下書きの詳細ページへリダイレクトするようにします。一瞬404になってしまいますが、きちんと詳細ページがレンダリングされることが確認できました。
ページの処理を書く
引数のpreview
がtrue
だった時の処理を追加していきます。
ポイントとして、microCMSは下書きのデータを取得する際にはその下書きのdraftKey
をパラメータとして指定しないといけないので、urlに追加するようにします。
// ...
// 省略
export const getStaticProps: GetStaticProps = async ({
params,
preview,
previewData
}) => {
const key = {
headers: { 'X-API-KEY': process.env.API_KEY }
}
let url = process.env.END_POINT + 'blogs/' + params?.id
// 下書きは draftKey を含む必要があるのでプレビューの時は追加
if (preview) {
url += `?draftKey=${previewData.draftKey}`
}
const res = await axios.get(url, key)
const data: Blogs = await res.data
return {
props: { blog: data }
}
}
指定したパス名以外でも通るように修正
getStaticPaths
の戻り値で、return { paths, fallback: false }
としていましたが、
ここのfallback
をtrue
にします。
fallback を falseにすると、paths
として返したパス以外はすべて自動的に404になってしまいます。
プレビューモードでdynamic routeにアクセスするときは、当然ここで指定したパス以外のページにルーティングされるため、falseのままだと404になってしまいます。
fallback を trueにすることで、ここで指定したパス以外で入ってきたときも通すようにできるようになります。
しかし、どんなパスでも通るようになるということなので、ページを表示するほうでエラー処理を追加します。
// ... 省略
import ErrorPage from 'next/error'
// ... 省略
const BlogDetail: NextPage<Props> = (props) => {
if (!props.blog) {
return <ErrorPage statusCode={404} />
}
// ... 省略
ブログのトップページのほうも、プレビュー時にはプレビューのデータを追加するように処理を追加します。
// ...
// 省略
export const getStaticProps: GetStaticProps = async ({
preview,
previewData
}): Promise<{
props: Props
}> => {
const key = {
headers: { 'X-API-KEY': process.env.API_KEY }
}
const res = await axios.get(process.env.END_POINT + 'blogs/?limit=9999', key)
const data: Array<Blogs> = await res.data.contents
// プレビュー時は draft のコンテンツを追加
if (preview) {
const draftUrl =
process.env.END_POINT +
'blogs/' +
previewData.id +
`?draftKey=${previewData.draftKey}`
const draftRes = await axios.get(draftUrl, key)
data.unshift(await draftRes.data)
}
return {
props: {
blogs: data
}
}
}
export default BlogHome
これでプレビュー用のAPI routeの設定完了です。
5. microCMSから画面プレビューする
先ほどの変更をpush し、nowでデプロイします。
アプリのURLをコピーし、microCMS側で設定します。
microCMSの「API設定」の「画面プレビュー」で、先ほどのURLを入力、secretを自分で決めたsecret keyの文字列を入力してください。
上手くいかない場合は、URLのところをlocalhost:3000
等にしてみて、ローカルで検証すると良いです。
コンテンツを追加する部分で、下書きを追加し、「画面プレビュー」をクリックすると、プレビュー用に設定したURLに飛び、下書きが追加された状態のアプリを見ることができます。
最後に
少し長くなってしまいましたが、なにか間違っているところなどあればお知らせいただけますと幸いです。