15
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

【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
4/17追記) 結局MarkdownをやめてGraphQLバックエンドになりました:grinning:

新機能

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

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

getStaticProps

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

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

  return {
    props: {
      posts,
    },
  }
}

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

参考: 公式Doc

getStaticPaths

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

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

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

従来のコード

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

  • 自作サイトのIndexページの例
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に書き換えることで、毎回取得する必要がなくなります。

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型
今までgetInitialPropsで返却していたオブジェクトを、{props: Object}とするだけです。

動的ルーティング

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

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

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;
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
pathsにルーティングするパスの配列を入れるだけです。
今回は存在するファイルの数しかルーティングは出来ないので、fallbackはfalseです。

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

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

  • Next.js 9.2以前
    old build result

サイトで確認

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

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

  • 今回のビルド結果
    new.gif

  • Next.js 9.2以前
    old.gif

終わりに

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

Next + NowのDXは素晴らしいですが、SSGも強化されてさらにNext推しになってしまいそうですね。
ブログも随時更新中ですのでよければ。 -> ragnar blog

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
15
Help us understand the problem. What are the problem?