LoginSignup
1
2

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

Last updated at Posted at 2023-01-30

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

ブログ投稿ページごとの動的ルートを定める

作例におけるブログ投稿ページごとの動的ルート(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の動的ルートです。なお、pages/posts/first-post.tsxはもう使わないので、削除しても構いません。

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をつぎのように定めます。戻り値はコメントに加えたとおりオブジェクトの配列です。

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 will fail.
(「Implement getStaticPaths」)

関数getAllPostIdsimportするのは、モジュールpages/posts/[id].tsxです。関数getStaticPathsから呼び出します。戻り値には、静的に生成するパスのリストを定めてください。

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にもとづいて取り出した投稿ファイルのメタデータです。

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].tsximportし、新たに加える関数getStaticPropsから呼び出します。関数の受け取る引数から取り出したparamsidを持っているので、getPostDataに渡してください。

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].tsxPostコンポーネントは、引数オブジェクトからブログ投稿データ(postData)が取り出せるようになりました。戻り値のJSXもこのデータにもとづいて、整えましょう。これで、ブログ投稿ページごとに、マークダウンファイルのデータが示されるはずです(図00)。

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

マークダウンファイルの本文データをレンダリングする

マークダウンファイルの投稿本文データをレンダリングするために、ライブラリremarkをインストールします。

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.
(「Render Markdown」)

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を用いて要素(<div>)に加えなければなりません。

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

ページのレイアウトを整える

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

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で変えます。インストールはつぎのとおりです。

npm install date-fns

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

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;

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

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の戻り値です。

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

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

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

ソース01■Next入門04 動的ルートを定める

React + TypeScript: Next入門シリーズ

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2