Next.jsを使用して複数の作成者のブログを作成する

公開: 2022-03-10
クイックサマリー↬この記事では、Next.jsアプリケーションでさまざまな種類のコンテンツを接続する方法について説明します。 この手法を使用すると、プロジェクトに1対1、1対多、さらには多対多の関係を追加できます。

この記事では、2人以上の作成者をサポートするNext.jsを使用してブログを作成します。 各投稿を著者に帰属させ、投稿とともに名前と写真を表示します。 各作成者は、投稿したすべての投稿を一覧表示するプロファイルページも取得します。 次のようになります。

左側:作成する完成したブログインデックス。右側:作成者のプロフィールページにリンクしている個々の投稿のページ。
左側:作成する完成したブログインデックス。 右側:作成者のプロフィールページにリンクしている個々の投稿のページ。 (大プレビュー)
すべての投稿にリンクしている著者のプロフィールページ。
すべての投稿にリンクしている著者のプロフィールページ(大きなプレビュー)

すべての情報をローカルファイルシステム上のファイルに保存します。 投稿と作成者の2種類のコンテンツは、異なる種類のファイルを使用します。 テキストの多い投稿はMarkdownを使用するため、編集プロセスが簡単になります。 著者に関する情報は軽いので、JSONファイルに保存します。 ヘルパー関数を使用すると、さまざまなファイルタイプを読み取り、それらのコンテンツを簡単に組み合わせることができます。

Next.jsを使用すると、さまざまなソースやさまざまなタイプのデータを簡単に読み取ることができます。 その動的ルーティングとnext/linkおかげで、サイトのさまざまなページをすばやく構築してナビゲートできます。 また、 next/imageパッケージで画像の最適化を無料で利用できます。

「付属のバッテリー」Next.jsを選択することで、アプリケーション自体に集中できます。 新しいプロジェクトで頻繁に発生する反復的な基礎作業に時間を費やす必要はありません。 すべてを手作業で構築する代わりに、テスト済みで実証済みのフレームワークに頼ることができます。 Next.jsの背後にある大規模で活発なコミュニティにより、途中で問題が発生した場合に簡単にサポートを受けることができます。

この記事を読むと、1つのNext.jsプロジェクトにさまざまな種類のコンテンツを追加できるようになります。 また、それらの間に関係を作成することができます。 これにより、作者と投稿、コースとレッスン、俳優と映画などをリンクできます。

この記事は、Next.jsの基本的な知識があることを前提としています。 これまでに使用したことがない場合は、最初にページを処理してデータをフェッチする方法を確認することをお勧めします。

この記事ではスタイリングについては説明せず、代わりにすべてを機能させることに焦点を当てます。 結果はGitHubで取得できます。 この記事をフォローしたい場合は、プロジェクトにドロップできるスタイルシートもあります。 ナビゲーションを含めて同じフレームを取得するには、 pages/_app.jsをこのファイルに置き換えます。

ジャンプした後もっと! 以下を読み続けてください↓

設定

まず、 create-next-appを使用して新しいプロジェクトを設定し、そのディレクトリに移動します。

 $ npx create-next-app multiauthor-blog $ cd multiauthor-blog

後でMarkdownファイルを読み取る必要があります。 これを簡単にするために、開始する前にさらにいくつかの依存関係を追加します。

 multiauthor-blog$ yarn add gray-matter remark remark-html

インストールが完了したら、 devスクリプトを実行してプロジェクトを開始できます。

 multiauthor-blog$ yarn dev

これで、サイトを探索できます。 ブラウザで、https:// localhost:3000を開きます。 create-next-appによって追加されたデフォルトのページが表示されます。

create-next-appによって作成されるデフォルトのページ。
これが表示された場合、セットアップは機能します。 (大プレビュー)

少しすると、ページにアクセスするためのナビゲーションが必要になります。 ページが存在する前でも、 pages/_app.jsに追加できます。

 import Link from 'next/link' import '../styles/globals.css' export default function App({ Component, pageProps }) { return ( <> <header> <nav> <ul> <li> <Link href="/"> <a>Home</a> </Link> </li> <li> <Link href="/posts"> <a>Posts</a> </Link> </li> <li> <Link href="/authors"> <a>Authors</a> </Link> </li> </ul> </nav> </header> <main> <Component {...pageProps} /> </main> </> ) }

