React + TypeScript: Next入門04 動的ルートを定める

Next.jsチュートリアルの公式作例を、TypeScriptも加えて、一からつくってみるシリーズの最終回です。プログ投稿ごとのパスをデータから動的につくり、静的に生成した投稿ページにトップページからリンクします。でき上がりがこの[Next.js公式作例](https://next-learn-starter.vercel.app/)です。


# ブログ投稿ページごとの動的ルートを定める
作例におけるブログ投稿ページごとの動的ルート([Dynamic Routes](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes))のつくり方は、つぎのとおりです。

* 各投稿のパスは`/posts/<id>`とします。
    - `<id>`はトップレベルの`posts`ディレクトリに収めたマークダウンファイル名です(拡張子`.md`は削除)。
* マークダウンファイル名からパスは、つぎのように決まります。
    - `ssg-ssr.md`のパス: `/posts/ssg-ssr`
    - `pre-rendering.md`のパス: `/posts/pre-rendering`

まずつくるのは、つぎのような各投稿ページのモジュール`pages/posts/[id].tsx`です(`Post`コンポーネント戻り値のJSXは仮なので、あとで完成させます)。角かっこ`[]`でくくられたページが、Next.jsの[動的ルート](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes)です。なお、`pages/posts/first-post.tsx`はもう使わないので、削除しても構いません。

```react:pages/posts/[id].tsx
import type { FC } from 'react';
import Layout from '../../components/layout';

const Post: FC = () => {
	return <Layout>...</Layout>;
};
export default Post;
```

つぎに、モジュール`lib/posts.ts`に新たな関数`getAllPostIds`をつぎのように定めます。戻り値はコメントに加えたとおりオブジェクトの配列です。

```react:lib/posts.ts
export const getAllPostIds = () => {
	const fileNames = fs.readdirSync(postsDirectory);
	/* つぎのような配列が返される:
	[
		 {
			 params: {
				 id: 'ssg-ssr'
			 }
		 },
		 {
			 params: {
				 id: 'pre-rendering'
			 }
		 }
	] */
	return fileNames.map((fileName) => {
		return {
			params: {
				id: fileName.replace(/\.md$/, ''),
			},
		};
	});
};
```

関数`getAllPostIds`の戻り値は、プロパティ`params`を備えたオブジェクトの配列でなければなりません。そして、動的ルートの`[id]`に用いるファイル名を`id`プロパティに収めます。

> **Important**: The returned list is *not* just an array of strings — it **must** be an array of objects that look like the comment above. Each object must have the `params` key and contain an object with the `id` key (because we’re using `[id]` in the file name). Otherwise, [`getStaticPaths`](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-paths) will fail.
(「[Implement getStaticPaths](https://nextjs.org/learn/basics/dynamic-routes/implement-getstaticpaths)」)

関数`getAllPostIds`を`import`するのは、モジュール`pages/posts/[id].tsx`です。関数[`getStaticPaths`](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-paths)から呼び出します。戻り値には、静的に生成するパスのリストを定めてください。

```react:pages/posts/[id].tsx
import { getAllPostIds } from '../../lib/posts';

export const getStaticPaths = async () => {
	// idに対して可能な値のリストを返す
	const paths = getAllPostIds();
	return {
		paths,
		fallback: false,
	};
};
```

さらに、モジュール`lib/posts.ts`に新たな関数`getPostData`を加えましょう。戻り値は、引数に受け取った`id`にもとづいて取り出した投稿ファイルのメタデータです。

```react:lib/posts.ts
export const getPostData = (id: string) => {
	const fullPath = path.join(postsDirectory, `${id}.md`);
	const fileContents = fs.readFileSync(fullPath, 'utf8');
	// gray-matterで投稿ファイルのメタデータを取り出す
	const matterResult = matter(fileContents);
	// データにidを加える
	return {
		id,
		...matterResult.data,
	};
};
```

`getPostData`はモジュール`pages/posts/[id].tsx`が`import`し、新たに加える関数`getStaticProps`から呼び出します。関数の受け取る引数から取り出した`params`が`id`を持っているので、`getPostData`に渡してください。

```react:pages/posts/[id].tsx
import type { GetStaticProps } from 'next';

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

export const getStaticProps: GetStaticProps = async ({ params }) => {
	// params.idを用いてブログ投稿のデータを取り出す
	const postData = getPostData(params?.id as string);
	return {
		props: {
			postData,
		},
	};
};
```

モジュール`pages/posts/[id].tsx`の`Post`コンポーネントは、引数オブジェクトからブログ投稿データ(`postData`)が取り出せるようになりました。戻り値のJSXもこのデータにもとづいて、整えましょう。これで、ブログ投稿ページごとに、マークダウンファイルのデータが示されるはずです(図00)。

```react:pages/posts/[id].tsx
type PostData = {
	postData: {
		id: string;
		title: string;
		date: string;
		contentHtml: string;
	}
};

// const Post: FC = () => {
const Post: FC<PostData> = ({ postData }) => {
	// return <Layout>...</Layout>;
	return (
		<Layout>
			{postData.title}
			<br />
			{postData.id}
			<br />
			{postData.date}
		</Layout>
	);
};
```

#### 図001■ブログページごとに投稿データが表示される
![qiita_2301004_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/e659b991-5585-d741-2a98-4a53b4bf48c7.png)


# マークダウンファイルの本文データをレンダリングする
マークダウンファイルの投稿本文データをレンダリングするために、ライブラリ[`remark`](https://github.com/remarkjs/remark)をインストールします。

```terminal
npm install remark remark-html
```

マークダウンのデータの解析を担うのは、`lib/posts.ts`モジュールです。関数`getPostData`から`remark`を、以下のように`await`で非同期呼び出ししてください。したがって、関数には`async`を加えなければなりません。

> **Important**: We added the **`async`** keyword to `getPostData` because we need to use `await` for `remark`. `async`/`await` allow you to fetch data [asynchronously](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/async_function).
(「[Render Markdown](https://nextjs.org/learn/basics/dynamic-routes/render-markdown)」)

```react:lib/posts.ts
import { remark } from 'remark';
import html from 'remark-html';

// export const getPostData = (id: string) => {
export const getPostData = async (id: string) => {

	// remarkを使ってマークダウンをHTML文字列に変換する
	const processedContent = await remark()
		.use(html)
		.process(matterResult.content);
	const contentHtml = processedContent.toString();
	// 戻り値にcontentHtmlのデータを加える
	return {

		contentHtml,

	};
};
```

モジュール`pages/posts/[id].tsx`の関数`getStaticProps`からの`getPostData`の呼び出しにも`await`キーワードを加えてください。`Post`コンポーネントが取り出した投稿本文のデータ(`postData.contentHtml`)は、[`dangerouslySetInnerHTML`](https://ja.react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html)を用いて要素(`<div>`)に加えなければなりません。

```react:pages/posts/[id].tsx
export const getStaticProps: GetStaticProps = async ({ params }) => {
	// つぎのようにawaitキーワードを加える
	// const postData = getPostData(params?.id as string);
	const postData = await getPostData(params?.id as string);

};

const Post: FC<PostData> = ({ postData }) => {
	return (
		<Layout>

			<br />
			<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
		</Layout>
	);
};
```

これで、ブログ投稿ページに本文データがHTMLテキストで表示されるようになりました(図002)。

#### 図002■ブログ投稿ページに本文が示された
![qiita_2301004_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/1f10b09d-22e9-ae0d-4126-0baf0e07f4ff.png)


# ページのレイアウトを整える
仕上げにページのレイアウトを整えましょう。モジュール`pages/posts/[id].tsx`には、`Head`コンポーネントで`<title>`を加えます。ブラウザのタブに表示されるのは、投稿ページごとのタイトル(`postData.title`)です。

```react:pages/posts/[id].tsx
// Add this import
import Head from 'next/head';

const Post: FC<PostData> = ({ postData }) => {
	return (
		<Layout>
			{/* Headコンポーネントで<title>を加える */}
			<Head>
				<title>{postData.title}</title>
			</Head>

		</Layout>
	);
};
```

日付の表示形式は、ライブラリ[`date-fns`](https://date-fns.org/)で変えます。インストールはつぎのとおりです。

```terminal
npm install date-fns
```

このライブラリ`date-fns`を用いるモジュールは、新たに`components/date.tsx`としてつぎのように定めてください。

```react:components/date.tsx
import { parseISO, format } from 'date-fns';
import type { FC } from 'react';

type Props = {
	dateString: string;
};
const Date: FC<Props> = ({ dateString }) => {
	const date = parseISO(dateString);
	return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>;
}
export default Date;
```

コンポーネント`Date`は`pages/posts/[id].tsx`から`import`して、日付データ(`postData.date`)の文字列を指定した表示形式にして差し込みましょう。

```react:pages/posts/[id].tsx
// importを加える
import Date from '../../components/date';

const Post: FC<PostData> = ({ postData }) => {
	return (
		<Layout>

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

		</Layout>
	);
};
```

さらに、CSSモジュール`styles/utils.module.css`から読み込んだCSS(`utilStyles`)を与えたのが、つぎの`Post`コンポーネントのJSXの戻り値です。

```react:pages/posts/[id].tsx
// importを加える
import utilStyles from '../../styles/utils.module.css';

const Post: FC<PostData> = ({ postData }) => {
	return (
		<Layout>

			{/* {postData.title}
			<br />
			{postData.id}
			<br />
			<Date dateString={postData.date} />
			<br />
			<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} /> */}
			<article>
				<h1 className={utilStyles.headingXl}>{postData.title}</h1>
				<div className={utilStyles.lightText}>
					<Date dateString={postData.date} />
				</div>
				<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
			</article>
		</Layout>
	);
};
```

ブログ投稿ページのレイアウトは、つぎのように整えられました(図003)。

#### 図003■レイアウトが整えられたブログ投稿ページ
![qiita_2301004_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/7fc5168c-218e-456f-b430-79d745eb31d7.png)

最後は、トップレベルのモジュール`pages/index.tsx`です。コンポーネント`Link`と`Date`を`import`したうえで、`Home`コンポーネントが返すJSXの`<li>`要素はつぎのように書き替えてください。

```react:pages/index.tsx
import Link from 'next/link';
import Date from '../components/date';

export default function Home({ allPostsData }: Props) {
	return (
		<Layout home>

			<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>

				<ul className={utilStyles.list}>
					{allPostsData.map(({ id, date, title }) => (
						/* <li className={utilStyles.listItem} key={id}>

						</li> */
						<li className={utilStyles.listItem} key={id}>
							<Link href={`/posts/${id}`}>{title}</Link>
							<br />
							<small className={utilStyles.lightText}>
								<Date dateString={date} />
							</small>
						</li>
					))}
				</ul>
			</section>
		</Layout>
	);
}
```

各ブログ投稿ページへのリンクが加わり、日付の表示形式も整えられたでしょう。これで、Next.js公式チュートリアルと同じ作例ができ上がりました。アプリケーションの具体的なコードについては、以下のソース01をご参照ください。

#### 図004■レイアウトが整えられたトップページ
![qiita_2301004_004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/3d0b47cc-f345-fb8b-64d1-e5f25608b447.png)

#### ソース01■Next入門04 動的ルートを定める
https://github.com/FumioNonaka/react-typescript-next-example/tree/react-typescript-next-example-04


## React + TypeScript: Next入門シリーズ

* [Next入門01 チュートリアルの作例を一からつくってみる](https://qiita.com/FumioNonaka/items/37ad26dab7e5051f672e)
* [Next入門02 イメージとメタデータおよびCSSを扱う](https://qiita.com/FumioNonaka/items/4df0bfe8538eb346cad5)
* [Next入門03 プリレンダリングとデータ取得](https://qiita.com/FumioNonaka/items/82fd364d746de5206749)
* Next入門04 動的ルートを定める