# Next.jsのチュートリアル、その4

Next.jsのチュートリアル、その1Next.jsのチュートリアル、その2Next.jsのチュートリアル、その3に続いて4つ目です。

# Dynamic Routes

indexページをブログのデータを使ってやってきましたが、それぞれのブログ記事のページはまだありません。ってことで、これらをdynamic routesを使ってやっていきましょうの回。

ここで学ぶことは、

  • getStaticPathsを使ってdynamic routesを使って静的ページを生成していく(字面だけ追うと静的/動的がわかりにくいですね。笑)
  • getStaticPathsを使ってそれぞれのブログ記事のデータを取得
  • remarkを使ってマークダウンをレンダリングする
  • pretty-print date strings(ってなんじゃらほい?現時点で分かっていませんがw、日付をいい感じのフォーマットでやりくりしてくれる文字列用の便利なヤツかな?)
  • ページをdynamic routesにリンクさせる
  • dynamic routesに関する耳打ちな情報

# 外部データに依存したページのパス

前回のレッスンではgetStaticPropsを使ってデータを取得してindexページにレンダリングしたけど、今回は外部データによってページのパスを設定しつつそれぞれの静的ページを動的にNext.jsの中で出力するところをやっていきます、と。

で、どうやってDynamic Routesを使って静的ページを作っていくかというと、

  • /posts/<id>というパスにするとして、idはマークダウンファイルのファイル名にすると。で、そのファイルはpostsディレクトリの下にあるファイル達
  • 今はssg-ssr.mdpre-rendering.mdがあるわけですが、それを/posts/ssg-ssr/posts/pre-renderingという感じにしてやる

全体像としては、[id].jspages/postsに作っていきます、と。で、この[]がNext.jsにおけるdynamic routesです。ってことで、pages/posts/[id].jsを以下のようにしてやるイメージ。今まで作ってきたページと同様に。

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

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

そして、これから新しいヤツが出てきてgetStaticPathsっていうファンクション。ここではidに突っ込むリストを返す必要があります、と。

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

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

export async function getStaticPaths() {
  // Return a list of possible value for id
}

で、getStaticPropsがまた出てきたけど、これはidを元にデータを取得しにいくヤツ。で、paramsにはidが入ってくるよ、と(ファイルが[id].jsなので)。

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

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

export async function getStaticPaths() {
  // Return a list of possible value for id
}

export async function getStaticProps({ params }) {
  // params.idを使って必要なデータを取ってくる
}

で、いよいよ実装ですよ、と。

  • まずはpages/posts[id].jsを作っていって
  • first-post.isは要らないので消していきます、と。

ってことで、まずはpages/posts[id].jsを以下のように。Layoutの...は後から実装していきます。

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

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

で、次にlib/posts.jsを開いて、getAllPostIdsっていうファンクションを下部に追加。これによってpostsディレクトリのファイル名のリストを返してくれる。その際.mdの部分は取り除かれる。

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

  return fileNames.map(fileName => {
    return {
      params: {
        id: fileName.replace(/\.md$/, '')
      }
    }
  })
}

ここで、Importantって書いてあって、このリターンされるリストは単純に文字列の配列っていうわけではなくて↓のように、それぞれがparamsというキーを持って、そこにidがキーになっているオブジェクトがないとダメということ。これによってファイル名の[id]が活用できるということで、こうなってないとgetStaticPathsが失敗してしまいますよ、と。

[
  {
    params: {
      id: 'ssg-ssr'
    }
  },
  {
    params: {
      id: 'pre-rendering'
    }
  }
]

ってことで、このgetAllPostIdsファンクションをインポートしてgetStaticPathsの中で使っていきましょう。

getAllPostIdsをインポートして

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

getStaticPaths()ファンクションを実装

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

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

pathsgetAllPostIds()から返されたパス(というかファイル名)の配列で、fallback: falseについては後ほど説明、と。

ってことで、ほぼ完成したけど、getStaticPropsやんなきゃね。

idを元にして必要なブログのデータを取ってくる必要があるので、lib/posts.jsgetPostDataファンクションを追加していきます。これはidによってブログポストのデータを取得してくるものです。

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

  // gray-matterでmetadetaをパース
  const matterResult = matter(fileContents)

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