この記事全体を通して、ナビゲーションポイントを指すこれらの欠落しているページを追加します。 最初にいくつかの投稿を追加して、ブログの概要ページで作業できるようにします。

投稿の作成

コンテンツをコードから分離するために、投稿を_posts/というディレクトリに配置します。 書き込みと編集を簡単にするために、各投稿をマークダウンファイルとして作成します。 各投稿のファイル名は、後でルートのスラッグとして機能します。 たとえば、ファイル_posts/hello-world.md/posts/hello-worldからアクセスできます。

完全なタイトルや短い抜粋などの一部の情報は、ファイルの先頭にあります。

 --- title: "Hello World!" excerpt: "This is my first blog post." createdAt: "2021-05-03" --- Hey, how are you doing? Welcome to my blog. In this post, …

ブログが空で始まらないように、このようなファイルをさらにいくつか追加します。

 multi-author-blog/ ├─ _posts/ │ ├─ hello-world.md │ ├─ multi-author-blog-in-nextjs.md │ ├─ styling-react-with-tailwind.md │ └─ ten-hidden-gems-in-javascript.md └─ pages/ └─ …

独自のサンプル投稿を追加するか、GitHubリポジトリからこれらのサンプル投稿を取得できます。

すべての投稿を一覧表示

いくつかの投稿ができたので、それらをブログに載せる方法が必要です。 それらすべてをリストするページを追加することから始めましょう。これはブログのインデックスとして機能します。

Next.jsでは、 pages/posts/index.jsの下に作成されたファイルは、当サイトの/postsとしてアクセスできます。 ファイルは、そのページの本文として機能する関数をエクスポートする必要があります。 その最初のバージョンは次のようになります。

 export default function Posts() { return ( <div className="posts"> <h1>Posts</h1> {/* TODO: render posts */} </div> ) }

Markdownファイルを読み取る方法がまだないため、それほど遠くはありません。 すでにhttps:// localhost:3000 / postsに移動できますが、見出しのみが表示されます。

「投稿」という見出しのある空のページ。
私たちは自分のページにアクセスして、それを人生で満たすことができます。 (大プレビュー)

ここに投稿を取得する方法が必要です。 Next.jsは、 getStaticProps()という関数を使用して、ページコンポーネントにデータを渡します。 この関数は、返されたオブジェクトのpropsを小道具としてコンポーネントに渡します。

getStaticProps()から、 postsと呼ばれる小道具として投稿をコンポーネントに渡します。 この最初のステップでは、2つのプレースホルダー投稿をハードコーディングします。 このように開始することで、後で実際の投稿を受け取る形式を定義します。ヘルパー関数がこの形式でそれらを返す場合、コンポーネントを変更せずにそれに切り替えることができます。

投稿の概要には、投稿の全文は表示されません。 このページでは、各投稿のタイトル、抜粋、パーマリンク、および日付で十分です。

 export default function Posts() { … } +export function getStaticProps() { + return { + props: { + posts: [ + { + title: "My first post", + createdAt: "2021-05-01", + excerpt: "A short excerpt summarizing the post.", + permalink: "/posts/my-first-post", + slug: "my-first-post", + }, { + title: "My second post", + createdAt: "2021-05-04", + excerpt: "Another summary that is short.", + permalink: "/posts/my-second-post", + slug: "my-second-post", + } + ] + } + } +}

接続を確認するために、小道具から投稿を取得して、 Postsコンポーネントに表示できます。 タイトル、作成日、抜粋、投稿へのリンクを含めます。 今のところ、そのリンクはまだどこにもつながりません。

 +import Link from 'next/link' -export default function Posts() { +export default function Posts({ posts }) { return ( <div className="posts"> <h1>Posts</h1> - {/* TODO: render posts */} + {posts.map(post => { + const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { + month: 'short', + day: '2-digit', + year: 'numeric', + }) + + return ( + <article key={post.slug}> + <h2> + <Link href={post.permalink}> + <a>{post.title}</a> + </Link> + </h2> + + <time dateTime={post.createdAt}>{prettyDate}</time> + + <p>{post.excerpt}</p> + + <Link href={post.permalink}> + <a>Read more →</a> + </Link> + </article> + ) + })} </div> ) } export function getStaticProps() { … }

ブラウザでページをリロードすると、次の2つの投稿が表示されます。

