Создание блога с несколькими авторами с помощью 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. Имя файла каждого поста позже будет использоваться в качестве слага в наших маршрутах. Например, файл _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 . На этом первом шаге мы жестко закодируем два поста-заполнителя. Начав таким образом, мы определяем, в каком формате мы впоследствии хотим получать настоящие посты. Если вспомогательная функция возвращает их в этом формате, мы можем переключиться на него, не меняя компонент.

В обзоре сообщений не будет отображаться полный текст сообщений. Для этой страницы достаточно названия, выдержки, постоянной ссылки и даты каждого поста.

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

После перезагрузки страницы в браузере теперь отображаются эти два сообщения:

Список наших двух постов-заполнителей.
Связь работает. Теперь мы можем работать над размещением реальных сообщений здесь. (Большой превью)

Мы не хотим постоянно жестко кодировать все наши сообщения в блоге в 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 }) }

После чтения файла мы получаем его содержимое в виде длинной строки. Чтобы отделить вступительную часть от текста поста, мы пропускаем эту строку через 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(), } } }

После перезагрузки браузера мы теперь видим наши настоящие сообщения вместо заполнителей, которые у нас были раньше.

Список наших реальных сообщений в блоге.
Благодаря вспомогательной функции на этой странице теперь отображаются наши настоящие посты. (Большой превью)

Добавление отдельных страниц сообщений

Ссылки, которые мы добавили в каждый пост, пока никуда не ведут. Пока нет страницы, которая отвечает на такие URL-адреса, как /posts/hello-world . С помощью динамической маршрутизации мы можем добавить страницу, которая соответствует всем путям, подобным этому.

Файл, созданный как pages/posts/[slug].js , будет соответствовать всем URL-адресам, которые выглядят как /posts/abc . Значение, отображаемое вместо [slug] в URL-адресе, будет доступно странице в качестве параметра запроса. Мы можем использовать это в getStaticProps() соответствующей страницы как params.slug для вызова вспомогательной функции.

В качестве аналога getAllPosts() мы назовем эту вспомогательную функцию getPostBySlug(slug) . Вместо всех сообщений он вернет одно сообщение, соответствующее слагу, который мы ему передаем. На странице поста нам также нужно показать содержимое Markdown базового файла.

Страница для отдельных сообщений выглядит так же, как и для обзора сообщений. Вместо того, чтобы передавать posts на страницу в getStaticProps() , мы передаем только одно 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() заключается в том, что на этот раз нам также нужно прочитать содержимое записи в формате Markdown. Мы можем вернуть его как готовый к рендерингу HTML вместо необработанного Markdown, сначала обработав его с remark .

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

Теоретически мы могли бы использовать функцию getAllPosts() внутри getPostBySlug(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, + }, + })), + } +}

Поскольку мы анализируем содержимое Markdown в getPostBySlug(slug) , теперь мы можем отобразить его на странице. Для этого шага нам нужно использовать dangerouslySetInnerHTML , чтобы Next.js мог отображать HTML позади post.body . Несмотря на название, использовать это свойство в этом сценарии безопасно. Поскольку у нас есть полный контроль над нашими сообщениями, маловероятно, что они будут внедрять небезопасные скрипты.

 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/ и добавьте в него несколько файлов авторов. Как и в случае с записями, назовите файлы по псевдониму каждого автора. Мы будем использовать это для поиска авторов позже. В каждом файле мы указываем полное имя автора в объекте JSON.

 { "name": "Adrian Webber" }

На данный момент достаточно двух авторов в нашем проекте.

Чтобы придать им больше индивидуальности, давайте также добавим изображение профиля для каждого автора. Мы поместим эти статические файлы в каталог public/ . Назвав файлы одним и тем же слагом, мы можем соединить их, используя только подразумеваемое соглашение. Мы могли бы добавить путь к изображению в файл JSON каждого автора, чтобы связать их. Называя все файлы слагами, мы можем управлять этим соединением, не записывая его. Объекты 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 для описания авторов, нам не нужно здесь анализировать Markdown с remark . Нам также не нужно gray-matter , чтобы анализировать переднее вещество. Вместо этого мы можем использовать встроенный в 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 . Поскольку у авторов нет текстового контента, все, что мы можем сейчас добавить, это их имена и изображения профилей. Нам также нужен еще один getStaticPaths() , чтобы сообщить Next.js, для каких слагов создавать страницы.

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

