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
はもう使わないので、削除しても構いません。
import type { FC } from 'react';
import Layout from '../../components/layout';
const Post: FC = () => {
return <Layout>...</Layout>;
};
export default Post;
つぎに、モジュールlib/posts.ts
に新たな関数getAllPostIds
をつぎのように定めます。戻り値はコメントに加えたとおりオブジェクトの配列です。
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 theid
key (because we’re using[id]
in the file name). Otherwise,getStaticPaths
will fail.
(「Implement getStaticPaths」)
関数getAllPostIds
をimport
するのは、モジュールpages/posts/[id].tsx
です。関数getStaticPaths
から呼び出します。戻り値には、静的に生成するパスのリストを定めてください。
import { getAllPostIds } from '../../lib/posts';
export const getStaticPaths = async () => {
// idに対して可能な値のリストを返す
const paths = getAllPostIds();
return {
paths,
fallback: false,
};
};
さらに、モジュールlib/posts.ts
に新たな関数getPostData
を加えましょう。戻り値は、引数に受け取ったid
にもとづいて取り出した投稿ファイルのメタデータです。
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
に渡してください。
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)。
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■ブログページごとに投稿データが表示される
マークダウンファイルの本文データをレンダリングする
マークダウンファイルの投稿本文データをレンダリングするために、ライブラリremark
をインストールします。
npm install remark remark-html
マークダウンのデータの解析を担うのは、lib/posts.ts
モジュールです。関数getPostData
からremark
を、以下のようにawait
で非同期呼び出ししてください。したがって、関数にはasync
を加えなければなりません。
Important: We added the
async
keyword togetPostData
because we need to useawait
forremark
.async
/await
allow you to fetch data asynchronously.
(「Render Markdown」)
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>
)に加えなければなりません。
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■ブログ投稿ページに本文が示された
ページのレイアウトを整える
仕上げにページのレイアウトを整えましょう。モジュールpages/posts/[id].tsx
には、Head
コンポーネントで<title>
を加えます。ブラウザのタブに表示されるのは、投稿ページごとのタイトル(postData.title
)です。
// 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
としてつぎのように定めてください。
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
)の文字列を指定した表示形式にして差し込みましょう。
// 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の戻り値です。
// 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■レイアウトが整えられたブログ投稿ページ
最後は、トップレベルのモジュールpages/index.tsx
です。コンポーネントLink
とDate
をimport
したうえで、Home
コンポーネントが返すJSXの<li>
要素はつぎのように書き替えてください。
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■レイアウトが整えられたトップページ
ソース01■Next入門04 動的ルートを定める
React + TypeScript: Next入門シリーズ
- Next入門01 チュートリアルの作例を一からつくってみる
- Next入門02 イメージとメタデータおよびCSSを扱う
- Next入門03 プリレンダリングとデータ取得
- Next入門04 動的ルートを定める