# Next.jsのチュートリアル、その4
Next.jsのチュートリアル、その1、Next.jsのチュートリアル、その2、Next.jsのチュートリアル、その3に続いて4つ目です。
# Dynamic Routes
indexページをブログのデータを使ってやってきましたが、それぞれのブログ記事のページはまだありません。ってことで、これらをdynamic routesを使ってやっていきましょうの回。
ここで学ぶことは、
getStaticPaths
を使ってdynamic routesを使って静的ページを生成していく(字面だけ追うと静的/動的がわかりにくいですね。笑)getStaticPaths
を使ってそれぞれのブログ記事のデータを取得remark
を使ってマークダウンをレンダリングする- pretty-print date strings(ってなんじゃらほい?現時点で分かっていませんがw、日付をいい感じのフォーマットでやりくりしてくれる文字列用の便利なヤツかな?)
- ページをdynamic routesにリンクさせる
- dynamic routesに関する耳打ちな情報
# 外部データに依存したページのパス
前回のレッスンではgetStaticProps
を使ってデータを取得してindexページにレンダリングしたけど、今回は外部データによってページのパスを設定しつつそれぞれの静的ページを動的にNext.jsの中で出力するところをやっていきます、と。
で、どうやってDynamic Routesを使って静的ページを作っていくかというと、
/posts/<id>
というパスにするとして、id
はマークダウンファイルのファイル名にすると。で、そのファイルはposts
ディレクトリの下にあるファイル達- 今は
ssg-ssr.md
とpre-rendering.md
があるわけですが、それを/posts/ssg-ssr
と/posts/pre-rendering
という感じにしてやる
全体像としては、[id].js
をpages/posts
に作っていきます、と。で、この[]がNext.jsにおけるdynamic routesです。ってことで、pages/posts/[id].js
を以下のようにしてやるイメージ。今まで作ってきたページと同様に。
import Layout from '../../components/layout'
export default function Post() {
return <Layout>...</Layout>
}
そして、これから新しいヤツが出てきてgetStaticPaths
っていうファンクション。ここではid
に突っ込むリストを返す必要があります、と。
import Layout from '../../components/layout'
export default function Post() {
return <Layout>...</Layout>
}
export async function getStaticPaths() {
// Return a list of possible value for id
}
で、getStaticProps
がまた出てきたけど、これはid
を元にデータを取得しにいくヤツ。で、params
にはid
が入ってくるよ、と(ファイルが[id].js
なので)。
import Layout from '../../components/layout'
export default function Post() {
return <Layout>...</Layout>
}
export async function getStaticPaths() {
// Return a list of possible value for id
}
export async function getStaticProps({ params }) {
// params.idを使って必要なデータを取ってくる
}
で、いよいよ実装ですよ、と。
- まずは
pages/posts
に[id].js
を作っていって first-post.is
は要らないので消していきます、と。
ってことで、まずはpages/posts[id].js
を以下のように。Layoutの...は後から実装していきます。
import Layout from '../../components/layout'
export default function Post() {
return <Layout>...</Layout>
}
で、次にlib/posts.js
を開いて、getAllPostIds
っていうファンクションを下部に追加。これによってposts
ディレクトリのファイル名のリストを返してくれる。その際.md
の部分は取り除かれる。
export function getAllPostIds() {
const fileNames = fs.readdirSync(postsDirectory)
return fileNames.map(fileName => {
return {
params: {
id: fileName.replace(/\.md$/, '')
}
}
})
}
ここで、Importantって書いてあって、このリターンされるリストは単純に文字列の配列っていうわけではなくて↓のように、それぞれがparamsというキーを持って、そこにidがキーになっているオブジェクトがないとダメということ。これによってファイル名の[id]が活用できるということで、こうなってないとgetStaticPathsが失敗してしまいますよ、と。
[
{
params: {
id: 'ssg-ssr'
}
},
{
params: {
id: 'pre-rendering'
}
}
]
ってことで、このgetAllPostIds
ファンクションをインポートしてgetStaticPaths
の中で使っていきましょう。
getAllPostIdsをインポートして
import {getAllPostIds} from '../../lib/posts'
getStaticPaths()ファンクションを実装
import { getAllPostIds } from '../../lib/posts'
export async function getStaticPaths() {
const paths = getAllPostIds()
return {
paths,
fallback: false
}
}
paths
はgetAllPostIds()
から返されたパス(というかファイル名)の配列で、fallback: false
については後ほど説明、と。
ってことで、ほぼ完成したけど、getStaticProps
やんなきゃね。
id
を元にして必要なブログのデータを取ってくる必要があるので、lib/posts.js
にgetPostData
ファンクションを追加していきます。これはid
によってブログポストのデータを取得してくるものです。
export function getPostData(id) {
const fullPath = path.join(postsDirectory, `${id}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// gray-matterでmetadetaをパース
const matterResult = matter(fileContents)
// Combine the data with the id
return {
id,
...matterResult.data
}
}
この...
はSpread operatorと呼ばれていて、gray-matterから返ってきたdata(---で囲まれたところが、dataというkeyの中でそれっぽくなって返ってくる)を展開してといった動作をするのだそうで、idと一緒にそのコンテンツを返してあげましょう的な。
そして、pages/posts/[id].js
を開いてgetAllPostIdsだけでなく上記のgetPostDataもimportするように変更してそれを使うように。
import { getAllPostIds, getPostData } from '../../lib/posts'
export async function getStaticProps({ params }) {
const postData = getPostData(params.id)
return {
props: {
postData
}
}
}
それぞれのブログポストのページはgetPostData
ファンクションをgetStaticProps
で呼んで、それをページコンポーネントに返す感じで。
ってことで、PostコンポーネントをpostData
を使う形に改修しやすよ、と。pages/posts/[id].js
で、Postコンポーネントのところを以下のように。
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
</Layout>
)
}
↓こんな感じになりました〜
ってことで、dynamic routesっていうのはこうやってやるんだぜ的な感じで。
とは言え、マークダウンの本体部分はまだ表示されてないので、次にそれをやっていきます。
# Markdownのレンダリング
マークダウンのコンテンツをレンダリングするのにremark
というライブラリを使うので、まずはnpmでインストール。
a$ npm install remark remark-html
added 52 packages, and audited 339 packages in 3s
88 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
そしてlib/posts.js
を開いて以下のようにimportする。
import remark from 'remark'
import html from 'remark-html'
getPostData()
の中でremark
を使っていかのように。
export async function getPostData(id) {
const fullPath = path.join(postsDirectory, `${id}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// gray-matter で metadata セクションをパース
const matterResult = matter(fileContents)
// remarkでマークダウンをHTML文字列に変換
const processedContent = await remark()
.use(html)
.process(matterResult.content)
const contentHtml = processedContent.toString()
// 今までidとメタデータだったところに本体のHTMLを追加
return {
id,
contentHtml,
...matterResult.data
}
}
getPostDataをasyncにしてるのはremark
をawait
しないといけないからなのだそうです。(逆にmatterはawaitじゃなくて良いのかな、、とか思ったりも。。)
ってことで、pages/posts/[id].js
のgetStaticProps
もgetPostData
する時はawait
してあげます、と。
export async function getStaticProps({ params }) {
// awaitを追加
const postData = await getPostData(params.id)
// ...
}
そして、最後にcontentHTML
をdangerouslySetInnerHTML
を使ってpages/posts/[id].js
のPostコンポーネントでレンダリングします、と。(デンジャラスリー。。)
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
<br />
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</Layout>
)
}
↓こんな感じでちゃんと本体も表示されるようになりました!
# ブログの各ページをイイ感じにする
pages/posts/[id].js
の中にtitle
タグをPostのデータを使って追加していきましょうなヤツ。next/head
というのを使うといい感じにメタタグとかやりくりしてくれるのかな的な気がしてくる風。ってことで、まずはnext/head
のインポートから。
import Head from 'next/head'
そして、それを使って以下のように。
export default function Post({ postData }) {
return (
<Layout>
{/* この <Head> タグを追加 */}
<Head>
<title>{postData.title}</title>
</Head>
{/* 既存のコードはそのまま */}
</Layout>
)
}
そして、日付のフォーマット。これはdate-fns
というライブラリを使うそうで、まずはnpmでインストール。
$ npm install date-fns
added 1 package, and audited 340 packages in 2s
89 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
今度はcomponents/date.js
というファイルを作って、そこにDateというコンポーネントを作りますよ、と。
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>
}
なんかLが連続で4個出てくるのか馴染みがないけど、JavaでSimpleDateFormatとか使ってたおじさん的にはこんな感じなのねぇという気もする。
んま、このformat()
に関してはココ見ときなって感じらしい。(LLLLは、『January, February, ..., December』にしてくれるのだそうです)
ってことでpages/posts/[id].js
を開いてDateコンポーネントをインポートしてやっていくよ、と。
import Date from '../../components/date'
export default function Post({ postData }) {
return (
<Layout>
{/* 既存のコードはそのまま */}
{/* {postData.date} をDateコンポーネントを使うやつにする */}
<Date dateString={postData.date} />
{/* 既存のコードはそのまま */}
</Layout>
)
}
↓ちゃんとそれっぽくなってますね〜
# スタイルシートを追加していく
styles/utils.module.css
が既にあるのでpages/posts/[id].js
に適応していく感じ。
やることはpages/posts/[id].js
でそれをインポートして、
import utilStyles from '../../styles/utils.module.css'
h1とかdivをそれっぽく。
<article>
<h1 className={utilStyles.headingXl}>{postData.title}</h1>
<div className={utilStyles.lightText}>
<Date dateString={postData.date} />
</div>
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</article>
↓なんだかイイ感じになりました 😃
# Indexページもイイ感じにしていく
pages/index.js
で各ブログポストにリンクをLinkコンポーネントを使ってやっていく感じ。
必要なものをインポートして、
import Link from 'next/link'
import Date from '../components/date'
元々あるliタグを置き換えて行く感じ
<li className={utilStyles.listItem} key={id}>
<Link href={`/posts/${id}`}>
<a>{title}</a>
</Link>
<br />
<small className={utilStyles.lightText}>
<Date dateString={date} />
</small>
</li>
↓整いました。
# 外部APIやデータベースへのクエリ
getStaticProps
とgetStaticPaths
のように外部からデータをフェッチできるのをgetAllPostIdsっていうので今回はgetStaticPathsから呼んでやりましたが、ファイルシステムのアクセスでなくて外部APIを呼んでもイイ、と。(Algoliaにインデクシングさせるならここがイイのかな〜。どっかから取ってきたデータを突っ込んでおけば、クライアントからInstantSearchで直接検索できる)
export async function getAllPostIds() {
// Instead of the file system,
// fetch post data from an external API endpoint
const res = await fetch('..')
const posts = await res.json()
return posts.map(post => {
return {
params: {
id: post.id
}
}
})
}
で、今までも何回か出てきたけど、developmentとproductionで振る舞いが違う(リクエストごととビルドごと)ので気つけてや、と。(とは言え、開発モードでも毎回AlgoliaのIndexing叩いたら微妙なのかな…?まぁAlgoliaはIndexingの回数では課金されないからレコード数が多くなければお金の面では問題なさげだけど、、)
そして出てきましたFallback。
getStaticPaths
でfallback: false
にしたのはどんな意味があったのでしょうか?という。
fallback
がfalse
だったら、getStaticPathsで返ってこなかったページは404になります、と。fallback
がtrue
だったら、getStaticPathsの振る舞いが変わります、と。- getStaticPathsから返ってきたヤツはHTMLがビルド時に生成される
- ビルド時に生成されなかったパスは404になるわけではなく、Next.jsによってフォールバックされたヤツが表示される
- Next.jsはバックグラウンドで密かにリクエストされたパスを静的に生成するんだそうで、次にそのパスにリクエストがあったらそのページが表示されるのだそうです
fallback
がblocking
だったら、新しいパスはサーバー側でgetStaticPropsを使ってレンダリングされてキャッシュされるので、そのパス毎に一回だけ的な。これtrueの時と似てる気がするけど違いは実際にHTMLを生成する/しないとか、、って感じなのかな。。
んま、この fallback
が true
の時と blocking
の時については fallbackのドキュメント読みましょう、と。
次に、全部のルートをキャッチする。ここで...
このドット3つがまた出てくる。っていうかこれを使うと全てのパスがキャッチできるのだそうで、例えば、pages/posts/[...id].js
っていう風にしておけば、posts/a
だけでなくposts/a/b
やposts/a/b/c
なんかもOKだぜ、と。
まぁ、なんかややこしいけど、これをやるならgetStaticPaths
でid
を↓のようにしるんだそうですわ。
return [
{
params: {
// Statically Generates /posts/a/b/c
id: ['a', 'b', 'c']
}
}
//...
]
で、params.id
はgetStaticPropsの中で↓こんな感じ。
export async function getStaticProps({ params }) {
// params.id will be like ['a', 'b', 'c']
}
これもドキュメント読め系だけど、おじさん的にはURLなければ404で、あんまりパスを複雑にしない方向で…とか思ってしまったり、しまわなかったりね。笑
他にもNext.jsのRouterにアクセスしたければ、 useRouter
っていうのを next/router
からどうぞ、とか、あー、このカスタム404ページは便利かもですね。pages/404.js
を作って↓こんなヤツ
// pages/404.js
export default function Custom404() {
return <h1>404 - Page Not Found</h1>
}
んま、そんな感じで、次回はNext.jsでのAPI Routesってことらしいです。