🚀

Next.js公式チュートリアル要点抜粋 Dynamic Routes

2022/01/29に公開

What You’ll Learn in This Lesson

Page Path Depends on External Data

  • Next.jsでは外部のデータを使用してページのパスを生成できる。

How to Statically Generate Pages with Dynamic Routes

サンプルのブログ投稿記事の場合、動的ルーティングは以下の仕様となる。

  • すべての記事は/posts/<id>のパスを持つ、<id>はトップレベルのpostsディレクトリー内のマックダウンファイル名。
  • サンプルのファイル名はssg-ssr.md とpre-rendering.mdのため、パスは/posts/ssg-ssr/posts/pre-renderingとなる。

Overview of the Steps

動的ルーティング(動的パス)を生成するための流れ:

  1. pages/postsの下に[id].jsというファイル(ページ)を生成する。[]はNext.jsでは動的に変わる部分を指す。
  2. pages/posts/[id].jsにページを作成するコードを書く。(いままで通り)
import Layout from '../../components/layout'

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

  1. 同じページ(ファイル)内に非同期関数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
}
  1. 最後にgetStaticPropsをもう一度実装する必要がある。渡されたidでブログ投稿のデータをfetchする役割を持つ。(引数paramsに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
}

export async function getStaticProps({ params }) {
  // Fetch necessary data for the blog post using params.id
}

全体構造図:

https://nextjs.org/static/images/learn/dynamic-routes/how-to-dynamic-routes.png

Implement getStaticPaths

getStaticPathsの実装例(チュートリアルの実装例):

  • ページに表示するコンポーネントの作成
// pages/posts/[id].js
import Layout from '../../components/layout'

export default function Post() {
  return <Layout>...</Layout>
}
  • マックダウンファイル名を抽出するヘルパー関数の作成
// lib/posts.js
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$/, '')
      }
    }
  })
}

getStaticPathsで使用するために戻り値のフォーマットを整形する必要がある。文字列の配列ではなく、オブジェクトの配列の形にしないといけない。また、オブジェクトにはparamsというキーを持たせる必要があり、さらに今回は[id].jsで示したようにidを取得する必要があるため、オブジェクト内にさらにidキーを持たせる必要がある。(コメントのデータ形式)さもなければgetStaticPathsは失敗する。

最後、getAllPostIdsgetStaticPaths内で呼び出すようにする。

// pages/posts/[id].js
import { getAllPostIds } from '../../lib/posts'

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

Postコンポネントの上に貼り付ける(下でも大丈夫!)

Implement getStaticProps

getStaticPathsid(ファイル名)を取得できたので、それらを使ってレンダリングすべき投稿のデータをfetchする仕組みがある。(すでに経験済みのgetStaticProps

getStaticPropsの実装例:

  • 指定idで投稿データを取得するヘルパー関数の作成,
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
  }
}
  • getStaticPropsの実装

pages/posts/[id].js内の

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

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

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

に変更する。**getStaticProps**内ではヘルパー関数**getStaticPaths**を使用している。

  • 最後、コンポネント内に表示の内容を追加して、完了~
// pages/posts/[id].js
export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
    </Layout>
  )
}

Summary

全体構造図:

https://nextjs.org/static/images/learn/dynamic-routes/how-to-dynamic-routes.png

Render Markdown

  • markdownコンテンツを表示するためにremarkライブラリーが必要だ。
npm install remark remark-html
  • lib/posts.jsにライブラリーをimportする。
// lib/posts.js
import { remark } from 'remark'
import html from 'remark-html'
  • 同じファイルのgetPostData()にremarkを使用してmarkdownをHTMLに変換する処理を追加する。
// lib/posts.js
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
  }
}
  • remarkを呼び出すためにawaitを使うので、getPostDataの前にasyncキーワードを追加する必要がある。getPostDataを使用するgetStaticPropsのところも非同期仕様に置き換える。
// pages/posts/[id].js
export async function getStaticProps({ params }) {
  // Add the "await" keyword like this:
  const postData = await getPostData(params.id)
  // ...
}
// pages/posts/[id].js
export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
      <br />
      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
    </Layout>
  )
}

結果:

