【Next.js 9.3】getStaticPropsでmarkdownブログを高速に

# Next.js 9.3
本日(2020/03/10)リリースされた**Next.js 9.3**では、主に静的サイト生成(Static Site Generation)の機能が追加されました。
これにより、今まで`getInitialProps`でSSRしていたものも、静的なHTML + JSONとしてビルド時に吐き出されるようになりました。

早速Next.jsをupgradeし実装してみたところ、
:tada: **(自作のmarkdownブログでは少しですが)高速化！**

getInitialPropsで書いていた部分を置き換えるだけで実装できたので、コードとともに紹介します。

サイトはこちら -> [ragnar blog](https://ragnar-blog.net)
4/17追記) 結局MarkdownをやめてGraphQLバックエンドになりました:grinning:

## 新機能

SSG用のメソッドとしては、
- *getStaticProps*
- *getStaticPaths*

の2つが追加されました。

### getStaticProps
`getStaticProps`は、`getInitialProps`で行っていた外部リソースの取得を行うメソッドです。
`getInitialProps`との違いは、常にリソースを取得するわけではなく、ビルド時に一度だけ実行されます。

```tsx
export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
  }
}
```

この場合、postsはビルド時一度だけ実行され、更新されません。

参考: [公式Doc](https://nextjs.org/docs/basic-features/data-fetching)


### getStaticPaths

`getStaticPaths`は、名前の通りパスを取得するメソッドです。

`pages/post/[postId].tsx`というファイルを作れば、/post/1, /post/2...と動的なルーティングが可能です。
`[postId].tsx`に`getStaticPaths`を記述し、生成されるパスを取得するようにしておけば、Next.jsがビルド時にルーティング先を静的に事前レンダリングしてくれます。

```tsx
export async function getStaticPaths() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  const paths = posts.map(post => ({
    params: { id: post.id },
  }))

  return { paths, fallback: false }
}
```
`fallback`は、事前生成したルーティングに一致しないURLへのリクエストが来た際の挙動を示します。
`fallback = true`の場合、事前ビルドされていないパスに対して404を返却せず、`getStaticProps`を呼び出しそのパスに対する応答を返すようです。
公式Docでは高速なビルドをしつつ、データに依存する非常に多数の静的ページがある場合に便利とのこと。

静的ページとして吐き出す際、`[slag].tsx`ファイルには`getStaticPaths`および`getStaticProps`を実装している必要があるので注意が必要です。
参考: [公式Doc](https://nextjs.org/docs/basic-features/data-fetching)

## 従来のコード

Next.jsでのmarkdownブログは、`Page.getInitialProps`でmarkdownを読み出していました。

- 自作サイトのIndexページの例

```tsx:src/pages/index.tsx
import fs from 'fs';
import { NextPage } from 'next';


type CodeProps = {
  code: string;
  language: string;
};

type InitialProps = {
  profile: CodeProps;
  site: CodeProps;
};

// InitialPropsはgetInitialPropsの返り値型
const Index: NextPage<InitialProps> = props => {
  return (
    <>
      
      ...略
   </>
  )
};

Index.getInitialProps = async () => { 
   const profile = fs.readFileSync("./src/codes/profileCode.py"); 
   const site = fs.readFileSync("./src/codes/thisSite.ts"); 
   return { 
     profile: { code: profile.toString(), language: "python" },
     site: { code: site.toString(), language: "typescript" }, 
   }; 
 };

export default Index
```

このコードでは、リクエストの度に毎回`fs.readFileSync`を実行しファイルを読み出していました。
これを`getStaticProps`に書き換えることで、毎回取得する必要がなくなります。

```tsx:src/pages/index.tsx
import { GetStaticProps, NextPage } from 'next';

// Index.getInitialPropsのところを書き換える
export const getStaticProps: GetStaticProps = async () => {
  const profile = fs.readFileSync("./src/codes/profileCode.py");
  const site = fs.readFileSync("./src/codes/thisSite.ts");
  return {
    props: {
      profile: { code: profile.toString(), language: "python" },
      site: { code: site.toString(), language: "typescript" },
    },
  };
};
```
`getInitialProps`とは異なり、ファイル内に`export async function getStaticProps`としておくだけで良いようです。

- 型定義
 
`GetStaticProps`の型定義はこんな感じ。
![getStaticProps型](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508043/402bacb0-88c6-1027-01b6-159e44707459.png)
今まで`getInitialProps`で返却していたオブジェクトを、`{props: Object}`とするだけです。


## 動的ルーティング

先の例はパスに依存しない、静的なページでの外部リソース読み込みの例でしたが、次は動的ルーティングを行っていたページを書き換えます。
markdownブログでは、`/post`では特定のフォルダ内のファイルを読み込み、その数だけファイル名でリンクを作成し、`/post/[postId]`ページで対象のmdファイルを読み込み、renderしていました。

省略していますが、以前のコードは大体こんな感じ。

```tsx:src/pages/post/index.tsx
import matter from 'gray-matter';
import { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';

type Props = {
  posts: {
    fileName: string;
    md: matter.GrayMatterFile<any>;
  }[];
};

export const PostListPage: NextPage<Props> = props => {
  const posts = props.posts;
  return (
    <>
      <Head>
        <title key="title">記事一覧 - Ragnar Blog</title>
      </Head>
      <Title>記事一覧</Title>
      {posts.map((item, key) => (
        <PostCard key={key} fileName={item.fileName} md={item.md} />
      ))}
    </>
  );
};

PostListPage.getInitialProps = async function() {
  // get all .md files from the src/posts dir
  const contexts = require.context("../../md", true, /\.md$/);
  const posts = contexts.keys().map(path => {
    // 拡張子を省いたファイル名
    const fileName = path.match(/([^/]*)(?:\.([^.]+$))/)[1];
    const mdData = matter(contexts(path).default);
    return {
      fileName: fileName,
      md: mdData,
    };
  });
  // postを日付でソート
  posts.sort((a, b) => {
    if (a.md.data.date > b.md.data.date) return -1;
    if (a.md.data.date < b.md.data.date) return 1;
    return 0;
  });

  return {
    posts: posts,
  };
};

export default PostListPage;
```

```tsx:src/pages/post/[postId].tsx
import matter from 'gray-matter';
import { NextPage, NextPageContext } from 'next';
import Head from 'next/head';
import React from 'react';

type Props = {
  md?: matter.GrayMatterFile<any>;
};

const PostDetailPage: NextPage<Props> = props => {
  const meta = props.md.data;
  const formattedDate = meta.date ? dateFormat(meta.date) : "2000-01-01";
  return (
    <>
      <Head>
        <title key="title">{meta.title} - Ragnar Blog</title>
      </Head>
      <TitleHeader title={meta.title} date={formattedDate} />
      <MarkDownViewer md={props.md.content} />  
    </>
  );
};

// postIDから同じファイル名のファイルを読み出してPropsとして渡す
PostDetailPage.getInitialProps = async (context: NextPageContext): Promise<any> => {
  const { postId } = context.query;
  const contexts = require.context("../../md", true, /\.md$/);
  const content = contexts.keys().filter(path => {
    const fileName = path.match(/([^/]*)(?:\.([^.]+$))/)[1];
    return fileName === postId;
  });
  const data = matter(contexts(content[0]).default);
  return { md: data };
};

export default PostDetailPage;
```

## 書き換え
`[postId].tsx`から書き換えて行きます。
`getStaticPaths`および`getStaticProps`を記述します。

- 型定義

`GetStaticPaths`の型定義。
![スクリーンショット 2020-03-10 23.20.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508043/cbe71f32-ad39-0ff0-1b63-aaf16138a551.png)
pathsにルーティングするパスの配列を入れるだけです。
今回は存在するファイルの数しかルーティングは出来ないので、fallbackはfalseです。

`getInitialProps`の場合context.queryからslugを取得していましたが、
`getStaticProps`では`context.params`に変わりました。

```tsx:src/pages/post/[postId].tsx
import { GetStaticPaths, GetStaticProps, NextPage } from 'next';

const PostDetailPage: NextPage<Props> = props => {
    return <>...</>
}

export const getStaticPaths: GetStaticPaths = async function() {
  const contexts = require.context("../../md", true, /\.md$/);
  const allPosts = contexts.keys().map(path => {
    const fileName = path.match(/([^/]*)(?:\.([^.]+$))/)[1];
    return { params: fileName };
  });
  // /post/fileName がパス
  return {
    paths: allPosts.map(post => `/post/${post.params}`) || [],
    fallback: false,
  };
};

export const getStaticProps: GetStaticProps = async context => {
  // slugはcontext.paramsから取得
  const { postId } = context.params;
  const contexts = require.context("../../md", true, /\.md$/);
  const content = contexts.keys().filter(path => {
    const fileName = path.match(/([^/]*)(?:\.([^.]+$))/)[1];
    return fileName === postId;
  });
  const matterData = matter(contexts(content[0]).default);
  delete matterData.orig;
  if (matterData.data.date) {
    matterData.data.date = new Date(matterData.data.date).toISOString();
  }
  // { props: Props }にする
  return { props: { md: matterData } };
};
```


## デプロイして確認
Nowにデプロイしてみると、`/post/[postId]`以下のページが生成されており、それらが`○ (SSG)`となっていることがわかります。
同時に、ビルド時間が伸びていることもわかります。

- 今回のビルド結果
![new build result](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508043/08a104b5-d609-1337-9d73-be48c8ccb62b.png)

- Next.js 9.2以前
![old build result](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508043/0fe8367a-91bd-951a-2749-bb134dcb2aa5.png)



## サイトで確認
サイトでも確認してみます。
投稿一覧ページから、投稿詳細ページへの遷移は体感半分程度になった気がします。

もともと`ReactMarkDown`のレンダリングがボトルネックになっているところもあり、差は小さいのですが・・・

- 今回のビルド結果
![new.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508043/21b716fb-b4ae-4c04-3dc5-f0ae6c5ce9ac.gif)


- Next.js 9.2以前
![old.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508043/c882b742-e203-b189-1abf-d0649b9cbd7b.gif)

## 終わりに
`getInitialProps`を利用していた部分の書き換えは思っていた以上にそのままでした。
`getInitialProps`はビルド、サーバーサイド、クライアントサイドで実行されうるメソッドだったので、今回からはより挙動も把握しやすくなったため、
9.3以降では主に`getStaticProps`等が利用されていくことになると思います。

Next + NowのDXは素晴らしいですが、SSGも強化されてさらにNext推しになってしまいそうですね。
ブログも随時更新中ですのでよければ。 -> [ragnar blog](https://nextblog.ragnar1904.now.sh)