Tworzenie bloga wielu autorów za pomocą Next.js
Opublikowany: 2022-03-10W tym artykule zamierzamy zbudować blog z Next.js, który obsługuje dwóch lub więcej autorów. Każdy post przypiszemy do autora, a wraz z postami pokażemy jego imię i nazwisko oraz zdjęcie. Każdy autor otrzymuje również stronę profilu, która zawiera listę wszystkich postów, które wniósł. Będzie to wyglądać mniej więcej tak:
Będziemy przechowywać wszystkie informacje w plikach w lokalnym systemie plików. Dwa rodzaje treści, posty i autorzy, będą używać różnych typów plików. Posty z dużą ilością tekstu będą używać Markdown, co pozwoli na łatwiejszy proces edycji. Ponieważ informacje o autorach są lżejsze, zachowamy je w plikach JSON. Funkcje pomocnicze ułatwią odczytywanie różnych typów plików i łączenie ich zawartości.
Next.js pozwala nam bez wysiłku odczytywać dane z różnych źródeł i różnych typów. Dzięki dynamicznemu routingowi i next/link
możemy szybko budować i nawigować po różnych stronach naszej witryny. Optymalizację obrazu otrzymujemy również za darmo z pakietem next/image
.
Wybierając „baterie w zestawie” Next.js, możemy skupić się na samej naszej aplikacji. Nie musimy spędzać czasu na powtarzalnych pracach przygotowawczych, z którymi często pojawiają się nowe projekty. Zamiast budować wszystko ręcznie, możemy polegać na sprawdzonym i sprawdzonym frameworku. Duża i aktywna społeczność stojąca za Next.js ułatwia uzyskanie pomocy, jeśli po drodze napotkamy problemy.
Po przeczytaniu tego artykułu będziesz mógł dodawać wiele rodzajów treści do jednego projektu Next.js. Będziesz także mógł tworzyć relacje między nimi. Dzięki temu możesz łączyć takie rzeczy, jak autorzy i posty, kursy i lekcje lub aktorzy i filmy.
W tym artykule założono podstawową znajomość Next.js. Jeśli nie używałeś go wcześniej, możesz przeczytać, jak obsługuje strony i najpierw pobiera dla nich dane.
W tym artykule nie będziemy omawiać stylizacji i skupimy się na tym, aby wszystko działało. Możesz uzyskać wynik na GitHub. Istnieje również arkusz stylów, który możesz wrzucić do swojego projektu, jeśli chcesz śledzić wraz z tym artykułem. Aby uzyskać tę samą ramkę, w tym nawigację, zastąp swój plik pages/_app.js
tym plikiem.
Organizować coś
Zaczynamy od utworzenia nowego projektu za pomocą create-next-app
i przejścia do jego katalogu:
$ npx create-next-app multiauthor-blog $ cd multiauthor-blog
Będziemy musieli później przeczytać pliki Markdown. Aby to ułatwić, przed rozpoczęciem dodamy jeszcze kilka zależności.
multiauthor-blog$ yarn add gray-matter remark remark-html
Po zakończeniu instalacji możemy uruchomić skrypt dev
, aby rozpocząć nasz projekt:
multiauthor-blog$ yarn dev
Możemy teraz eksplorować naszą stronę. W przeglądarce otwórz https://localhost:3000. Powinieneś zobaczyć domyślną stronę dodaną przez create-next-app.
Za chwilę będziemy potrzebować nawigacji, aby dotrzeć do naszych stron. Możemy je dodać w pages/_app.js
jeszcze przed powstaniem strony.
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> </> ) }
W całym tym artykule dodamy te brakujące strony, do których wskazuje nawigacja. Dodajmy najpierw kilka postów, abyśmy mieli nad czym pracować na stronie przeglądu bloga.
Tworzenie postów
Aby treść była oddzielona od kodu, umieścimy nasze posty w katalogu o nazwie _posts/
. Aby ułatwić pisanie i edytowanie, utworzymy każdy post jako plik Markdown. Nazwa pliku każdego posta będzie później służyć jako ślimak w naszych trasach. Plik _posts/hello-world.md
będzie dostępny na przykład w /posts/hello-world
.
Niektóre informacje, takie jak pełny tytuł i krótki fragment, pojawiają się na początku pliku.
--- 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, …
Dodaj jeszcze kilka takich plików, aby blog nie zaczynał się pusty:
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/ └─ …
Możesz dodać własne lub pobrać te przykładowe wpisy z repozytorium GitHub.
Lista wszystkich postów
Teraz, gdy mamy już kilka postów, potrzebujemy sposobu na umieszczenie ich na naszym blogu. Zacznijmy od dodania strony zawierającej je wszystkie, służącej jako indeks naszego bloga.
W Next.js plik utworzony w pages/posts/index.js
będzie dostępny jako /posts
na naszej stronie. Plik musi wyeksportować funkcję, która będzie służyć jako treść strony. Jego pierwsza wersja wygląda mniej więcej tak:
export default function Posts() { return ( <div className="posts"> <h1>Posts</h1> {/* TODO: render posts */} </div> ) }
Nie zajdziemy daleko, ponieważ nie mamy jeszcze możliwości odczytania plików Markdown. Możemy już przejść do https://localhost:3000/posts, ale widzimy tylko nagłówek.
Potrzebujemy teraz sposobu na umieszczenie tam naszych postów. Next.js używa funkcji o nazwie getStaticProps()
do przekazywania danych do komponentu strony. Funkcja przekazuje props
w zwróconym obiekcie do komponentu jako props.
getStaticProps()
przekażemy posty do komponentu jako prop o nazwie posts
. W pierwszym kroku zakodujemy na stałe dwa posty zastępcze. Zaczynając w ten sposób, definiujemy, w jakim formacie później chcemy otrzymywać prawdziwe posty. Jeśli funkcja pomocnicza zwróci je w tym formacie, możemy przełączyć się na nią bez zmiany komponentu.
Przegląd postów nie zawiera pełnego tekstu postów. W przypadku tej strony wystarczy tytuł, fragment, permalink i data każdego postu.
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", + } + ] + } + } +}
Aby sprawdzić połączenie, możemy pobrać posty z rekwizytów i pokazać je w komponencie Posts
. Podamy tytuł, datę utworzenia, fragment i link do posta. Na razie ten link jeszcze nigdzie nie prowadzi.
+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() { … }
Po ponownym załadowaniu strony w przeglądarce pokazuje teraz te dwa posty:
Nie chcemy na stałe kodować wszystkich naszych postów na blogu w getStaticProps()
na zawsze. W końcu dlatego wcześniej utworzyliśmy wszystkie te pliki w katalogu _posts/
. Potrzebujemy teraz sposobu na odczytanie tych plików i przekazanie ich zawartości do komponentu strony.
Możemy to zrobić na kilka sposobów. Możemy odczytać pliki bezpośrednio w getStaticProps()
. Ponieważ ta funkcja działa na serwerze, a nie na kliencie, mamy dostęp do natywnych modułów Node.js, takich jak fs
. Moglibyśmy czytać, przekształcać, a nawet manipulować plikami lokalnymi w tym samym pliku, w którym przechowujemy komponent strony.
Aby plik był krótki i skoncentrowany na jednym zadaniu, zamiast tego przeniesiemy tę funkcjonalność do osobnego pliku. W ten sposób składnik Posts
musi tylko wyświetlać dane, bez konieczności samodzielnego czytania tych danych. To dodaje trochę separacji i organizacji do naszego projektu.
Zgodnie z konwencją, funkcje odczytujące dane umieścimy w pliku o nazwie lib/api.js
. Ten plik będzie zawierał wszystkie funkcje, które pobierają naszą zawartość dla komponentów, które ją wyświetlają.
Na stronie przeglądu postów potrzebujemy funkcji, która odczytuje, przetwarza i zwraca wszystkie posty. Nazwiemy to getAllPosts()
. W nim najpierw używamy path.join()
do zbudowania ścieżki do katalogu _posts/
. Następnie używamy fs.readdirSync()
do odczytania tego katalogu, co daje nam nazwy wszystkich znajdujących się w nim plików. Odwzorowując te nazwy, odczytujemy po kolei każdy plik.
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 }) }
Po odczytaniu pliku otrzymujemy jego zawartość jako długi ciąg. Aby oddzielić frontmatter od tekstu posta, przepuszczamy ten ciąg przez gray-matter
. Zamierzamy również pobrać slug każdego posta, usuwając .md
z końca jego nazwy pliku. Potrzebujemy tego ślimaka do zbudowania adresu URL, z którego post będzie później dostępny. Ponieważ nie potrzebujemy treści Markdown postów dla tej funkcji, możemy zignorować pozostałą zawartość.
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}`, + } }) }
Zwróć uwagę, jak rozprowadzamy tutaj ...data
do zwróconego obiektu. To pozwala nam uzyskać dostęp do wartości z frontmatter jako {post.title}
zamiast {post.data.title}
później.
Wracając na naszą stronę przeglądu postów, możemy teraz zastąpić posty zastępcze tą nową funkcją.
+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(), } } }
Po ponownym załadowaniu przeglądarki widzimy teraz nasze prawdziwe posty, a nie symbole zastępcze, które mieliśmy wcześniej.
Dodawanie indywidualnych stron postów
Linki, które dodaliśmy do każdego postu, jeszcze nigdzie nie prowadzą. Nie ma jeszcze strony, która odpowiadałaby na adresy URL, takie jak /posts/hello-world
. Dzięki routingowi dynamicznemu możemy dodać stronę, która pasuje do wszystkich ścieżek w ten sposób.
Plik utworzony jako pages/posts/[slug].js
będzie pasował do wszystkich adresów URL, które wyglądają jak /posts/abc
. Wartość, która pojawia się zamiast [slug]
w adresie URL, będzie dostępna na stronie jako parametr zapytania. Możemy użyć tego w odpowiedniej getStaticProps()
jako params.slug
do wywołania funkcji pomocniczej.
Jako odpowiednik getAllPosts()
, wywołamy tę funkcję pomocniczą getPostBySlug(slug)
. Zamiast wszystkich postów zwróci pojedynczy post, który pasuje do przekazanego przez nas slugu. Na stronie posta musimy również pokazać zawartość Markdown pliku bazowego.
Strona dla poszczególnych postów wygląda jak ta z przeglądem postów. Zamiast przekazywać posts
do strony w getStaticProps()
, przekazujemy tylko jeden post
. Zróbmy najpierw ogólną konfigurację, zanim przyjrzymy się, jak przekształcić treść Markdown posta w użyteczny kod HTML. Pominiemy tutaj wpis zastępczy, korzystając z funkcji pomocniczej, którą natychmiast dodamy w następnym kroku.
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), }, } }
Teraz musimy dodać funkcję getPostBySlug(slug)
do naszego pliku pomocniczego lib/api.js
. Przypomina to getAllPosts()
, z kilkoma znaczącymi różnicami. Ponieważ możemy pobrać nazwę pliku posta z ślimaka, nie musimy najpierw czytać całego katalogu. Jeśli slug to 'hello-world'
, będziemy czytać plik o nazwie _posts/hello-world.md
. Jeśli ten plik nie istnieje, Next.js wyświetli stronę błędu 404.
Kolejną różnicą w stosunku do getAllPosts()
jest to, że tym razem musimy również przeczytać zawartość Markdown posta. Możemy zwrócić go jako gotowy do renderowania kod HTML zamiast surowego Markdowna, przetwarzając go najpierw z 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, + } +}
Teoretycznie moglibyśmy użyć funkcji getAllPosts()
wewnątrz getPostBySlug(slug)
. Najpierw otrzymywaliśmy z nim wszystkie posty, które moglibyśmy następnie wyszukać taki, który pasuje do danego ślimaka. Oznaczałoby to, że zawsze musielibyśmy przeczytać wszystkie posty, zanim otrzymamy jeden, co jest niepotrzebną pracą. getAllPosts()
również nie zwraca zawartości Markdown postów. Moglibyśmy go zaktualizować, aby to zrobić, w którym to przypadku wykonałby więcej pracy niż obecnie.
Ponieważ te dwie funkcje pomocnicze robią różne rzeczy, zamierzamy je rozdzielić. W ten sposób możemy skoncentrować się na funkcjach dokładnie i tylko na zadaniach, których potrzebuje każdy z nich.
Strony korzystające z routingu dynamicznego mogą udostępniać getStaticPaths()
obok swojej getStaticProps()
. Ta funkcja informuje Next.js, dla jakich wartości segmentów ścieżki dynamicznej należy budować strony. Możemy to zapewnić, używając getAllPosts()
i zwracając listę obiektów, które definiują slug
każdego posta.
-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, + }, + })), + } +}
Ponieważ analizujemy zawartość Markdown w getPostBySlug(slug)
, możemy teraz renderować ją na stronie. W tym kroku musimy użyć dangerouslySetInnerHTML
, aby Next.js mógł renderować kod HTML znajdujący się za post.body
. Pomimo swojej nazwy, w tym scenariuszu można bezpiecznie używać tej właściwości. Ponieważ mamy pełną kontrolę nad naszymi postami, jest mało prawdopodobne, że wstrzykną niebezpieczne skrypty.
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() { … }
Jeśli podążymy za jednym z linków z przeglądu postów, przejdziemy teraz do własnej strony tego posta.
Dodawanie autorów
Teraz, gdy mamy już okablowane posty, musimy powtórzyć te same kroki dla naszych autorów. Tym razem do ich opisania użyjemy JSON zamiast Markdown. Możemy mieszać różne typy plików w tym samym projekcie, kiedy ma to sens. Funkcje pomocnicze, których używamy do odczytu plików, usuwają za nas wszelkie różnice. Strony mogą korzystać z tych funkcji, nie wiedząc, w jakim formacie przechowujemy nasze treści.
Najpierw utwórz katalog o nazwie _authors/
i dodaj do niego kilka plików autorów. Podobnie jak w przypadku postów, nazwij pliki według ślimaka każdego autora. Wykorzystamy to później do wyszukania autorów. W każdym pliku podajemy pełne imię i nazwisko autora w obiekcie JSON.
{ "name": "Adrian Webber" }
Na razie wystarczy mieć dwóch autorów w naszym projekcie.
Aby nadać im trochę więcej osobowości, dodajmy również zdjęcie profilowe dla każdego autora. Umieścimy te statyczne pliki w katalogu public/
. Nazywając pliki tym samym slugiem, możemy je połączyć używając samej dorozumianej konwencji. Moglibyśmy dodać ścieżkę obrazu do pliku JSON każdego autora, aby połączyć te dwa. Nazywając wszystkie pliki według ślimaków, możemy zarządzać tym połączeniem bez konieczności jego zapisywania. Obiekty JSON muszą tylko przechowywać informacje, których nie możemy zbudować za pomocą kodu.
Kiedy skończysz, twój katalog projektu powinien wyglądać mniej więcej tak.
multi-author-blog/ ├─ _authors/ │ ├─ adrian-webber.json │ └─ megan-carter.json ├─ _posts/ │ └─ … ├─ pages/ │ └─ … └─ public/ ├─ adrian-webber.jpg └─ megan-carter.jpg
Podobnie jak w przypadku postów, potrzebujemy teraz funkcji pomocniczych, aby przeczytać wszystkich autorów i uzyskać poszczególnych autorów. Nowe funkcje getAllAuthors()
i getAuthorBySlug(slug)
również trafiają do lib/api.js
. Robią prawie to samo, co ich odpowiedniki z postów. Ponieważ używamy JSON do opisywania autorów, nie musimy tutaj analizować żadnego Markdowna z remark
. Nie potrzebujemy również gray-matter
aby przeanalizować frontmatter. Zamiast tego możemy użyć wbudowanej w JavaScript funkcji JSON.parse()
, aby wczytać zawartość tekstową naszych plików do obiektów.
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" }
Mając tę wiedzę, nasze funkcje pomocnicze wyglądają tak:
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, + } +}
Mając sposób na wczytanie autorów do naszej aplikacji, możemy teraz dodać stronę z ich listą. Utworzenie nowej strony pod pages/authors/index.js
daje nam stronę /authors
na naszej stronie.
Funkcje pomocnicze zajmują się odczytaniem plików za nas. Ten składnik strony nie musi wiedzieć, że autorzy są plikami JSON w systemie plików. Może używać getAllAuthors()
bez wiedzy, skąd i jak pobiera swoje dane. Format nie ma znaczenia, o ile nasze funkcje pomocnicze zwracają swoje dane w formacie, z którym możemy pracować. Takie abstrakcje pozwalają nam mieszać różne rodzaje treści w naszej aplikacji.
Strona indeksu dla autorów wygląda bardzo podobnie do strony dla postów. Wszystkich autorów otrzymujemy w getStaticProps()
, która przekazuje ich do komponentu Authors
. Ten składnik mapuje każdego autora i wyświetla niektóre informacje na jego temat. Nie musimy tworzyć żadnych innych linków ani adresów URL na podstawie ślimaka. Funkcja pomocnicza już zwraca autorów w użytecznym formacie.
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(), }, } }
Jeśli odwiedzimy /authors
na naszej stronie, zobaczymy listę wszystkich autorów wraz z ich nazwiskami i zdjęciami.
Linki do profili autorów jeszcze nigdzie nie prowadzą. Aby dodać strony profilu tworzymy plik pod pages/authors/[slug].js
. Ponieważ autorzy nie mają żadnych treści tekstowych, na razie możemy tylko dodać ich nazwiska i zdjęcia profilowe. Potrzebujemy również innej getStaticPaths()
, aby poinformować Next.js, dla jakich slug ma budować strony.
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, }, })), } }
Dzięki temu mamy teraz podstawową stronę profilu autora, która zawiera bardzo jasne informacje.
W tym momencie autorzy i posty nie są jeszcze połączone. Następnie zbudujemy ten most, abyśmy mogli dodać listę postów każdego autora do ich stron profilowych.
Łączenie postów i autorów
Aby połączyć dwa fragmenty treści, musimy odwołać się do jednego w drugim. Ponieważ już identyfikujemy posty i autorów po ich slugach, będziemy się do nich odwoływać. Moglibyśmy dodawać autorów do postów i posty do autorów, ale wystarczy jeden kierunek, aby je połączyć. Ponieważ chcemy przypisywać posty autorom, dodamy ślimak autora do frontmatter każdego posta.
--- 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, …
Jeśli tak zachowamy, przepuszczanie posta przez gray-matter
dodaje pole autora do posta jako ciąg:
const post = getPostBySlug("hello-world") const author = post.author console.log(author) // "adrian-webber"
Aby pobrać obiekt reprezentujący autora, możemy użyć tego ślimaka i wywołać z nim 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" // }
Aby dodać autora do strony pojedynczego posta, musimy wywołać getAuthorBySlug(slug)
raz w 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), + }, }, } }
Zwróć uwagę, jak rozprzestrzeniamy ...post
w obiekcie zwanym również post
w getStaticProps()
. Umieszczając author
za tym wierszem, w końcu zastępujemy ciągową wersję autora pełnym obiektem. To pozwala nam uzyskać dostęp do właściwości autora poprzez post.author.name
w komponencie Post
.
Dzięki tej zmianie otrzymujemy teraz link do strony profilu autora, wraz z jego nazwiskiem i zdjęciem, na stronie posta.
Dodanie autorów do strony przeglądu wpisów wymaga podobnej zmiany. Zamiast wywoływać getAuthorBySlug(slug)
raz, musimy zmapować wszystkie posty i wywołać to dla każdego z nich.
+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), + })), } } }
To dodaje autorów do każdego posta w przeglądzie postów:
Nie musimy dodawać listy postów autora do jego pliku JSON. Na ich stronach profilowych najpierw otrzymujemy wszystkie posty za pomocą getAllPosts()
. Następnie możemy przefiltrować pełną listę pod kątem tych przypisanych do tego autora.
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() { … }
To daje nam listę artykułów na stronie profilu każdego autora.
Na stronie przeglądu autorów dodamy tylko liczbę postów, które napisali, aby nie zaśmiecać interfejsu.
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), + })), } } }
Dzięki temu strona przeglądu autorów pokazuje, ile postów napisał każdy autor.
I to wszystko! Posty i autorzy są teraz całkowicie połączone. Z posta możemy przejść na stronę profilu autora, a stamtąd do innych jego postów.
Podsumowanie i perspektywy
W tym artykule połączyliśmy dwa powiązane typy treści za pomocą ich unikalnych ślimaków. Zdefiniowanie relacji od posta do autora umożliwiło różne scenariusze. Możemy teraz pokazywać autora każdego posta i wyświetlać jego posty na ich stronach profilowych.
Dzięki tej technice możemy dodać wiele innych rodzajów relacji. Każdy post może mieć recenzenta nad autorem. Możemy to skonfigurować, dodając pole reviewer
do frontmatter postu.
--- 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, …
W systemie plików recenzentem jest inny autor z katalogu _authors/
. Możemy również użyć getAuthorBySlug(slug)
, aby uzyskać ich informacje.
export function getStaticProps({ params }) { const post = getPostBySlug(params.slug) return { props: { post: { ...post, author: getAuthorBySlug(post.author), + reviewer: getAuthorBySlug(post.reviewer), }, }, } }
Możemy nawet wspierać współautorów, wymieniając dwóch lub więcej autorów w poście zamiast tylko jednej osoby.
--- 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, …
W tym scenariuszu nie moglibyśmy już wyszukać pojedynczego autora w funkcji getStaticProps()
. Zamiast tego zmapowalibyśmy tę tablicę autorów, aby uzyskać ich wszystkich.
export function getStaticProps({ params }) { const post = getPostBySlug(params.slug) return { props: { post: { ...post, - author: getAuthorBySlug(post.author), + authors: post.authors.map(getAuthorBySlug), }, }, } }
Za pomocą tej techniki możemy również tworzyć inne rodzaje scenariuszy. Umożliwia każdy rodzaj relacji jeden do jednego, jeden do wielu, a nawet wiele do wielu. Jeśli Twój projekt zawiera również biuletyny i studia przypadków, możesz dodać autorów do każdego z nich.
Na stronie poświęconej uniwersum Marvela mogliśmy łączyć postacie i filmy, w których się pojawiają. W sporcie moglibyśmy łączyć graczy i drużyny, w których aktualnie grają.
Ponieważ funkcje pomocnicze ukrywają źródło danych, zawartość może pochodzić z różnych systemów. Mogliśmy czytać artykuły z systemu plików, komentarze z API i scalać je z naszym kodem. Jeśli jakiś fragment treści odnosi się do innego rodzaju treści, możemy połączyć je tym wzorcem.
Dalsze zasoby
Next.js oferuje więcej informacji na temat funkcji, których użyliśmy na ich stronie poświęconej pobieraniu danych. Zawiera łącza do przykładowych projektów, które pobierają dane z różnych typów źródeł.
Jeśli chcesz dalej rozwijać ten początkowy projekt, zapoznaj się z tymi artykułami:
- Tworzenie klonu strony ze sztuczkami CSS za pomocą Strapi i Next.js
Zastąp pliki w lokalnym systemie plików backendem opartym na Strapi. - Porównanie metod stylizacji w Next.js
Poznaj różne sposoby pisania niestandardowego CSS, aby zmienić styl tego startera. - Markdown/MDX z Next.js
Dodaj MDX do swojego projektu, aby móc używać komponentów JSX i React w Markdown.