Polishing the Post Page
投稿ページをもっときれいに仕上げましょう

  • まずタイトルを追加する。next/headをimportし、titleタグを使用してPostコンポネントをアップグレードしょう!
// Add this import
import Head from 'next/head'

export default function Post({ postData }) {
  return (
    <Layout>
      {/* Add this <Head> tag */}
      <Head>
        <title>{postData.title}</title>
      </Head>

      {/* Keep the existing code here */}
    </Layout>)
}

Formatting the Date
日付をフォーマットする

  • まずはdate-fnsをインストールする。
npm install date-fns
  • 次にファイル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>
}

※ 日付のフォーマット形式をここを参照してください。

  • pages/posts/[id].jsDateコンポネントを追加して、{postData.date}と置き換えてください。
// Add this import
import Date from '../../components/date'

export default function Post({ postData }) {
  return (
    <Layout>
      {/* Keep the existing code here */}

      {/* Replace {postData.date} with this */}
      <Date dateString={postData.date} />

      {/* Keep the existing code here */}
    </Layout>
  )
}

http://localhost:3000/posts/pre-renderingにアクセスしたら日付はJanuary 1, 2020に変わってるはずだ。

Adding CSS
cssを追加する

  • 最後にcssを追加して見栄えをよくしましょう。前に追加したstyles/utils.module.cssを使用する。pages/posts/[id].jsを開きCSSファイルをimportする行を追加し、Postコンポネントを以下のように書き換える。
// Add this import at the top of the file
import utilStyles from '../../styles/utils.module.css'

export default function Post({ postData }) {
  return (
    <Layout>
      <Head>
        <title>{postData.title}</title>
      </Head>
      <article>
        <h1 className={utilStyles.headingXl}>{postData.title}</h1>
        <div className={utilStyles.lightText}>
          <Date dateString={postData.date} />
        </div>
        <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
      </article>
    </Layout>
  )
}

結果:

Polishing the Index Page
ホームページを磨いて行こう

  • ホームページ(pages/index.js)をアップデートする第一歩は、Linkコンポネントを使って個々の投稿にリンクを追加することだ。pages/index.jsを開き、LinkおよびDateのために以下のコンポネントをimportする。
import Link from 'next/link'
import Date from '../components/date'
  • 次にHomeコンポネントの底辺の部分を以下のように書き換えてください。
<li className={utilStyles.listItem} key={id}>
  <Link href={`/posts/${id}`}>
    <a>{title}</a>
  </Link>
  <br />
  <small className={utilStyles.lightText}>
    <Date dateString={date} />
  </small>
</li>
  • http://localhost:3000にアクセスすると記事のタイトルはリンクに変え、日付もいい感じフォーマットされる。うまく行かない場合はサンプルコードと比較してみてください。

Dynamic Routes Details
動的ルーティングの詳細

Fetch External API or Query Database
外部APIおよびデータベースへのアクセス

  • getStaticPropsと同様、getStaticPathsにもデータをfetchする処理を書ける。以下のコードではgetAllPostIdsgetStaticPaths内で使用)外部APIエンドポイントにアクセスする例を示す。
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 v.s. Production
開発モード vs 本番モード

  • 開発モード(npm run devあるいは yarn dev)ではgetStaticPathsはすべてのリクエスト時に実行される。
  • 本番モードではgetStaticPathsはビルド時にのみ実行される。

Fallback

Catch-all Routes
すべてのルートをキャッチする

  • 動的ルーティングはブラケットに三点リーダー(...)を追加することですぺてのパスをキャッツできるようになります。たとえばpages/posts/[...id].jsで記述する場合は、/posts/aも、 /posts/a/bも、 /posts/a/b/cなどなどはすべてヒットする。
  • そうしたらgetStaticPaths内でidになり得る値を配列で返す必要がある。
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']
}

Router

  • もしあなたはNext.jsのルーターオブジェクトにアクセスしたいなら、next/routerからuseRouterをimportすればいい。

404 Pages

  • 404ページをカスタムしたい場合はpages/404.jsを作成する。このファイルはビルド時に静的生成される。
// pages/404.js
export default function Custom404() {
  return <h1>404 - Page Not Found</h1>
}

Discussion