猫でもわかるWebプログラミングと副業

本業エンジニアリングマネージャー。副業Webエンジニア。Web開発のヒントや、副業、日常生活のことを書きます。

Next.js で React の Server-side Rendering と Static Generation をやる #4 Dynamin Routes

前回の記事

この記事は第4回の記事です。

今までの記事

今回はルーティング周りをやっていきます。

Dynamic Routes とは

ブログの記事とか、書くたびに Routes 、つまり URL が増えていきます。こういうもののことを Dynamic Routes と呼びます。 /posts/<id> のような形式のルーティングのことです。

前回の記事で、ブログ記事を markdown ファイルで用意し、読み込むことをしました。今回はこの markdown のファイル名を ID として、 Dynamic Routes を設定しようと思います。

例えば、こんな URL になります。

  • posts/ssg-ssr
  • /posts/pre-rendering

これをするには、pages/posts/[id].js というファイルを作ります。 [] が Dynamin Routes を表します。

そして、新しく、 getStatisPaths 関数が登場します。idとして利用可能な値をこの関数で返します。

例:

import Layout from '../../components/layout'

export default function Post() {
  return <Layout>...</Layout>
}

export async function getStaticPaths() {
  // id として取りうる値を列挙して返す
}

続いて、今まで同様に getStatisProps を実装しますが、パラメータで id を受け取るように変更します。

pages/posts/first-post.js ファイルは使わなくなるので消ます。

代わりに [id].js を作成します。中身はまだ空です。

import Layout from '../../components/layout'

export default function Post() {
  return <Layout></Layout>
}

getStatisPaths 関数で、ページ一覧が必要になるので、lib/posts.js にメソッドを追加します。レスポンス形式はコメントに書いてあるとおりです。 string[] を返すわけはないので、注意が必要です。

export function getAllPostIds() {
  const fileNames = fs.readdirSync(postsDirectory)

  // Returns an array that looks like this:
  // [
  //   {
  //     params: {
  //       id: 'ssg-ssr'
  //     }
  //   },
  //   {
  //     params: {
  //       id: 'pre-rendering'
  //     }
  //   }
  // ]
  return fileNames.map(fileName => {
    return {
      params: {
        id: fileName.replace(/\.md$/, '')
      }
    }
  })
}

これを [id].js から使います。fallback: false については、ここでは説明しません。

import { getAllPostIds } from '../../lib/posts'

export async function getStaticPaths() {
  const paths = getAllPostIds()
  return {
    paths,
    fallback: false
  }
}

続いて、 lib/posts.js に、postの中身を取得する関数を追加します。

export function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')

  // Use gray-matter to parse the post metadata section
  const matterResult = matter(fileContents)

  // Combine the data with the id
  return {
    id,
    ...matterResult.data
  }
}

これを pages/posts/[id].jsgetStaticProps の中で使います。

import { getAllPostIds, getPostData } from '../../lib/posts'

// params を受け取るようにする
export async function getStaticProps({ params }) {
  // id に対応するデータを取得
  const postData = getPostData(params.id)
  
  return {
    props: {
      postData
    }
  }
}

最後に、 [id].jsPost コンポーネントの中身を実装します。

export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
    </Layout>
  )
}

http://127.0.0.1:3000/posts/ssg-ssr にアクセスすると、ブログのタイトルと日付だけ出ているはずです。

f:id:yoshiki_utakata:20210205171149p:plain

ここまでの差分: https://github.com/yoshikyoto/nextjs-blog/commit/3ebbc61d0ea55077c4b9daf79a40f98502daa900

Markdown で書かれた記事を表示する

markdown のライブラリを追加します。

npm install remark remark-html

docker再起動

docker-compose down
docker-compose up -d

lib/posts.jsgetPostData を修正します。await remark() するために、関数が async になっている必要があります。

import remark from 'remark'
import html from 'remark-html'

export async function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')

  // Use gray-matter to parse the post metadata section
  const matterResult = matter(fileContents)

  // Use remark to convert markdown into HTML string
  const processedContent = await remark()
    .use(html)
    .process(matterResult.content)
  const contentHtml = processedContent.toString()

  // Combine the data with the id and contentHtml
  return {
    id,
    contentHtml,
    ...matterResult.data
  }
}

メソッドが async になったので、pages/posts/[id].jsでも await するようにします。

export async function getStaticProps({ params }) {
  const postData = await getPostData(params.id)
}

最後に、Post コンポーネントに修正を加えます。

export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
      <br />
      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
    </Layout>
  )
}

ここまでの差分: https://github.com/yoshikyoto/nextjs-blog/commit/094a103c36460d205044772b8cc180b3d997394a

日付をきれいに表示する(CSS)

特に重要なところはないので GitHub の差分だけ

https://github.com/yoshikyoto/nextjs-blog/commit/1cb1d36da21d6913fe8d1fe8a58442e0d8af6ad9

さらに CSS を追加してきれいにする

https://github.com/yoshikyoto/nextjs-blog/commit/c73c4786d0022d23ad2677033befd19724d5d4f1

さらに index ページにも CSS あてる

https://github.com/yoshikyoto/nextjs-blog/commit/c5c97a051d02b7bf457b8aa2df1f70b313271bd8

fallback について

fallback: false は、ページが見つからなかった時に 404 を返します。

fallback についてはドキュメントを読むのが良さそうです。

https://nextjs.org/docs/basic-features/data-fetching#the-fallback-key-required

Catch-all Routes

pages/posts/[...id].js とすると、以下のようなものにマッチするようになります。

  • /posts/a
  • /posts/a/b
  • /posts/a/b/c
// getStaticPaths
return [
  {
    params: {
      // Statically Generates /posts/a/b/c
      id: ['a', 'b', 'c']
    }
  }
]

export async function getStaticProps({ params }) {
  // params.id will be like ['a', 'b', 'c']
}

404 ページをカスタマイズする場合

pages/404.js を作成すると良い

export default function Custom404() {
  return <h1>404 - Page Not Found</h1>
}

API を叩く際の注意点

Next.js を使えば、サーバーサイドのコードも実装できますが、基本的に、API は JS で実装しないほうが良さそうです。

getStaticProps や getStaticPaths といったメソッドは、ビルド時に実行されることになります。ビルド時に API が叩けない状態だと、解決に失敗します。つまり、 Next で API も実装して、さらにクライアントサイドも実装する場合、 getStatisProps などを解決刷る時に困ります。

解決豊富としては、 getStaticProps や getStaticPaths の中では直接 DB などに接続することです。コードはクライアントに公開されないので、これらのメソッドの中では直接DBを読み書きしても問題ありません。

詳細はドキュメントを参照してください。

https://nextjs.org/learn/basics/api-routes/creating-api-routes

次回

次回はデプロイをやっていきます。