使用 Next.js 創建多作者博客

已發表: 2022-03-10
快速總結↬本文解釋了我們如何在 Next.js 應用程序中連接不同類型的內容。 使用這種技術,我們可以在我們的項目中添加任何類型的一對一、一對多甚至多對多的關係。

在本文中,我們將使用 Next.js 構建一個支持兩個或多個作者的博客。 我們會將每個帖子歸於作者,並在帖子中顯示他們的姓名和圖片。 每個作者還會獲得一個個人資料頁面,其中列出了他們貢獻的所有帖子。 它看起來像這樣:

左側:我們要構建的已完成的博客索引。右側:單個帖子的頁面,鏈接到其作者的個人資料頁面。
左側:我們將要構建的已完成的博客索引。 右側:單個帖子的頁面,鏈接到其作者的個人資料頁面。 (大預覽)
作者的個人資料頁面,鏈接到他們的所有帖子。
作者的個人資料頁面,鏈接到他們的所有帖子。(大預覽)

我們將把所有信息保存在本地文件系統的文件中。 兩種類型的內容,帖子和作者,將使用不同類型的文件。 文字較多的帖子將使用 Markdown,從而簡化編輯過程。 因為作者的信息比較少,我們將把它保存在 JSON 文件中。 輔助函數將使讀取不同的文件類型和組合它們的內容更容易。

Next.js 讓我們可以毫不費力地讀取來自不同來源和不同類型的數據。 由於其動態路由和next/link ,我們可以快速構建和導航到我們網站的各個頁面。 我們還通過next/image包免費獲得圖像優化。

通過選擇“包含的電池”Next.js,我們可以專注於我們的應用程序本身。 我們不必在新項目經常出現的重複性基礎工作上花費任何時間。 我們可以依賴經過測試和驗證的框架,而不是手動構建所有內容。 Next.js 背後龐大而活躍的社區讓我們在遇到問題時很容易獲得幫助。

閱讀本文後,您將能夠將多種內容添加到單個 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/的目錄中。 為了使編寫和編輯更容易,我們將每個帖子創建為 Markdown 文件。 每個帖子的文件名將在以後作為我們路線中的 slug。 例如,文件_posts/hello-world.md可以在/posts/hello-world下訪問。

一些信息,如完整的標題和簡短的摘錄,放在文件開頭的 frontmatter 中。

 --- 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的道具傳遞給組件。 在第一步中,我們將硬編碼兩個佔位符帖子。 通過這種方式,我們定義了以後想要接收真實帖子的格式。如果輔助函數以這種格式返回它們,我們可以在不更改組件的情況下切換到它。

帖子概述不會顯示帖子的全文。 對於這個頁面,每個帖子的標題、摘錄、永久鏈接和日期就足夠了。

 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", + } + ] + } + } +}

要檢查連接,我們可以從 props 中獲取帖子並將它們顯示在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() { … }

在瀏覽器中重新加載頁面後,它現在顯示以下兩個帖子:

我們的兩個佔位符帖子的列表。
連接有效。 現在我們可以在這裡發布真實的帖子。 (大預覽)

我們不想永遠在getStaticProps()中硬編碼我們所有的博客文章。 畢竟,這就是我們之前在_posts/目錄中創建所有這些文件的原因。 我們現在需要一種方法來讀取這些文件並將它們的內容傳遞給頁面組件。

我們有幾種方法可以做到這一點。 我們可以直接在getStaticProps()中讀取文件。 因為這個函數在服務器而不是客戶端上運行,所以我們可以訪問其中的原生 Node.js 模塊,比如fs 。 我們可以在保存頁面組件的同一個文件中讀取、轉換甚至操作本地文件。

為了使文件簡短並專注於一項任務,我們將把該功能移到一個單獨的文件中。 這樣, 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 }) }

讀取文件後,我們將其內容作為長字符串獲取。 為了將 frontmatter 與帖子的文本分開,我們通過gray-matter運行該字符串。 我們還將通過從文件名末尾刪除.md來獲取每個帖子的 slug。 我們需要那個 slug 來構建稍後可以訪問帖子的 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傳播到此處返回的對像中。 這讓我們可以從它的 frontmatter 中以{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) 。 它將返回與我們傳遞的 slug 匹配的單個帖子,而不是所有帖子。 在帖子的頁面上,我們還需要顯示底層文件的 Markdown 內容。

單個帖子的頁面看起來像帖子概述的頁面。 我們只傳遞一個post ,而不是在getStaticProps()中將posts傳遞到頁面。 在我們了解如何將帖子的 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() ,但有一些顯著差異。 因為我們可以從 slug 中獲取帖子的文件名,所以我們不需要先讀取整個目錄。 如果 slug 是'hello-world' ,我們將讀取一個名為_posts/hello-world.md的文件。 如果該文件不存在,Next.js 將顯示 404 錯誤頁面。

getAllPosts()的另一個區別是,這一次,我們還需要讀取帖子的 Markdown 內容。 我們可以通過首先使用remark處理它,將它作為渲染就緒的 HTML 而不是原始的 Markdown 返回。

 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()函數。 我們首先使用它獲取所有帖子,然後我們可以搜索與給定 slug 匹配的帖子。 這意味著我們總是需要閱讀所有帖子才能獲得一篇文章,這是不必要的工作。 getAllPosts()也不會返回帖子的 Markdown 內容。 我們可以更新它來做到這一點,在這種情況下,它會做比目前需要的更多的工作。

因為這兩個輔助函數做不同的事情,所以我們將它們分開。 這樣,我們就可以準確地將功能集中在我們需要它們完成的工作上。

