こんにちは、エンジニアのみーや(@miiya387)です。

Reactのフレームワーク「Next.js」への入門として、公式チュートリアルを何回かに分けてまとめていきます。
今回は、5. Dynamic Routes と 6. API Routesについてまとめていきます。
それ以前の章を知りたい方は各章に過去記事のリンクを付けておりますのでそちらからご覧ください。

  1. Create a Next.js App
  2. Navigate Between Pages
  3. Assets, Metadata, and CSS
  4. Pre-rendering and Data Fetching
  5. Dynamic Routes
  6. API Routes
  7. Deploying Your Next.js App

やってみる

5. Dynamic Routes

前回までは、getStaticPropsを利用して外部データを取得し、インデックスページの描画を行っていました。

今回はDynamic Routesを使って、ブログページへのパスを動的に作成していきます。

Implement getStaticPaths

まず初めに、以前作成したpages/posts/first-post.jsは不要になるのでここで削除しておきましょう。

次に、pages/posts 配下に [id].js ファイルを作成します。

[]で動的に指定するパラメータ名を囲むことで、リクエストされたURLに応じて動的にページが返されるようになります。これがDynamic Routesです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import Layout from '../../components/layout';
export default function Post() {
return <Layout>...</Layout>;
}
import Layout from '../../components/layout'; export default function Post() { return <Layout>...</Layout>; }
import Layout from '../../components/layout';

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

次に、lib/posts.js に以下を追加します。

getAllPostIdsはposts配下のファイル名(.mdを除く)一覧を返す関数です。

返却データにはオブジェクトでキーにidを持つデータが含まれている必要があります。(ファイル名にidを使用しているため)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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$/, ''),
},
};
});
}
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$/, ''), }, }; }); }
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/[id].js 内でimportして使用します。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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>
}
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> }
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>
}

次に、指定されたidでブログをレンダリングできるようにします。

lib/posts.js ファイルの末尾に次を追記します。getPostDataは、ブログのデータをidを元に返す関数です。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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,
};
}
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, }; }
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,
  };
}

次に、[id].jsのimport文を書き換え、getStaticPropsを追加します。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// import { getAllPostIds } from '../../lib/posts'
import { getAllPostIds, getPostData } from '../../lib/posts';
export async function getStaticPaths() {
const paths = getAllPostIds()
return {
paths,
fallback: false
}
}
export async function getStaticProps({ params }) {
const postData = getPostData(params.id)
return {
props: {
postData
}
}
}
// import { getAllPostIds } from '../../lib/posts' import { getAllPostIds, getPostData } from '../../lib/posts'; export async function getStaticPaths() { const paths = getAllPostIds() return { paths, fallback: false } } export async function getStaticProps({ params }) { const postData = getPostData(params.id) return { props: { postData } } }
// import { getAllPostIds } from '../../lib/posts'
import { getAllPostIds, getPostData } from '../../lib/posts';

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

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

これにより、getStaticPropsがブログデータをgetPostDataから受け取ってpropsとして返すようになります。

そして、Post関数を以下のように書き換えます。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
</Layout>
);
}
export default function Post({ postData }) { return ( <Layout> {postData.title} <br /> {postData.id} <br /> {postData.date} </Layout> ); }
export default function Post({ postData }) {
    return (
        <Layout>
            {postData.title}
            <br />
            {postData.id}
            <br />
            {postData.date}
        </Layout>
    );
}

これで各ブログページにアクセスしたらページが見れるようになるはずです。

しかし、私の場合は以下のエラーが発生しました。

gray-matterがなぜかCan’t resolve になっています。今までは何も起きていなかったのですが、エラーが出てしまったので改めてインストールしてサーバーを再起動します。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ npm install gray-matter
$ npm run dev
$ npm install gray-matter $ npm run dev
$ npm install gray-matter
$ npm run dev

見れるようになりました。

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

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

バッチリDynamic Routesでのページ描画ができていますね!

試しに posts配下に test.md を新規で作成してアクセスできるかも試してみましたが、しっかり表示されました。

http://localhost:3000/posts/test

しかし、まだブログのマークダウンコンテンツの描画ができていないようなので対応しましょう。

Render Markdown

マークダウンコンテンツを描画するために以下のライブラリをインストールします。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm install remark remark-html
npm install remark remark-html
npm install remark remark-html

lib/posts.js にimport文を追加します。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { remark } from 'remark';
import html from 'remark-html';
import { remark } from 'remark'; import html from 'remark-html';
import { remark } from 'remark';
import html from 'remark-html';

そして、getPostDataを以下のように書き換えます。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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,
};
}
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, }; }
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に変更しています。

また、[id].js内のgetPostDataの呼び出しもawaitに変更しておきましょう。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
export async function getStaticProps({ params }) {
const postData = await getPostData(params.id)
return {
props: {
postData
}
}
}
export async function getStaticProps({ params }) { const postData = await getPostData(params.id) return { props: { postData } } }
export async function getStaticProps({ params }) {
    const postData = await getPostData(params.id)
    return {
        props: {
            postData
        }
    }
}

さらに、[id].jsのPost関数を以下の内容に更新します。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
<br />
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</Layout>
);
}
export default function Post({ postData }) { return ( <Layout> {postData.title} <br /> {postData.id} <br /> {postData.date} <br /> <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} /> </Layout> ); }
export default function Post({ postData }) {
    return (
        <Layout>
            {postData.title}
            <br />
            {postData.id}
            <br />
            {postData.date}
            <br />
            <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
        </Layout>
    );
}

再度、各ブログページへアクセスするとマークダウンのコンテンツが表示されるようになりました。

dangerouslySetInnerHTMLでcontentHtmlをレンダリングすることでHTMLが生成できます。

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

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

チュートリアルでは、ここからさらにページ表示の改善を行なっていますが、Dynamic Routesについてはここまでで十分理解できたので、本記事では割愛して次の章に進みます。

6. API Routes

Creating API Routes

API Routes を使うことで、Next.js内にAPIエンドポイントを作成することができます。

Create a simple API endpoint

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
export default function handler(req, res) {
res.status(200).json({ text: 'Hello' });
}
export default function handler(req, res) { res.status(200).json({ text: 'Hello' }); }
export default function handler(req, res) {
  res.status(200).json({ text: 'Hello' });
}

http://localhost:3000/api/hello にアクセスすると指定したレスポンスが表示されました。

Next.js内でバックエンドの実装も可能になるのでとても便利ですね!

実際のAPIは別にあるとしても、バックエンドの実装を待たずにフロントエンドの実装を進めていきたい場面などではテストAPIの用意もNext.jsなら容易にできそうでとても助かりそうです!

まとめ

今回はNex.jsの公式チュートリアルをもとにDynamic Routes と API Routesについて触れてみました。

ファイル名を[]で囲むだけで動的ルートが適用できるのはとても簡単で便利でしたね!

Next.jsは標準で用意されている機能だけで十分開発を進められる印象が強いです。

ここまでのソースはgithubに上げたので興味のある方はぜひ一緒にNext.jsと仲良くなっていきましょう!

次回は最後の章である「Deploying Your Next.js App」をやってみます。

Join Us !

ウエディングパークでは、一緒に働く仲間を募集しています!
ご興味ある方は、お気軽にお問合せください(カジュアル面談から可)

採用情報を見る