2つのプレースホルダー投稿のリスト。
接続は機能します。 これで、ここに実際の投稿を配置することができます。 (大プレビュー)

getStaticProps()ですべてのブログ投稿を永久にハードコーディングしたくありません。 結局のところ、これが、これらすべてのファイルを以前に_posts/ディレクトリに作成した理由です。 これらのファイルを読み取り、そのコンテンツをページコンポーネントに渡す方法が必要です。

それを行うにはいくつかの方法があります。 getStaticProps()でファイルを直接読み取ることができます。 この関数はクライアントではなくサーバーで実行されるため、 fsなどのネイティブNode.jsモジュールにアクセスできます。 ページコンポーネントを保持しているのと同じファイル内のローカルファイルを読み取ったり、変換したり、操作したりすることもできます。

ファイルを短くして1つのタスクに集中させるために、代わりにその機能を別のファイルに移動します。 そうすれば、 Postsコンポーネントはデータを表示するだけでよく、そのデータ自体を読み取る必要もありません。 これにより、プロジェクトに分離と編成が追加されます。

慣例により、データを読み取る関数をlib/api.jsというファイルに配置します。 そのファイルには、それを表示するコンポーネントのコンテンツを取得するすべての関数が含まれます。

投稿の概要ページには、すべての投稿を読み取り、処理し、返す関数が必要です。 これをgetAllPosts()と呼びます。 その中で、最初にpath.join()を使用して_posts/ディレクトリへのパスを作成します。 次に、 fs.readdirSync()を使用してそのディレクトリを読み取り、そのディレクトリ内のすべてのファイルの名前を取得します。 これらの名前をマッピングしてから、各ファイルを順番に読み取ります。

 import fs from 'fs' import path from 'path' export function getAllPosts() { const postsDirectory = path.join(process.cwd(), '_posts') const filenames = fs.readdirSync(postsDirectory) return filenames.map(filename => { const file = fs.readFileSync(path.join(process.cwd(), '_posts', filename), 'utf8') // TODO: transform and return file }) }

ファイルを読み取った後、その内容を長い文字列として取得します。 フロントgray-matterを投稿のテキストから分離するために、その文字列をグレイマターに通します。 また、ファイル名の末尾から.mdを削除して、各投稿のスラッグを取得します。 後で投稿にアクセスできるURLを作成するには、そのスラグが必要です。 この関数には投稿のMarkdown本文は必要ないため、残りのコンテンツは無視できます。

 import fs from 'fs' import path from 'path' +import matter from 'gray-matter' export function getAllPosts() { const postsDirectory = path.join(process.cwd(), '_posts') const filenames = fs.readdirSync(postsDirectory) return filenames.map(filename => { const file = fs.readFileSync(path.join(process.cwd(), '_posts', filename), 'utf8') - // TODO: transform and return file + // get frontmatter + const { data } = matter(file) + + // get slug from filename + const slug = filename.replace(/\.md$/, '') + + // return combined frontmatter and slug; build permalink + return { + ...data, + slug, + permalink: `/posts/${slug}`, + } }) }

ここで、返されたオブジェクトに...dataをどのように拡散するかに注意してください。 これにより、後で{post.title} {post.data.title}フロントマターから値にアクセスできるようになります。

投稿の概要ページに戻ると、プレースホルダーの投稿をこの新しい関数に置き換えることができます。

 +import { getAllPosts } from '../../lib/api' export default function Posts({ posts }) { … } export function getStaticProps() { return { props: { - posts: [ - { - title: "My first post", - createdAt: "2021-05-01", - excerpt: "A short excerpt summarizing the post.", - permalink: "/posts/my-first-post", - slug: "my-first-post", - }, { - title: "My second post", - createdAt: "2021-05-04", - excerpt: "Another summary that is short.", - permalink: "/posts/my-second-post", - slug: "my-second-post", - } - ] + posts: getAllPosts(), } } }

ブラウザをリロードすると、以前のプレースホルダーではなく、実際の投稿が表示されるようになります。

私たちの実際のブログ投稿のリスト。
ヘルパー機能のおかげで、このページに実際の投稿が表示されるようになりました。 (大プレビュー)

個別の投稿ページの追加

各投稿に追加したリンクは、まだどこにもつながりません。 /posts/hello-worldようなURLに応答するページはまだありません。 動的ルーティングを使用すると、このようなすべてのパスに一致するページを追加できます。

pages/posts/[slug].jsとして作成されたファイルは、 /posts/abcのようなすべてのURLと一致します。 URLで[slug]の代わりに表示される値は、クエリパラメータとしてページで使用できます。 対応するページのgetStaticProps()params.slugとして使用して、ヘルパー関数を呼び出すことができます。

getAllPosts()に対応するものとして、そのヘルパー関数getPostBySlug(slug)を呼び出します。 すべての投稿の代わりに、通過したスラッグに一致する単一の投稿が返されます。 投稿のページでは、基になるファイルのMarkdownコンテンツも表示する必要があります。

個々の投稿のページは、投稿の概要のページのように見えます。 getStaticProps()でページにpostsを渡す代わりに、1つのpostのみを渡します。 投稿のMarkdown本文を使用可能なHTMLに変換する方法を確認する前に、まず一般的な設定を行いましょう。 次のステップですぐに追加するヘル​​パー関数を使用して、ここではプレースホルダーの投稿をスキップします。

 import { getPostBySlug } from '../../lib/api' export default function Post({ post }) { const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', }) return ( <div className="post"> <h1>{post.title}</h1> <time dateTime={post.createdAt}>{prettyDate}</time> {/* TODO: render body */} </div> ) } export function getStaticProps({ params }) { return { props: { post: getPostBySlug(params.slug), }, } }

ここで、関数getPostBySlug(slug)をヘルパーファイルlib/api.jsに追加する必要があります。 これはgetAllPosts()に似ていますが、いくつかの顕著な違いがあります。 スラッグから投稿のファイル名を取得できるため、最初にディレクトリ全体を読み取る必要はありません。 スラッグが'hello-world'場合、 _posts/hello-world.mdというファイルを読み取ります。 そのファイルが存在しない場合、Next.jsは404エラーページを表示します。

getAllPosts()とのもう1つの違いは、今回は投稿のMarkdownコンテンツも読み取る必要があることです。 最初にコメントを付けて処理することで、生のremarkではなくレンダリング対応のHTMLとして返すことができます。

 import fs from 'fs' import path from 'path' import matter from 'gray-matter' +import remark from 'remark' +import html from 'remark-html' export function getAllPosts() { … } +export function getPostBySlug(slug) { + const file = fs.readFileSync(path.join(process.cwd(), '_posts', `${slug}.md`), 'utf8') + + const { + content, + data, + } = matter(file) + + const body = remark().use(html).processSync(content).toString() + + return { + ...data, + body, + } +}

理論的には、 getPostBySlug(slug)内で関数getAllPosts()を使用できます。 最初にすべての投稿を取得し、次に指定されたスラッグに一致する投稿を検索します。 つまり、1つの投稿を取得する前に、常にすべての投稿を読む必要があります。これは不要な作業です。 getAllPosts()は、投稿のMarkdownコンテンツも返しません。 それを行うために更新することができます。その場合、現在必要以上の作業が行われます。

2つのヘルパー関数は異なることを行うため、それらを分離しておくことにします。 そうすることで、機能を正確に、そしてそれぞれが実行する必要のある仕事だけに集中させることができます。

動的ルーティングを使用するページは、 getStaticPaths() getStaticProps()提供できます。 この関数は、ページを作成する動的パスセグメントの値をNext.jsに通知します。 getAllPosts()を使用し、各投稿のslugを定義するオブジェクトのリストを返すことで、これらを提供できます。

 -import { getPostBySlug } from '../../lib/api' +import { getAllPosts, getPostBySlug } from '../../lib/api' export default function Post({ post }) { … } export function getStaticProps({ params }) { … } +export function getStaticPaths() { + return { + fallback: false, + paths: getAllPosts().map(post => ({ + params: { + slug: post.slug, + }, + })), + } +}

getPostBySlug(slug)でMarkdownコンテンツを解析するので、これでページにレンダリングできます。 Next.jsがpost.bodyの背後にHTMLをレンダリングできるように、このステップではdangerouslySetInnerHTMLを使用する必要があります。 その名前にもかかわらず、このシナリオではプロパティを使用しても安全です。 投稿を完全に制御できるため、安全でないスクリプトが挿入される可能性はほとんどありません。

 import { getAllPosts, getPostBySlug } from '../../lib/api' export default function Post({ post }) { const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', }) return ( <div className="post"> <h1>{post.title}</h1> <time dateTime={post.createdAt}>{prettyDate}</time> - {/* TODO: render body */} + <div dangerouslySetInnerHTML={{ __html: post.body }} /> </div> ) } export function getStaticProps({ params }) { … } export function getStaticPaths() { … }

投稿の概要からのリンクの1つをたどると、その投稿の独自のページに移動します。

個々の投稿のページ。
投稿の内容を表示することはできますが、誰が書いたかはまだわかりません。 (大プレビュー)

著者の追加

投稿が配線されたので、作成者に対して同じ手順を繰り返す必要があります。 今回は、Markdownの代わりにJSONを使用して説明します。 意味があるときはいつでも、このように同じプロジェクトにさまざまな種類のファイルを混在させることができます。 ファイルを読み取るために使用するヘルパー関数は、違いを処理します。 ページは、コンテンツを保存する形式を知らなくても、これらの関数を使用できます。

まず、 _authors/というディレクトリを作成し、それにいくつかの作成者ファイルを追加します。 投稿で行ったように、各作成者のスラッグでファイルに名前を付けます。 これを使用して、後で著者を検索します。 各ファイルでは、JSONオブジェクトで作成者のフルネームを指定します。

 { "name": "Adrian Webber" }

今のところ、私たちのプロジェクトには2人の著者がいるだけで十分です。

彼らにもう少し個性を与えるために、各作者のプロフィール写真も追加しましょう。 これらの静的ファイルをpublic/ディレクトリに配置します。 同じスラッグでファイルに名前を付けることにより、暗黙の規則のみを使用してファイルを接続できます。 画像のパスを各作成者のJSONファイルに追加して、2つをリンクすることができます。 すべてのファイルにスラッグで名前を付けることにより、この接続を書き出すことなく管理できます。 JSONオブジェクトは、コードで構築できない情報を保持するだけで済みます。

完了すると、プロジェクトディレクトリは次のようになります。

 multi-author-blog/ ├─ _authors/ │ ├─ adrian-webber.json │ └─ megan-carter.json ├─ _posts/ │ └─ … ├─ pages/ │ └─ … └─ public/ ├─ adrian-webber.jpg └─ megan-carter.jpg

投稿と同じように、すべての作成者を読み取り、個々の作成者を取得するためのヘルパー関数が必要になりました。 新しい関数getAllAuthors()getAuthorBySlug(slug)lib/api.jsにあります。 彼らは彼らのポストの対応物とほとんど同じことをします。 JSONを使用して作成者を記述するため、ここでコメント付きのremarkを解析する必要はありません。 また、フロントgray-matterは必要ありません。 代わりに、JavaScriptの組み込みJSON.parse()を使用して、ファイルのテキストコンテンツをオブジェクトに読み込むことができます。

 const contents = fs.readFileSync(somePath, 'utf8') // ⇒ looks like an object, but is a string // eg '{ "name": "John Doe" }' const json = JSON.parse(contents) // ⇒ a real JavaScript object we can do things with // eg { name: "John Doe" }

その知識があると、ヘルパー関数は次のようになります。

 export function getAllPosts() { … } export function getPostBySlug(slug) { … } +export function getAllAuthors() { + const authorsDirectory = path.join(process.cwd(), '_authors') + const filenames = fs.readdirSync(authorsDirectory) + + return filenames.map(filename => { + const file = fs.readFileSync(path.join(process.cwd(), '_authors', filename), 'utf8') + + // get data + const data = JSON.parse(file) + + // get slug from filename + const slug = filename.replace(/\.json/, '') + + // return combined frontmatter and slug; build permalink + return { + ...data, + slug, + permalink: `/authors/${slug}`, + profilePictureUrl: `${slug}.jpg`, + } + }) +} + +export function getAuthorBySlug(slug) { + const file = fs.readFileSync(path.join(process.cwd(), '_authors', `${slug}.json`), 'utf8') + + const data = JSON.parse(file) + + return { + ...data, + permalink: `/authors/${slug}`, + profilePictureUrl: `/${slug}.jpg`, + slug, + } +}

著者をアプリケーションに読み込む方法で、すべての著者をリストするページを追加できるようになりました。 pages/authors/index.jsの下に新しいページを作成すると、私たちのサイトに/authorsページが表示されます。

ヘルパー関数がファイルの読み取りを処理します。 このページコンポーネントは、作成者がファイルシステム内のJSONファイルであることを知る必要はありません。 データをどこでどのように取得するかを知らなくても、 getAllAuthors()を使用できます。 ヘルパー関数が使用可能な形式でデータを返す限り、形式は重要ではありません。 このような抽象化により、アプリケーション全体でさまざまなタイプのコンテンツを混在させることができます。

著者のインデックスページは、投稿のインデックスページとよく似ています。 getStaticProps()ですべての作成者を取得し、作成Authorsコンポーネントに渡します。 そのコンポーネントは、各作成者をマッピングし、それらに関するいくつかの情報を一覧表示します。 スラッグから他のリンクやURLを作成する必要はありません。 ヘルパー関数は、すでに作成者を使用可能な形式で返します。

 import Image from 'next/image' import Link from 'next/link' import { getAllAuthors } from '../../lib/api/authors' export default function Authors({ authors }) { return ( <div className="authors"> <h1>Authors</h1> {authors.map(author => ( <div key={author.slug}> <h2> <Link href={author.permalink}> <a>{author.name}</a> </Link> </h2> <Image alt={author.name} src={author.profilePictureUrl} height="40" width="40" /> <Link href={author.permalink}> <a>Go to profile →</a> </Link> </div> ))} </div> ) } export function getStaticProps() { return { props: { authors: getAllAuthors(), }, } }

当サイトの/authorsにアクセスすると、すべての著者のリストとその名前と写真が表示されます。

著者のリスト。
すべての著者をリストすることはできますが、彼らが投稿した記事の数を知る方法はありません。 (大プレビュー)

著者のプロフィールへのリンクはまだどこにも通じていません。 プロファイルページを追加するには、 pages/authors/[slug].js下にファイルを作成します。 著者にはテキストコンテンツがないため、今のところ追加できるのは名前とプロフィール写真だけです。 また、ページを構築するためのスラッグをNext.jsに指示するために、別のgetStaticPaths()が必要です。

 import Image from 'next/image' import { getAllAuthors, getAuthorBySlug } from '../../lib/api' export default function Author({ author }) { return ( <div className="author"> <h1>{author.name}</h1> <Image alt={author.name} src={author.profilePictureUrl} height="80" width="80" /> </div> ) } export function getStaticProps({ params }) { return { props: { author: getAuthorBySlug(params.slug), }, } } export function getStaticPaths() { return { fallback: false, paths: getAllAuthors().map(author => ({ params: { slug: author.slug, }, })), } }

これで、情報が非常に少ない基本的な著者プロフィールページができました。

作者のプロフィールページ。名前とヘッドショットが表示されます。
著者のプロフィールページは現在ほとんど空です。 (大プレビュー)

この時点では、作成者と投稿はまだ接続されていません。 次に、そのブリッジを構築して、各作成者の投稿のリストをプロファイルページに追加できるようにします。

投稿と著者をつなぐ

2つのコンテンツを接続するには、一方を他方で参照する必要があります。 投稿と作成者はスラッグですでに識別されているので、それを参照します。 投稿に著者を追加したり、著者に投稿を追加したりすることもできますが、それらをリンクするには1つの方向で十分です。 投稿を作成者に帰属させたいので、各投稿のフロントマターに作成者のスラッグを追加します。

 --- title: "Hello World!" excerpt: "This is my first blog post." createdAt: "2021-05-03" +author: adrian-webber --- Hey, how are you doing? Welcome to my blog. In this post, …

それを維持すると、投稿をgray-matterで実行すると、作成者フィールドが文字列として投稿に追加されます。

 const post = getPostBySlug("hello-world") const author = post.author console.log(author) // "adrian-webber"

作成者を表すオブジェクトを取得するには、そのスラッグを使用してgetAuthorBySlug(slug)を呼び出します。

 const post = getPostBySlug("hello-world") -const author = post.author +const author = getAuthorBySlug(post.author) console.log(author) // { // name: "Adrian Webber", // slug: "adrian-webber", // profilePictureUrl: "/adrian-webber.jpg", // permalink: "/authors/adrian-webber" // }

著者を単一の投稿のページに追加するには、 getStaticProps() getAuthorBySlug(slug)を1回呼び出す必要があります。

 +import Image from 'next/image' +import Link from 'next/link' -import { getPostBySlug } from '../../lib/api' +import { getAuthorBySlug, getPostBySlug } from '../../lib/api' export default function Post({ post }) { const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', }) return ( <div className="post"> <h1>{post.title}</h1> <time dateTime={post.createdAt}>{prettyDate}</time> + <div> + <Image alt={post.author.name} src={post.author.profilePictureUrl} height="40" width="40" /> + + <Link href={post.author.permalink}> + <a> + {post.author.name} + </a> + </Link> + </div> <div dangerouslySetInnerHTML={{ __html: post.body }}> </div> ) } export function getStaticProps({ params }) { + const post = getPostBySlug(params.slug) return { props: { - post: getPostBySlug(params.slug), + post: { + ...post, + author: getAuthorBySlug(post.author), + }, }, } }

getStaticProps()postとも呼ばれるオブジェクトに...postをどのように拡散するかに注意してください。 その行の後に作成authorを配置することにより、作成者の文字列バージョンを完全なオブジェクトに置き換えることになります。 これにより、 Postコンポーネントのpost.author.nameを介して作成者のプロパティにアクセスできます。

この変更により、投稿のページに、名前と写真が記載された作成者のプロフィールページへのリンクが表示されるようになりました。

完成した投稿ページには、著者の名前とヘッドショットが含まれています。
これで、投稿は作成者に適切に帰属します。 (大プレビュー)

投稿の概要ページに作成者を追加するには、同様の変更が必要です。 getAuthorBySlug(slug)を一度呼び出す代わりに、すべての投稿にマップして、投稿ごとに呼び出す必要があります。

 +import Image from 'next/image' +import Link from 'next/link' -import { getAllPosts } from '../../lib/api' +import { getAllPosts, getAuthorBySlug } from '../../lib/api' export default function Posts({ posts }) { return ( <div className="posts"> <h1>Posts</h1> {posts.map(post => { const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', }) return ( <article key={post.slug}> <h2> <Link href={post.permalink}> <a>{post.title}</a> </Link> </h2> <time dateTime={post.createdAt}>{prettyDate}</time> + <div> + <Image alt={post.author.name} src={post.author.profilePictureUrl} height="40" width="40" /> + + <span>{post.author.name}</span> + </div> <p>{post.excerpt}</p> <Link href={post.permalink}> <a>Read more →</a> </Link> </article> ) })} </div> ) } export function getStaticProps() { return { props: { - posts: getAllPosts(), + posts: getAllPosts().map(post => ({ + ...post, + author: getAuthorBySlug(post.author), + })), } } }

これにより、投稿の概要の各投稿に作成者が追加されます。

著者の名前と顔写真を含むブログ投稿のリスト。
これは今では本物のブログのように見えます。 (大プレビュー)

著者の投稿のリストをJSONファイルに追加する必要はありません。 彼らのプロフィールページでは、最初にgetAllPosts()を使用してすべての投稿を取得します。 次に、この作成者に起因するものの完全なリストをフィルタリングできます。

 import Image from 'next/image' +import Link from 'next/link' -import { getAllAuthors, getAuthorBySlug } from '../../lib/api' +import { getAllAuthors, getAllPosts, getAuthorBySlug } from '../../lib/api' export default function Author({ author }) { return ( <div className="author"> <h1>{author.name}</h1> <Image alt={author.name} src={author.profilePictureUrl} height="40" width="40" /> + <h2>Posts</h2> + + <ul> + {author.posts.map(post => ( + <li> + <Link href={post.permalink}> + <a> + {post.title} + </a> + </Link> + </li> + ))} + </ul> </div> ) } export function getStaticProps({ params }) { const author = getAuthorBySlug(params.slug) return { props: { - author: getAuthorBySlug(params.slug), + author: { + ...author, + posts: getAllPosts().filter(post => post.author === author.slug), + }, }, } } export function getStaticPaths() { … }

これにより、すべての著者のプロファイルページにある記事のリストが表示されます。

著者のプロフィールページ。投稿へのリンクのリストが含まれるようになりました。
これで、各作成者の投稿を一覧表示してリンクできます。 (大プレビュー)

著者の概要ページでは、インターフェースを乱雑にしないために、彼らが書いた投稿の数だけを追加します。

 import Image from 'next/image' import Link from 'next/link' -import { getAllAuthors } from '../../lib/api' +import { getAllAuthors, getAllPosts } from '../../lib/api' export default function Authors({ authors }) { return ( <div className="authors"> <h1>Authors</h1> {authors.map(author => ( <div key={author.slug}> <h2> <Link href={author.permalink}> <a> {author.name} </a> </Link> </h2> <Image alt={author.name} src={author.profilePictureUrl} height="40" width="40" /> + <p>{author.posts.length} post(s)</p> <Link href={author.permalink}> <a>Go to profile →</a> </Link> </div> ))} </div> ) } export function getStaticProps() { return { props: { - authors: getAllAuthors(), + authors: getAllAuthors().map(author => ({ + ...author, + posts: getAllPosts().filter(post => post.author === author.slug), + })), } } }

これにより、作成者の概要ページに、各作成者が投稿した投稿の数が表示されます。

投稿数を含む著者のリスト。
これで、投稿された投稿の数を各著者のエントリに含めることができます。 (大プレビュー)

以上です! 投稿と著者は完全にリンクされています。 投稿から作成者のプロフィールページに移動し、そこから他の投稿に移動できます。

まとめと展望

この記事では、2つの関連するタイプのコンテンツをそれぞれの固有のスラッグを介して接続しました。 投稿から作成者までの関係を定義することで、さまざまなシナリオが可能になりました。 これで、各投稿に作成者を表示し、プロフィールページに投稿を一覧表示できます。

この手法を使用すると、他の多くの種類の関係を追加できます。 各投稿には、作成者の上にレビュー担当者がいる場合があります。 投稿のフロントマターにreviewerフィールドを追加することで、これを設定できます。

 --- title: "Hello World!" excerpt: "This is my first blog post." createdAt: "2021-05-03" author: adrian-webber +reviewer: megan-carter --- Hey, how are you doing? Welcome to my blog. In this post, …

ファイルシステムでは、レビュー担当者は_authors/ディレクトリの別の作成者です。 getAuthorBySlug(slug)を使用して、それらの情報を取得することもできます。

 export function getStaticProps({ params }) { const post = getPostBySlug(params.slug) return { props: { post: { ...post, author: getAuthorBySlug(post.author), + reviewer: getAuthorBySlug(post.reviewer), }, }, } }

投稿に1人だけでなく、2人以上の著者を指名することで、共著者をサポートすることもできます。

 --- title: "Hello World!" excerpt: "This is my first blog post." createdAt: "2021-05-03" -author: adrian-webber +authors: + - adrian-webber + - megan-carter --- Hey, how are you doing? Welcome to my blog. In this post, …

このシナリオでは、投稿のgetStaticProps()で1人の作成者を検索できなくなりました。 代わりに、この一連の作成者をマッピングして、すべてを取得します。

 export function getStaticProps({ params }) { const post = getPostBySlug(params.slug) return { props: { post: { ...post, - author: getAuthorBySlug(post.author), + authors: post.authors.map(getAuthorBySlug), }, }, } }

この手法を使用して、他の種類のシナリオを作成することもできます。 これにより、あらゆる種類の1対1、1対多、さらには多対多の関係が可能になります。 プロジェクトにニュースレターやケーススタディも含まれている場合は、それぞれに作成者を追加することもできます。

マーベルユニバースに関するすべてのサイトで、キャラクターと登場する映画をつなぐことができました。スポーツでは、プレーヤーと現在プレイしているチームをつなぐことができました。

ヘルパー関数はデータソースを隠すため、コンテンツはさまざまなシステムから取得される可能性があります。 ファイルシステムから記事を読み取り、APIからコメントを読み取り、それらをコードにマージすることができます。 あるコンテンツが別のタイプのコンテンツに関連している場合、それらをこのパターンで結び付けることができます。

その他のリソース

Next.jsは、データフェッチのページで使用した関数の背景を詳しく説明しています。 さまざまなタイプのソースからデータをフェッチするサンプルプロジェクトへのリンクが含まれています。

このスタータープロジェクトをさらに進めたい場合は、次の記事を確認してください。

  • StripiとNext.jsを使用してCSSTricksWebサイトクローンを構築する
    ローカルファイルシステム上のファイルをStrapi搭載のバックエンドに置き換えます。
  • Next.jsのスタイリングメソッドの比較
    このスターターのスタイルを変更するために、カスタムCSSを作成するさまざまな方法を調べてください。
  • Next.jsを使用したMarkdown / MDX
    プロジェクトにMDXを追加して、MarkdownでJSXおよびReactコンポーネントを使用できるようにします。