Next.jsでGoogle SpreadsheetをHeadlessCMSとして使う

下記のようなスプレッドシートを読み込んで、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/939beeb8-7717-4e37-4001-1133f11925fb.png)

下記のように表示するようなNext.jsアプリを作ってみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/2f5fdb16-2f48-49b6-9bf1-5ac888535104.png)

## 環境
- ChromeOS Crostini (Linux) Debian 10.10
- Docker version 20.10.7
- Next.js 11.0.1

## GCPでサービスアカウントの準備
Node.jsでスプレッドシートに接続する方法はいくつかありますが、今回は**GCPのサービスアカウント**を使います。OAuth2等その他の方法については、[google-api-nodejs-clientのドキュメント](https://github.com/googleapis/google-api-nodejs-client#authentication-and-authorization)を読んでください。

下記手順でサービスアカウントを用意します。

1. GCPプロジェクトを作成
1. 「APIとサービス」→「APIとサービスを有効化」→「Google Sheets API」を有効化
1. 「サービスアカウント」→「サービスアカウントを作成」→適当な名前（next-spreadsheet-cms）で作成
1. JSON形式でサービスアカウントの鍵を作成
1. JSON内の`private_key`と`.client_email`を控えておく
    - `jq`コマンドを使うなら`$ cat XXXXXXXX.json | jq '.private_key,.client_email'`で取り出せます。

## スプレッドシートの作成
https://sheets.new からGoogleスプレッドシートを新規作成し、適当に作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/939beeb8-7717-4e37-4001-1133f11925fb.png)

ここでは`contents`というシート名で、A列にタイトル、B列に本文を入れてみます。（本来ブログみたいな物を作るならば、sulgとか投稿日時とかが欲しいですね）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/cae23526-2721-ead6-736a-6693db55ae7c.png)

URLの第三階層（`https://docs.google.com/spreadsheets/d/XXXXXXXX/edit#gid=0` の`XXXXXXXX` 部分）がスプレッドシートのIDになるので、控えておきます。

右上の「共有」ボタンから「ユーザーやグループを追加」で、先程控えたサービスアカウントのメールアドレスを追加します。権限は「閲覧者」でOKです。

## Next.jsプロジェクトの作成
適当な方法でNext.jsプロジェクトを準備します。今回は`$ yarn create next-app --typescript` で作成します。

```sh
$ mkdir next-spreadsheet-cms && cd $_
$ dun docker run --interactive --tty --rm --volume $(pwd):/app --workdir /app node:alpine yarn create next-app --typescript ./
```

`docker-compose.yml` を作成します。

```docker-compose.yml
version: '3'
services:
  app:
    image: node:alpine
    working_dir: /app
    ports:
      - 3000:3000
    volumes:
      - .:/app
    command: yarn dev
```

`$ docker-compose up`で起動して、localhost:3000でNext.jsの画面が映れば成功です。

## Next.jsからGoogleスプレッドシートに接続
`googleapis`をインストールします。

```sh
$ docker-compose run --rm app yarn add googleapis
```

[googleapisのテストコード](https://github.com/googleapis/google-api-nodejs-client/blob/084e3e2d9ee9c53ce40e2987b01d17b569692147/test/test.auth.ts#L39)を参考に、JWTを取得してスプレッドシートからデータを取得する関数を実装します。

```lib/spreadsheet.ts
import { GoogleApis, google } from 'googleapis';
import { Content } from './content';

const getSheets = () => {
  const googleapis = new GoogleApis();
  const scopes = ['https://www.googleapis.com/auth/spreadsheets.readonly'];
  const jwt = new googleapis.auth.JWT(
    process.env.GCP_SERVICEACCOUNT_EMAIL,
    undefined,
    (process.env.GCP_SERVICEACCOUNT_PRIVATE_KEY || '').replace(/\\n/g, '\n'),
    scopes
  );
  return google.sheets({ version: 'v4', auth: jwt });
};

export const getContents = async (): Promise<Content[]> => {
  const sheets = getSheets();
  const response = await sheets.spreadsheets.values.get({
    spreadsheetId: process.env.SPREADSHEET_ID,
    range: 'contents',
  });
  const rows = response.data.values;
  if (rows) {
    return rows.slice(1).map((row): Content => {
      return {
        title: row[0],
        content: row[1]
      };
    });
  }
  return [];
};
```

```lib/content.ts
export type Content = {
  title: string
  content: string
}
```

このスプレッドシートのデータを`getStaticProps()`で読み込んで使います。

```pages/index.tsx
import Head from 'next/head';
import styles from '../styles/Home.module.css';

import { getContents } from '../lib/spreadsheet';
import { Content } from '../lib/content';

export default function Home({ contents }: { contents: Content[] }) {
  return (
    <div className={styles.container}>
      <Head>
        <title>Next.js Spreadsheet CMS</title>
        <meta name="description" content="Next.js Spreadsheet CMS" />
      </Head>

      <main>
        { contents.map(content => {
          return <>
            <h2>{content.title}</h2>
            <p>{content.content}</p>
          </>;
        }) }
      </main>
    </div>
  );
}

export async function getStaticProps() {
  const contents = await getContents();
  return {
    props: { contents },
    revalidate: 3600,
  };
}
```

控えておいたサービスアカウントの情報とスプレッドシートのIDを、環境変数として渡します。開発環境であれば`.env.local`に記述すればOKです。デフォルトのNext.jsの設定であれば`.env.local`は`.gitignore`に入っているので安心です。

```.env.local
GCP_SERVICEACCOUNT_EMAIL=XXXX
GCP_SERVICEACCOUNT_PRIVATE_KEY=XXXX
SPREADSHEET_ID=XXXX
```

下記のようにスプレッドシートの中身が表示されれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/2f5fdb16-2f48-49b6-9bf1-5ac888535104.png)

以上です。今回の手順で作ったものが下記になります。

https://github.com/s2terminal/next-spreadsheet-cms

## 参考
- [Connecting NextJS Mini Project with Google Spreadsheets as Database \- DEV Community](https://dev.to/frasnym/connecting-your-nextjs-mini-project-with-google-spreadsheets-as-database-1o2d)