Next.js + microCMS で dynamic route / preview 機能を実装する

# はじめに
最近JAMstackで開発している人をたくさん見るようになり、Headless CMSがちらほらでてきたり、フレームワークがより開発しやすくなるように整備を始めたりしていて、大規模以外の案件等ではJAMstackを選択することが増えてくるのかな、と思っています。
Next.js も9.3からSSG用に`getStaticProps`と`getStaticPaths`が導入されたり、プレビュー機能が導入されたりと、よりJAMstackで開発しやすくなるようになってきているようです。

今回だいたい最小構成を作ったので解説です。
preview機能だけ書こうかと思いましたが、`getStaticProps`等を使っているリファレンスがあまりなかったので、動的ルーティングからの手順を書いています。

TypeScript覚えたてなので、型の指定の仕方が間違っていたりしたらすみません。

ーーーー**05/04追記**ーーー
Twitterで、SSGでもapi routeが使えるとのご指摘をいただきまして内容を多少修正しました。
[柴田さん](https://twitter.com/shibe97)、ありがとうございます。

## 対象読者
- microCMSでアカウントを作ってちょっと触ったことがある
- Next.jsを使ったことがある

今回TypeScriptで開発しましたが、使ってない人は適宜型などを削除して読んでいただければと思います。

## 環境
- Windows 10 pro
- node.js 12.14.1

### リポジトリ
Githubリポジトリは[こちら](https://github.com/itomise/next-microcms-preview-sample)です。
なお、こちらのリポジトリはqiita用に環境変数を削除して公開しているものなので、
環境変数を正しくいれるとうごきます。（microCMSの情報）

# 全体の流れ
1. microCMSでAPIを作る
- Next.jsでmicroCMS連携、dynamic routeのページを作る
- Nowでデプロイする、microCMSでwebhookの設定をする
- Next.jsでプレビュー用のapi routeを作る
- microCMSから画面プレビューする

## 1. microCMSでAPIを作る
API名：ブログ（違っても可）
エンドポイント：**blogs**

APIスキーマは以下の画像のように設定してください。
![api_schema.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441132/d0bbee43-d236-41d8-728a-fd5c09614755.png)

#### ここで、以下の情報をメモしてください。
「APIリファレンス」タブのエンドポイントの`X-API-KEY`
「試してみる」ボタンを押してでてくる`https://~~~~~~/api/v1/`までのURL（v1でない場合もあり）

## 2. Next.jsでmicroCMS連携、dynamic routeのページを作る

#### 環境構築
[こちらの記事](https://code-log.hatenablog.com/entry/2020/01/23/231848)等を参考にして、Next.jsの環境を作ってください。

axiosとdotenvを使うので、モジュールを追加してください。

```
yarn add axios dotenv
```

next 9.1から`src`に`pages`をいれることができるようになったので、`root/src/pages/`の形にします。
`next.config.js`を作り、以下のように記述します。

```javascript
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を定義するファイルを作ります。

```typescript:src/interfaces/index.ts
export interface Blogs {
  id: string
  createdAt: string
  title: string
  label: string
  description: string
}
```

`pages`ディレクトリに`blogs`ディレクトリを作り、`[id].tsx`ファイルを作成、以下のように入力します。
データの取得等はこのあと追加するので、とりあえずページを表示する部分です。

```typescript:src/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はデフォルトで返ってくるデータの数は１０個になっているので、パラメータとして`?limit=9999`を指定して全てのデータをとってこれるようにしています。

```typescript:src/pages/blogs/[id].tsx
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名のエンドポイントを指定し、一件だけのデータを取得してページに返すようにしています。

```typescript:src/pages/blogs/[id].tsx
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を作ります。以下のファイルを作成してください。

```typescript:src/pages/blogs/index.tsx
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ファイルと同じ情報を入力します。

```json
{
  "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も同じように変更するのを忘れないでください。**

```:.env
SECRET_KEY=xxxxxxx
```

`pages/api/preview.ts`を作成し、以下のように入力します。

```typescript:src/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に追加するようにします。

```typescript:src/pages/blogs/[id].tsx
// ...
// 省略
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にすることで、ここで指定したパス以外で入ってきたときも通すようにできるようになります。
しかし、どんなパスでも通るようになるということなので、ページを表示するほうでエラー処理を追加します。

```typescript:src/pages/blogs/[id].tsx
// ... 省略
import ErrorPage from 'next/error'
// ... 省略
const BlogDetail: NextPage<Props> = (props) => {
  if (!props.blog) {
    return <ErrorPage statusCode={404} />
  }
// ... 省略
```

ブログのトップページのほうも、プレビュー時にはプレビューのデータを追加するように処理を追加します。

```typescript:src/pages/blogs/index.tsx
// ...
// 省略
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`等にしてみて、ローカルで検証すると良いです。
![microCMS_preview.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441132/a9713da0-5cbe-4956-635b-fcb1bf7a4405.png)

コンテンツを追加する部分で、下書きを追加し、「画面プレビュー」をクリックすると、プレビュー用に設定したURLに飛び、下書きが追加された状態のアプリを見ることができます。

# 最後に
少し長くなってしまいましたが、なにか間違っているところなどあればお知らせいただけますと幸いです。