この...はSpread operatorと呼ばれていて、gray-matterから返ってきたdata(---で囲まれたところが、dataというkeyの中でそれっぽくなって返ってくる)を展開してといった動作をするのだそうで、idと一緒にそのコンテンツを返してあげましょう的な。

そして、pages/posts/[id].jsを開いてgetAllPostIdsだけでなく上記のgetPostDataもimportするように変更してそれを使うように。

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

export async function getStaticProps({ params }) {
  const postData = getPostData(params.id)
  return {
    props: {
      postData
    }
  }
}

それぞれのブログポストのページはgetPostDataファンクションをgetStaticPropsで呼んで、それをページコンポーネントに返す感じで。

ってことで、PostコンポーネントをpostDataを使う形に改修しやすよ、と。pages/posts/[id].jsで、Postコンポーネントのところを以下のように。

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

↓こんな感じになりました〜

ポスト

ってことで、dynamic routesっていうのはこうやってやるんだぜ的な感じで。

とは言え、マークダウンの本体部分はまだ表示されてないので、次にそれをやっていきます。

# Markdownのレンダリング

マークダウンのコンテンツをレンダリングするのにremarkというライブラリを使うので、まずはnpmでインストール。

a$ npm install remark remark-html

added 52 packages, and audited 339 packages in 3s

88 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

そしてlib/posts.jsを開いて以下のようにimportする。

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

getPostData()の中でremarkを使っていかのように。

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

  // gray-matter で metadata セクションをパース
  const matterResult = matter(fileContents)

  // remarkでマークダウンをHTML文字列に変換
  const processedContent = await remark()
    .use(html)
    .process(matterResult.content)
  const contentHtml = processedContent.toString()

  // 今までidとメタデータだったところに本体のHTMLを追加
  return {
    id,
    contentHtml,
    ...matterResult.data
  }
}

getPostDataをasyncにしてるのはremarkawaitしないといけないからなのだそうです。(逆にmatterはawaitじゃなくて良いのかな、、とか思ったりも。。)

ってことで、pages/posts/[id].jsgetStaticPropsgetPostDataする時はawaitしてあげます、と。

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

そして、最後にcontentHTMLdangerouslySetInnerHTMLを使ってpages/posts/[id].jsのPostコンポーネントでレンダリングします、と。(デンジャラスリー。。)

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

↓こんな感じでちゃんと本体も表示されるようになりました!

ポスト

# ブログの各ページをイイ感じにする

pages/posts/[id].jsの中にtitleタグをPostのデータを使って追加していきましょうなヤツ。next/headというのを使うといい感じにメタタグとかやりくりしてくれるのかな的な気がしてくる風。ってことで、まずはnext/headのインポートから。

import Head from 'next/head'

そして、それを使って以下のように。

export default function Post({ postData }) {
  return (
    <Layout>
      {/* この <Head> タグを追加 */}
      <Head>
        <title>{postData.title}</title>
      </Head>

      {/* 既存のコードはそのまま */}
    </Layout>
  )
}

そして、日付のフォーマット。これはdate-fnsというライブラリを使うそうで、まずはnpmでインストール。

$ npm install date-fns

added 1 package, and audited 340 packages in 2s

89 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

今度はcomponents/date.jsというファイルを作って、そこにDateというコンポーネントを作りますよ、と。

import { parseISO, format } from 'date-fns'

export default function Date({ dateString }) {
  const date = parseISO(dateString)
  return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
}

なんかLが連続で4個出てくるのか馴染みがないけど、JavaでSimpleDateFormatとか使ってたおじさん的にはこんな感じなのねぇという気もする。

んま、このformat()に関してはココ見ときなって感じらしい。(LLLLは、『January, February, ..., December』にしてくれるのだそうです)

ってことでpages/posts/[id].jsを開いてDateコンポーネントをインポートしてやっていくよ、と。

import Date from '../../components/date'

export default function Post({ postData }) {
  return (
    <Layout>
      {/* 既存のコードはそのまま */}

      {/* {postData.date} をDateコンポーネントを使うやつにする */}
      <Date dateString={postData.date} />

      {/* 既存のコードはそのまま */}
    </Layout>
  )
}

↓ちゃんとそれっぽくなってますね〜

日付

# スタイルシートを追加していく

