公式チュートリアルでNext.jsに入門してみた (3) 〜Dynamic Routes、API Routes編〜

2022.01.04

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、CX事業本部 IoT事業部の若槻です。

フロントエンドフレームワークNext.jsへの入門のために、次の公式チュートリアルを数回のシリーズに分けてこなしていき、基本的な機能に触れていこうと思います。

やってみた

本記事(3)では、チュートリアルのうち「Dynamic Routes」および「API Routes」をやっていき、Next.jsのDynamic RoutesおよびAPI Routesについて触れていきます。

Dynamic Routes

前回までの実装では外部データを使用してブログのインデックスページを作成しました。

ここではDynamic Routesを使用してブログページへのパスを作成します。

Implement getStaticPaths

まず、pages/posts/first-post.jsファイルはこの後の手順では不要なので削除します。

次に、lib/posts.jsファイルの末尾に以下の記述を追記します。postsディレクトリ配下のファイル名一覧を返すgetAllPostIds関数です。

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$/, '')
      }
    }
  })
}

pageディレクトリ配下にpostsディレクトリを作成します。

pages/posts配下に次の通り[id].jsファイルを作成します。この[で始まり]で終わるページにより、リクエストされたURLに応じてにより動的にページが返されるようになります。これがDynamic Routesです。

pages/posts/[id].js

import Layout from '../../components/layout'
import { getAllPostIds } from '../../lib/posts'

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

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

このgetStaticPathsでは、次の形式でpathsを返すことにより、どのパスでPre-renderingがされるのかが決定されるようになります。

return {
  paths: [
    { params: { id: '1' } },
    { params: { id: '2' } }
  ],
  fallback: ...
}

Implement getStaticProps

ここではidをもとにブログポストをレンダリングできるようにしていきます。

lib/posts.jsファイルの末尾に次の記述を追記します。getPostData関数はブログのポストデータをidをもとに返します。

lib/posts.js

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].jsファイルで次の行の記述を、

pages/posts/[id].js

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

次の記述で置き換えます。

pages/posts/[id].js

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

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

これによりgetStaticProps内のgetPostData関数がブログのポストデータをpropsとして返すようになります。

そしてpages/posts/[id].jsファイル内のPostを次の記述で置き換えます。

pages/posts/[id].js

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

ブログポストのURLにアクセスしたらそれぞれのページを開けるようになりました!

http://localhost:3000/posts/ssg-ssr

http://localhost:3000/posts/pre-rendering

Render Markdown

マークダウンのコンテンツをレンダリングするためにremarkライブラリをインストールします。

$ npm install remark remark-html

lib/posts.jsファイルの冒頭に次の記述を追記します。

lib/posts.js

import { remark } from 'remark'
import html from 'remark-html'

同じくlib/posts.jsファイルでgetPostData()関数を次の通り更新します。

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関数としています。

よってpages/posts/[id].jsファイルのgetStaticPropsで、getPostDataをawaitで実行するように更新します。

pages/posts/[id].js

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

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

さらにpages/posts/[id].jsファイルのPostコンポーネントを次のように更新します。

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>
  )
}

dangerouslySetInnerHTMLを使用してcontentHtmlをレンダリングすることにより、Reactから安全にHTMLを生成できます。(dangerouslyと付いているのは、コードからHTMLを生成するのはXSSの脆弱性を生む危険があることを認識させるためだそうです。)

ここでブログポストのURLにアクセスしたら次のようなエラーとなっていました。

前回と同じエラーですね。またnpm upgradeして開発サーバーを再起動します。

するとそれぞれのページを正常に開けました。マークダウンから変換されたコンテンツも見れています!

http://localhost:3000/posts/ssg-ssr

http://localhost:3000/posts/pre-rendering

Polishing the Post Page

ブログポストのページを改善します。

Adding title to the Post Page

ここではポストページにブログタイトルを付けるようにします。

pages/posts/[id].jsファイルの冒頭に次の記述を追加します。

pages/posts/[id].js

import Head from 'next/head'

さらにpages/posts/[id].jsファイルで、PostComponentで次のように{postData.title}<title><Head>で囲みます。

pages/posts/[id].js

export default function Post({ postData }) {
  return (
    <Layout>
      <Head>
        <title>{postData.title}</title>
      </Head>
Formatting the Date

ここでは、ブログの日付のフォーマットを行います。

date-fnsをインストールします。(必要に応じて再度npm upgradeも実行します。)

$ npm install date-fns

componentsディレクトリ配下に次の内容でdate.jsファイルを作成します。

components/date.js

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].jsファイルに次の記述を追加します。

pages/posts/[id].js

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

pages/posts/[id].jsファイルで、PostComponentを次のように{postData.date}Dateを使用するようにします。

pages/posts/[id].js

export default function Post({ postData }) {
  return (
    <Layout>
      <Head>
        <title>{postData.title}</title>
      </Head>
      <br />
      <Date dateString={postData.date} />

http://localhost:3000/posts/pre-renderingにアクセスすると、日付の形式をフォーマットできていますね!

Adding CSS

ここでは、ポストページをCSSによりスタイリングします。

pages/posts/[id].jsファイルに次の記述を追加します。

pages/posts/[id].js

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

pages/posts/[id].jsファイルで、PostComponentを次のように更新します。

pages/posts/[id].js

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>
  )
}

http://localhost:3000/posts/pre-renderingにアクセスすると、タイトルと日付のスタイリングができていますね!

Polishing the Index Page

インデックスのページを改善します。

pages/index.jsファイルの冒頭に次の記述を追記します。

pages/index.js

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

pages/index.jsファイルのHomeComponentで、<li>の内容を次の通り更新します。

pages/index.js

export default function Home({ allPostsData }) {
  return (
    <Layout home>
      {/* Keep the existing code here */}

      {/* Add this <section> tag below the existing <section> tag */}
      <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
        <h2 className={utilStyles.headingLg}>Blog</h2>
        <ul className={utilStyles.list}>
          {allPostsData.map(({ id, date, title }) => (
            <li className={utilStyles.listItem} key={id}>
              <Link href={`/posts/${id}`}>
                <a>{title}</a>
              </Link>
              <br />
              <small className={utilStyles.lightText}>
                <Date dateString={date} />
              </small>
            </li>
          ))}
        </ul>
      </section>
    </Layout>
  )
}

http://localhost:3000/にアクセスすると、インデックスで各ブログポストへのリンクと日付を表示させられました!

API Routes

Creating API Routes

API Routesを使用することにより、Next.jsアプリケーション内にAPIエンドポイントを作成することができます。

pagesディレクトリ配下にapiディレクトリを作成します。

pages/apiディレクトリ配下に、hello.jsファイルを次の通り作成します。

pages/api/hello.js

export default function handler(req, res) {
  res.status(200).json({ text: 'Hello' })
}

http://localhost:3000/api/helloにアクセスすると、APIエンドポイントで取得した内容が表示できました!

Next.jsのフレームワーク単体でフロントエンドとバックエンドAPIの実装ができるというのは便利ですね!

おわりに

公式チュートリアルでNext.jsに入門してみた (3) 〜Dynamic Routes、API Routes編〜 でした。

次回はこちらです。

以上