Благодаря этому у нас теперь есть базовая страница профиля автора, на которой очень мало информации.

Страница профиля автора с указанием его имени и портрета.
Страница профиля автора в настоящее время в основном пуста. (Большой превью)

На данный момент авторы и посты еще не связаны. Далее мы построим этот мост, чтобы мы могли добавить список сообщений каждого автора на страницы их профилей.

Соединение постов и авторов

Чтобы соединить две части контента, нам нужно сослаться на одну в другой. Поскольку мы уже идентифицируем сообщения и авторов по их слагам, мы будем ссылаться на них. Мы могли бы добавлять авторов к постам и посты к авторам, но достаточно одного направления, чтобы связать их. Поскольку мы хотим приписывать сообщения авторам, мы собираемся добавить слаг автора во вступительную часть каждого сообщения.

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

Чтобы добавить автора на страницу отдельного сообщения, нам нужно один раз вызвать getAuthorBySlug(slug) в getStaticProps() .

 +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 в объект, который также называется post в getStaticProps() . Поместив author после этой строки, мы в конечном итоге заменяем строковую версию автора его полным объектом. Это позволяет нам получить доступ к свойствам автора через post.author.name в компоненте Post .

С этим изменением мы теперь получаем ссылку на страницу профиля автора с его именем и изображением на странице поста.

Готовая страница поста, которая теперь включает имя автора и портрет.
Пост теперь правильно указан автором. (Большой превью)

Для добавления авторов на страницу обзора сообщений требуется аналогичное изменение. Вместо того, чтобы вызывать 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), + })), } } }

При этом на странице обзора авторов показано, сколько сообщений написал каждый автор.

Список авторов с количеством их постов.
Теперь мы можем указать количество опубликованных постов с записью каждого автора. (Большой превью)

Вот и все! Сообщения и авторы теперь полностью связаны. Мы можем перейти от поста к странице профиля автора, а оттуда — к другим его постам.

Резюме и перспективы

В этой статье мы соединили два связанных типа контента через их уникальные слаги. Определение связи между постом и автором позволяет реализовать множество сценариев. Теперь мы можем показать автора в каждом сообщении и перечислить его сообщения на страницах их профиля.

С помощью этой техники мы можем добавить много других видов отношений. Каждое сообщение может иметь рецензента поверх автора. Мы можем настроить это, добавив поле 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), }, }, } }

Мы также можем создавать другие виды сценариев с помощью этой техники. Он допускает любые виды отношений «один к одному», «один ко многим» или даже «многие ко многим». Если в вашем проекте также есть информационные бюллетени и тематические исследования, вы также можете добавить авторов к каждому из них.

На сайте, посвященном вселенной Marvel, мы могли бы связать персонажей и фильмы, в которых они появляются. В спорте мы могли бы связать игроков и команды, за которые они сейчас играют.

Поскольку вспомогательные функции скрывают источник данных, контент может поступать из разных систем. Мы могли читать статьи из файловой системы, комментарии из API и объединять их с нашим кодом. Если какой-то фрагмент контента относится к другому типу контента, мы можем связать их с этим паттерном.

Дополнительные ресурсы

Next.js предлагает больше информации о функциях, которые мы использовали на их странице, посвященной выборке данных. Он включает ссылки на примеры проектов, которые извлекают данные из разных типов источников.

Если вы хотите продолжить этот начальный проект, ознакомьтесь со следующими статьями:

  • Создание клона веб-сайта с трюками CSS с помощью Strapi и Next.js
    Замените файлы в локальной файловой системе серверной частью на основе Strapi.
  • Сравнение методов оформления в Next.js
    Изучите различные способы написания пользовательского CSS, чтобы изменить стиль этого стартового приложения.
  • Markdown/MDX с Next.js
    Добавьте MDX в свой проект, чтобы вы могли использовать компоненты JSX и React в своем Markdown.