styles/utils.module.cssが既にあるのでpages/posts/[id].jsに適応していく感じ。

やることはpages/posts/[id].jsでそれをインポートして、

import utilStyles from '../../styles/utils.module.css'

h1とかdivをそれっぽく。

      <article>
        <h1 className={utilStyles.headingXl}>{postData.title}</h1>
        <div className={utilStyles.lightText}>
          <Date dateString={postData.date} />
        </div>
        <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
      </article>

↓なんだかイイ感じになりました 😃

CSS

# Indexページもイイ感じにしていく

pages/index.jsで各ブログポストにリンクをLinkコンポーネントを使ってやっていく感じ。

必要なものをインポートして、

import Link from 'next/link'
import Date from '../components/date'

元々あるliタグを置き換えて行く感じ

<li className={utilStyles.listItem} key={id}>
  <Link href={`/posts/${id}`}>
    <a>{title}</a>
  </Link>
  <br />
  <small className={utilStyles.lightText}>
    <Date dateString={date} />
  </small>
</li>

↓整いました。

Index

# 外部APIやデータベースへのクエリ

getStaticPropsgetStaticPathsのように外部からデータをフェッチできるのをgetAllPostIdsっていうので今回はgetStaticPathsから呼んでやりましたが、ファイルシステムのアクセスでなくて外部APIを呼んでもイイ、と。(Algoliaにインデクシングさせるならここがイイのかな〜。どっかから取ってきたデータを突っ込んでおけば、クライアントからInstantSearchで直接検索できる)

export async function getAllPostIds() {
  // Instead of the file system,
  // fetch post data from an external API endpoint
  const res = await fetch('..')
  const posts = await res.json()
  return posts.map(post => {
    return {
      params: {
        id: post.id
      }
    }
  })
}

で、今までも何回か出てきたけど、developmentとproductionで振る舞いが違う(リクエストごととビルドごと)ので気つけてや、と。(とは言え、開発モードでも毎回AlgoliaのIndexing叩いたら微妙なのかな…?まぁAlgoliaはIndexingの回数では課金されないからレコード数が多くなければお金の面では問題なさげだけど、、)

そして出てきましたFallback。

getStaticPathsfallback: falseにしたのはどんな意味があったのでしょうか?という。

  • fallbackfalse だったら、getStaticPathsで返ってこなかったページは404になります、と。
  • fallbacktrue だったら、getStaticPathsの振る舞いが変わります、と。
    • getStaticPathsから返ってきたヤツはHTMLがビルド時に生成される
    • ビルド時に生成されなかったパスは404になるわけではなく、Next.jsによってフォールバックされたヤツが表示される
    • Next.jsはバックグラウンドで密かにリクエストされたパスを静的に生成するんだそうで、次にそのパスにリクエストがあったらそのページが表示されるのだそうです
  • fallbackblocking だったら、新しいパスはサーバー側でgetStaticPropsを使ってレンダリングされてキャッシュされるので、そのパス毎に一回だけ的な。これtrueの時と似てる気がするけど違いは実際にHTMLを生成する/しないとか、、って感じなのかな。。

んま、この fallbacktrue の時と blocking の時については fallbackのドキュメント読みましょう、と。

次に、全部のルートをキャッチする。ここで...このドット3つがまた出てくる。っていうかこれを使うと全てのパスがキャッチできるのだそうで、例えば、pages/posts/[...id].jsっていう風にしておけば、posts/aだけでなくposts/a/bposts/a/b/cなんかもOKだぜ、と。

まぁ、なんかややこしいけど、これをやるならgetStaticPathsidを↓のようにしるんだそうですわ。

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

で、params.idはgetStaticPropsの中で↓こんな感じ。

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

これもドキュメント読め系だけど、おじさん的にはURLなければ404で、あんまりパスを複雑にしない方向で…とか思ってしまったり、しまわなかったりね。笑

他にもNext.jsのRouterにアクセスしたければ、 useRouter っていうのを next/routerからどうぞ、とか、あー、このカスタム404ページは便利かもですね。pages/404.jsを作って↓こんなヤツ

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

んま、そんな感じで、次回はNext.jsでのAPI Routesってことらしいです。

このエントリーをはてなブックマークに追加

Algolia検索からの流入のみConversionボタン表示