Next.jsによるSSG最低限


SSG（Server Side Generation）を利用する機会がありそうなのでメモ。

>Next.jsには9.3からSSGが導入されました。

## やりたいこと

* TOPページ(index.js)に記事一覧
* [id].jsに個別ページ
* index.jsと[id].jsをSSGする
* 試しにcsr.jsも作成して、CSR（Client Side Rendering）の様子も見る

>[id].jsとか[name].jsはNextでの動的ルーティングのページになります。

## 簡易APIの作成（Blog記事APIを想定）

blog記事をSSGすることを想定し、簡単なMock APIを作成してみます。
ここでは静的なJSONを返していますが、実際はDBやHeadlessCMS等から返すことになります。

### 作業場の作成と必要ファイルの生成

```bash
mkdir mock-blog-api
cd mock-blog-api

npm install express cors
touch index.js
```

### 実装

index.jsを実装していきます。
ポイントは、

* クライアントとオリジンが異なるので簡易CORS対応をしている
* ポート3001で起動している（Next側が標準で3000なのでぶつからないようにする）

といったところです。

```js:index.js
const express = require("express");
const app = express();

//CORS対応（許可）
const cors = require("cors");
app.use(cors());

const data = {
    status: "OK",
    contents: [
        { id: 1, title: "記事1", body: "記事1の内容。" },
        { id: 2, title: "記事2", body: "記事2の内容。" },
        { id: 3, title: "記事3", body: "記事3の内容。" }
    ]
};

//記事一覧を返すAPI
app.get("/blog", (req, res) => {
    res.json(data);
});

//個別記事を返すAPI
app.get("/blog/:id", (req, res) => {
    const id = req.params.id;
    res.json(data.contents.find(d => d.id == id));
});

//サーバリッスン開始
app.listen(3001, () => {
    console.log("Server start on port 3001.");
});
```

### 実行

実行しておきます（Nextの実装後でもいいです）。

```bash
node index.js
```

## Next.jsの実装

API側ができたので本丸のNext側の実装です。

### プロジェクトの生成

nodeは入ってる前提でnpxでcraete-next-appを呼び出します。

```bash
npx create-next-app next-ssg-test
cd next-ssg-tset
```

### 必要ファイルの生成

pages以下にblogディレクトリを生成し、その中に[id].jsを生成。

```bash
mkdir pages/blog
touch pages/blog/[id].js
```

いちおうcsrのコードを確認するためcsr.jsも作成し実装してみます。

```bash
touch pages/csr.js
```

### index.js（記事一覧）

getStaticPropsがサイト実行前にサーバで実行され内容を反映したページが生成されます。

```js:pages/index.js
import Link from "next/link";

const Home = ({ blog }) => {
  return (
    <div>
      <h1>記事一覧(SSG)</h1>
      <ul>
        {blog.map((blog) => (
          <li key={blog.id}>
            <Link href={`/blog/${blog.id}`}>
              <a>{blog.title}</a>
            </Link>
          </li>
        ))}
      </ul>
      <p><Link href="/csr"><a>CSRページへ</a></Link></p>
    </div>
  );
}

//SSG処理
export const getStaticProps = async () => {
  const res = await fetch("http://localhost:3001/blog");
  const json = await res.json();
  return {
    props: {
      blog: json.contents,
    }
  }
}


export default Home;
```

### [id].js（個別ページ）

getStaticPathsで生成するページ（path）のパターンを定義。
getStaticPropsでそのパターンでの呼び出しを定義します。

```js:pages/blog/[id].js
import Link from "next/link";

const BlogId = ({ blog }) => {
    return (
        <div>
            <h1>{blog.title}</h1>
            <p>{blog.body}</p>
            <br />
            <p><Link href="/"><a>戻る</a></Link></p>
        </div>
    );
}

export default BlogId;

//どんなpathがあるか
export const getStaticPaths = async () => {
    const res = await fetch("http://localhost:3001/blog");
    const json = await res.json();

    const paths = json.contents.map((content) => `/blog/${content.id}`);
    return { paths, fallback: false };
}

//各ページのレンダリング
export const getStaticProps = async (context) => {
    const id = context.params.id;
    const res = await fetch(`http://localhost:3001/blog/${id}`)
    const json = await res.json();
    return {
        props: {
            blog: json
        }
    }
}
```

### csr.js（普通の？React的CSR）

useStateとuseEffectを利用した一般的なAPI連携。

```js:pages/csr.js
import Link from "next/link";
import { useState, useEffect } from "react";

const CsrPage = () => {

    //contentの入れ物
    const [contents, setContents] = useState([]);

    //ページ読み込み時に取得（CSR）
    useEffect(() => {
        const getContents = async () => {
            const res = await fetch("http://localhost:3001/blog");
            const json = await res.json();
            setContents(json.contents);
        }
        getContents();
    }, []);

    return (
        <div>
            <h1>記事一覧(CSR)</h1>
            <ul>
                {contents.map((content) => (
                    <li key={content.id}>
                        <Link href={`/blog/${content.id}`}>
                            <a>{content.title}</a>
                        </Link>
                    </li>
                ))}
            </ul>
            <Link href="/"><a>戻る</a></Link>
        </div>
    );
}

export default CsrPage;
```

## 実行

###　開発モード（開発中）

以下のようにします。

```bash
npm run dev
```

### 本番モード（SSG処理）

run buildでビルドし、生成ぶつを実行するにはrun startとします。

>buildの結果物は.next/server/pages以下に生成されるようです。

```bash
npm run build
npm run start
```

### その他

これらをgithub軽油でvercelやgithub pagesなどにデプロイします。

## おまけ

ついでにuseSWRを利用してデータを取得してみます。
SWRはRenderingの種類ではない。Stale-While-Revalidateというキャッシュ戦略の略。

```bash
touch pages/swr.js
```

実装。シンプルに書け、かついろいろ便利な機能があるが、ここでは以上とします。

```js:pages/swr.js
import useSWR from "swr";
import Link from "next/link";

//fetcherの定義
const url = "http://localhost:3001/blog";
const fetcher = () => fetch(url).then(r => r.json());

const SwrPage = () => {

    //swr
    const { data, error } = useSWR(url, fetcher);

    //エラーやローディング中の処理
    if (error) return <div>fail to load.</div>
    if (!data) return <div>Loading...</div>

    return (
        <div>
            <h1>記事一覧（SWR利用）</h1>
            <ul>
                {data.contents.map((content) => (
                    <li key={content.id}>
                        <Link href={`/blog/${content.id}`}>
                            <a>{content.title}</a>
                        </Link>
                    </li>
                ))}
            </ul>
        </div>
    );
}

export default SwrPage;
```

## 参考

* https://blog.microcms.io/microcms-next-jamstack-blog/