使用動態路由的頁面可以在其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 內容,我們現在可以在頁面上呈現它。 我們需要在此步驟中使用dangerouslySetInnerHTML ,以便 Next.js 可以呈現post.body後面的 HTML。 儘管它的名字,在這種情況下使用該屬性是安全的。 因為我們可以完全控制我們的帖子,所以他們不太可能會注入不安全的腳本。

 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() { … }

如果我們按照帖子概述中的鏈接之一,我們現在可以訪問該帖子自己的頁面。

單個帖子的頁面。
我們可以顯示帖子的內容,但還不知道是誰寫的。 (大預覽)

添加作者

既然我們已經連接了帖子,我們需要為我們的作者重複相同的步驟。 這一次,我們將使用 JSON 而不是 Markdown 來描述它們。 只要有意義,我們就可以像這樣在同一個項目中混合不同類型的文件。 我們用來讀取文件的輔助函數會為我們處理任何差異。 頁面可以在不知道我們存儲內容的格式的情況下使用這些功能。

首先,創建一個名為_authors/的目錄並向其中添加一些作者文件。 正如我們對帖子所做的那樣,按每個作者的 slug 命名文件。 稍後我們將使用它來查找作者。 在每個文件中,我們在 JSON 對像中指定作者的全名。

 { "name": "Adrian Webber" }

目前,在我們的項目中有兩個作者就足夠了。

為了給他們更多個性,我們還為每位作者添加個人資料圖片。 我們將把這些靜態文件放在public/目錄中。 通過用相同的 slug 命名文件,我們可以單獨使用隱含的約定來連接它們。 我們可以將圖片的路徑添加到每個作者的 JSON 文件中,以鏈接兩者。 通過以 slug 命名所有文件,我們可以管理此連接而無需將其寫出。 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的 Markdown。 我們也不需要gray-matter來解析frontmatter。 相反,我們可以使用 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組件。 該組件映射每個作者並列出有關他們的一些信息。 我們不需要從 slug 構建任何其他鏈接或 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下創建一個文件。 因為作者沒有任何文字內容,我們現在只能添加他們的姓名和頭像。 我們還需要另一個getStaticPaths()來告訴 Next.js 為哪些 slug 構建頁面。

 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, }, })), } }

有了這個,我們現在有一個基本的作者簡介頁面,信息非常簡單。

作者的個人資料頁面,顯示他們的姓名和頭像。
作者的個人資料頁面現在大部分是空的。 (大預覽)

此時,作者和帖子尚未連接。 接下來我們將搭建這個橋樑,以便我們可以將每個作者的帖子列表添加到他們的個人資料頁面。

連接帖子和作者

要連接兩段內容,我們需要在另一段中引用。 由於我們已經通過它們的 slug 識別帖子和作者,因此我們將引用它們。 我們可以將作者添加到帖子和帖子到作者,但是一個方向就足以將它們鏈接起來。 由於我們想將帖子歸因於作者,我們將把作者的 slug 添加到每個帖子的 frontmatter 中。

 --- 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"

要獲取代表作者的對象,我們可以使用該 slug 並使用它調用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) ) 。

 +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), + }, }, } }

請注意我們如何將...post傳播到getStaticProps()中也稱為post的對像中。 通過將author放在該行之後,我們最終將 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), + })), } } }

這樣,作者概述頁面會顯示每個作者貢獻了多少帖子。

作者列表及其帖子數量。
我們現在可以將他們貢獻的帖子數量與每個作者的條目一起輸入。 (大預覽)

就是這樣! 帖子和作者現在完全聯繫起來了。 我們可以從一個帖子到作者的個人資料頁面,然後從那裡到他們的其他帖子。

總結與展望

在本文中,我們通過它們獨特的 slug 連接了兩種相關類型的內容。 定義從帖子到作者的關係啟用了各種場景。 我們現在可以在每個帖子上顯示作者,並在他們的個人資料頁面上列出他們的帖子。

使用這種技術,我們可以添加許多其他類型的關係。 每個帖子都可能在作者之上有一個審稿人。 我們可以通過在帖子的前端添加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), }, }, } }

我們甚至可以通過在一個帖子上命名兩個或多個作者而不是一個人來支持共同作者。

 --- 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()中查找單個作者。 相反,我們將映射這組作者以獲取所有作者。

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

我們還可以使用這種技術生成其他類型的場景。 它支持任何類型的一對一、一對多甚至多對多關係。 如果您的項目還包含時事通訊和案例研究,您也可以為每個項目添加作者。

在一個關於漫威宇宙的網站上,我們可以連接角色和他們出現的電影。在體育運動中,我們可以連接球員和他們目前效力的球隊。

因為輔助函數隱藏了數據源,所以內容可能來自不同的系統。 我們可以從文件系統中讀取文章,從 API 中讀取評論,並將它們合併到我們的代碼中。 如果某些內容與另一種類型的內容相關,我們可以將它們與這種模式聯繫起來。

更多資源

Next.js 提供了有關我們在其數據獲取頁面中使用的功能的更多背景信息。 它包括從不同類型的來源獲取數據的示例項目的鏈接。

如果您想進一步了解此入門項目,請查看以下文章:

  • 使用 Strapi 和 Next.js 構建 CSS 技巧網站克隆
    用 Strapi 驅動的後端替換本地文件系統上的文件。
  • 比較 Next.js 中的樣式方法
    探索編寫自定義 CSS 以更改此啟動器樣式的不同方法。
  • 帶有 Next.js 的 Markdown/MDX
    將 MDX 添加到您的項目中,以便您可以在 Markdown 中使用 JSX 和 React 組件。