Criando um blog de vários autores com o Next.js
Publicados: 2022-03-10Neste artigo, vamos construir um blog com Next.js que suporte dois ou mais autores. Atribuiremos cada postagem a um autor e mostraremos seu nome e foto com suas postagens. Cada autor também recebe uma página de perfil, que lista todas as postagens que eles contribuíram. Vai parecer algo assim:
Vamos manter todas as informações em arquivos no sistema de arquivos local. Os dois tipos de conteúdo, posts e autores, usarão diferentes tipos de arquivos. As postagens com muito texto usarão o Markdown, permitindo um processo de edição mais fácil. Como as informações sobre os autores são mais leves, vamos mantê-las em arquivos JSON. As funções auxiliares facilitarão a leitura de diferentes tipos de arquivos e a combinação de seu conteúdo.
Next.js nos permite ler dados de diferentes fontes e de diferentes tipos sem esforço. Graças ao seu roteamento dinâmico e next/link
, podemos construir e navegar rapidamente pelas várias páginas do nosso site. Também obtemos otimização de imagem gratuitamente com o pacote next/image
.
Ao escolher as “baterias incluídas” Next.js, podemos nos concentrar em nosso próprio aplicativo. Não precisamos gastar tempo com o trabalho de base repetitivo que os novos projetos geralmente trazem. Em vez de construir tudo à mão, podemos confiar na estrutura testada e comprovada. A grande e ativa comunidade por trás do Next.js facilita a obtenção de ajuda se tivermos problemas ao longo do caminho.
Depois de ler este artigo, você poderá adicionar vários tipos de conteúdo a um único projeto Next.js. Você também será capaz de criar relacionamentos entre eles. Isso permite que você vincule coisas como autores e postagens, cursos e aulas ou atores e filmes.
Este artigo pressupõe familiaridade básica com Next.js. Se você não o usou antes, talvez queira ler sobre como ele lida com páginas e busca dados para elas primeiro.
Não abordaremos o estilo neste artigo e nos concentraremos em fazer tudo funcionar. Você pode obter o resultado no GitHub. Há também uma folha de estilo que você pode colocar em seu projeto se quiser acompanhar este artigo. Para obter o mesmo frame, incluindo a navegação, substitua seu pages/_app.js
por este arquivo.
Configuração
Começamos configurando um novo projeto usando create-next-app
e mudando para seu diretório:
$ npx create-next-app multiauthor-blog $ cd multiauthor-blog
Precisaremos ler os arquivos Markdown mais tarde. Para tornar isso mais fácil, também adicionamos mais algumas dependências antes de começar.
multiauthor-blog$ yarn add gray-matter remark remark-html
Quando a instalação estiver concluída, podemos executar o script dev
para iniciar nosso projeto:
multiauthor-blog$ yarn dev
Agora podemos explorar nosso site. No seu navegador, abra https://localhost:3000. Você deve ver a página padrão adicionada por create-next-app.
Daqui a pouco, precisaremos de uma navegação para chegar às nossas páginas. Podemos adicioná-los em pages/_app.js
antes mesmo de as páginas existirem.
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> </> ) }
Ao longo deste artigo, adicionaremos essas páginas ausentes para as quais a navegação aponta. Vamos primeiro adicionar algumas postagens para que tenhamos algo para trabalhar em uma página de visão geral do blog.
Criando postagens
Para manter nosso conteúdo separado do código, colocaremos nossos posts em um diretório chamado _posts/
. Para facilitar a escrita e a edição, criaremos cada postagem como um arquivo Markdown. O nome de arquivo de cada post servirá como o slug em nossas rotas mais tarde. O arquivo _posts/hello-world.md
estará acessível em /posts/hello-world
, por exemplo.
Algumas informações, como o título completo e um pequeno trecho, vão no frontmatter no início do arquivo.
--- 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, …
Adicione mais alguns arquivos como este para que o blog não comece vazio:
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/ └─ …
Você pode adicionar suas próprias postagens ou pegar essas postagens de exemplo do repositório do GitHub.
Listando todas as postagens
Agora que temos alguns posts, precisamos de uma maneira de colocá-los em nosso blog. Vamos começar adicionando uma página que lista todos eles, servindo como índice do nosso blog.
No Next.js, um arquivo criado em pages/posts/index.js
estará acessível como /posts
em nosso site. O arquivo deve exportar uma função que servirá como corpo dessa página. Sua primeira versão se parece com isso:
export default function Posts() { return ( <div className="posts"> <h1>Posts</h1> {/* TODO: render posts */} </div> ) }
Não vamos muito longe porque ainda não temos como ler os arquivos Markdown. Já podemos navegar para https://localhost:3000/posts, mas vemos apenas o cabeçalho.
Agora precisamos de uma maneira de colocar nossos posts lá. Next.js usa uma função chamada getStaticProps()
para passar dados para um componente de página. A função passa as props
no objeto retornado para o componente como props.
De getStaticProps()
, vamos passar os posts para o componente como um prop chamado posts
. Codificaremos duas postagens de espaço reservado nesta primeira etapa. Começando assim, definimos em qual formato queremos mais tarde receber os posts reais. Se uma função auxiliar os retornar neste formato, podemos alternar para ele sem alterar o componente.
A visão geral da postagem não mostrará o texto completo das postagens. Para esta página, basta o título, o trecho, o permalink e a data de cada postagem.
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", + } + ] + } + } +}
Para verificar a conexão, podemos pegar os posts dos adereços e mostrá-los no componente Posts
. Incluiremos o título, data de criação, trecho e um link para o post. Por enquanto, esse link não levará a lugar nenhum ainda.
+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() { … }
Depois de recarregar a página no navegador, ele agora mostra esses dois posts:
Não queremos codificar todos os nossos posts em getStaticProps()
para sempre. Afinal, é por isso que criamos todos esses arquivos no diretório _posts/
anteriormente. Agora precisamos de uma maneira de ler esses arquivos e passar seu conteúdo para o componente de página.
Existem algumas maneiras de fazermos isso. Poderíamos ler os arquivos diretamente em getStaticProps()
. Como essa função é executada no servidor e não no cliente, temos acesso a módulos nativos do Node.js como fs
nele. Poderíamos ler, transformar e até manipular arquivos locais no mesmo arquivo em que mantemos o componente de página.
Para manter o arquivo curto e focado em uma tarefa, vamos mover essa funcionalidade para um arquivo separado. Dessa forma, o componente Posts
precisa apenas exibir os dados, sem também ter que ler esses dados. Isso adiciona alguma separação e organização ao nosso projeto.
Por convenção, vamos colocar funções lendo dados em um arquivo chamado lib/api.js
. Esse arquivo conterá todas as funções que capturam nosso conteúdo para os componentes que o exibem.
Para a página de visão geral das postagens, queremos uma função que leia, processe e retorne todas as postagens. Vamos chamá-lo de getAllPosts()
. Nele, primeiro usamos path.join()
para construir o caminho para o diretório _posts/
. Em seguida, usamos fs.readdirSync()
para ler esse diretório, o que nos dá os nomes de todos os arquivos nele. Mapeando esses nomes, lemos cada arquivo por vez.
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 }) }
Depois de ler o arquivo, obtemos seu conteúdo como uma longa string. Para separar o frontmatter do texto do post, passamos essa string por gray-matter
. Também vamos pegar o slug de cada post removendo o .md
do final do nome do arquivo. Precisamos desse slug para construir a URL a partir da qual a postagem ficará acessível mais tarde. Como não precisamos do corpo Markdown das postagens para essa função, podemos ignorar o conteúdo restante.
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}`, + } }) }
Observe como espalhamos ...data
no objeto retornado aqui. Isso nos permite acessar valores de seu frontmatter como {post.title}
em vez de {post.data.title}
posteriormente.
De volta à nossa página de visão geral de postagens, agora podemos substituir as postagens de espaço reservado por essa nova função.
+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(), } } }
Depois de recarregar o navegador, agora vemos nossas postagens reais em vez dos espaços reservados que tínhamos antes.
Adicionando páginas de postagem individuais
Os links que adicionamos a cada postagem ainda não levam a lugar algum. Ainda não existe uma página que responda a URLs como /posts/hello-world
. Com roteamento dinâmico, podemos adicionar uma página que corresponda a todos os caminhos como este.
Um arquivo criado como pages/posts/[slug].js
corresponderá a todos os URLs que se parecem com /posts/abc
. O valor que aparece em vez de [slug]
na URL estará disponível para a página como um parâmetro de consulta. Podemos usar isso no getStaticProps()
da página correspondente como params.slug
para chamar uma função auxiliar.
Como contrapartida de getAllPosts()
, chamaremos essa função auxiliar getPostBySlug(slug)
. Em vez de todos os posts, ele retornará um único post que corresponde ao slug que passamos. Na página de uma postagem, também precisamos mostrar o conteúdo Markdown do arquivo subjacente.
A página para postagens individuais se parece com a da visão geral da postagem. Em vez de passar posts
para a página em getStaticProps()
, passamos apenas um único post
. Vamos fazer a configuração geral primeiro antes de vermos como transformar o corpo Markdown da postagem em HTML utilizável. Vamos pular a postagem do espaço reservado aqui, usando a função auxiliar que adicionaremos na próxima etapa imediatamente.
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), }, } }
Agora temos que adicionar a função getPostBySlug(slug)
ao nosso arquivo auxiliar lib/api.js
. É como getAllPosts()
, com algumas diferenças notáveis. Como podemos obter o nome do arquivo do post do slug, não precisamos ler o diretório inteiro primeiro. Se o slug for 'hello-world'
, vamos ler um arquivo chamado _posts/hello-world.md
. Se esse arquivo não existir, Next.js mostrará uma página de erro 404.
Outra diferença para getAllPosts()
é que, desta vez, também precisamos ler o conteúdo Markdown do post. Podemos retorná-lo como HTML pronto para renderização em vez de Markdown bruto processando-o com remark
primeiro.
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, + } +}
Em teoria, poderíamos usar a função getAllPosts()
dentro getPostBySlug(slug)
. Primeiro, obteríamos todas as postagens com ele, que poderíamos procurar por uma que corresponda ao slug fornecido. Isso significaria que sempre precisaríamos ler todos os posts antes que pudéssemos obter um único, o que é um trabalho desnecessário. getAllPosts()
também não retorna o conteúdo Markdown das postagens. Poderíamos atualizá-lo para fazer isso e, nesse caso, ele faria mais trabalho do que precisa atualmente.
Como as duas funções auxiliares fazem coisas diferentes, vamos mantê-las separadas. Dessa forma, podemos focar as funções exatamente e apenas no trabalho que precisamos que cada uma delas faça.
As páginas que usam roteamento dinâmico podem fornecer um getStaticPaths()
ao lado de seu getStaticProps()
. Essa função informa ao Next.js para quais valores dos segmentos de caminho dinâmico criar páginas. Podemos fornecer isso usando getAllPosts()
e retornando uma lista de objetos que definem o slug
de cada post.
-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, + }, + })), + } +}
Como analisamos o conteúdo do Markdown em getPostBySlug(slug)
, podemos renderizá-lo na página agora. Precisamos usar dangerouslySetInnerHTML
para esta etapa para que Next.js possa renderizar o HTML por trás post.body
. Apesar do nome, é seguro usar a propriedade neste cenário. Como temos controle total sobre nossas postagens, é improvável que eles injetem scripts inseguros.
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() { … }
Se seguirmos um dos links da visão geral do post, agora chegamos à própria página desse post.
Adicionando autores
Agora que temos as postagens conectadas, precisamos repetir as mesmas etapas para nossos autores. Desta vez, usaremos JSON em vez de Markdown para descrevê-los. Podemos misturar diferentes tipos de arquivos no mesmo projeto assim sempre que fizer sentido. As funções auxiliares que usamos para ler os arquivos cuidam de quaisquer diferenças para nós. As páginas podem usar essas funções sem saber em que formato armazenamos nosso conteúdo.
Primeiro, crie um diretório chamado _authors/
e adicione alguns arquivos de autor a ele. Assim como fizemos com os posts, nomeie os arquivos pelo slug de cada autor. Usaremos isso para procurar autores mais tarde. Em cada arquivo, especificamos o nome completo de um autor em um objeto JSON.
{ "name": "Adrian Webber" }
Por enquanto, basta ter dois autores em nosso projeto.
Para dar um pouco mais de personalidade, vamos também adicionar uma foto de perfil para cada autor. Vamos colocar esses arquivos estáticos no diretório public/
. Ao nomear os arquivos pelo mesmo slug, podemos conectá-los usando apenas a convenção implícita. Poderíamos adicionar o caminho da imagem ao arquivo JSON de cada autor para vincular os dois. Ao nomear todos os arquivos pelos slugs, podemos gerenciar essa conexão sem precisar escrevê-la. Os objetos JSON só precisam conter informações que não podemos construir com código.
Quando terminar, o diretório do seu projeto deve se parecer com isso.
multi-author-blog/ ├─ _authors/ │ ├─ adrian-webber.json │ └─ megan-carter.json ├─ _posts/ │ └─ … ├─ pages/ │ └─ … └─ public/ ├─ adrian-webber.jpg └─ megan-carter.jpg
Assim como nos posts, agora precisamos de funções auxiliares para ler todos os autores e obter autores individuais. As novas funções getAllAuthors()
e getAuthorBySlug(slug)
também vão em lib/api.js
. Eles fazem quase exatamente o mesmo que seus colegas de postagem. Como usamos JSON para descrever autores, não precisamos analisar nenhum Markdown com remark
aqui. Também não precisamos gray-matter
para analisar a matéria de frente. Em vez disso, podemos usar o JSON.parse()
integrado do JavaScript para ler o conteúdo de texto de nossos arquivos em objetos.
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" }
Com esse conhecimento, nossas funções auxiliares ficam assim:
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, + } +}
Com uma maneira de ler autores em nosso aplicativo, agora podemos adicionar uma página que lista todos eles. Criar uma nova página em pages/authors/index.js
nos dá uma página /authors
em nosso site.
As funções auxiliares se encarregam de ler os arquivos para nós. Este componente de página não precisa saber que os autores são arquivos JSON no sistema de arquivos. Ele pode usar getAllAuthors()
sem saber onde ou como obtém seus dados. O formato não importa, desde que nossas funções auxiliares retornem seus dados em um formato com o qual possamos trabalhar. Abstrações como essa nos permitem misturar diferentes tipos de conteúdo em nosso aplicativo.
A página de índice para autores se parece muito com a de posts. Obtemos todos os autores em getStaticProps()
, que os passa para o componente Authors
. Esse componente mapeia cada autor e lista algumas informações sobre eles. Não precisamos criar nenhum outro link ou URL a partir do slug. A função auxiliar já retorna os autores em um formato utilizável.
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(), }, } }
Se visitarmos /authors
em nosso site, veremos uma lista de todos os autores com seus nomes e fotos.
Os links para os perfis dos autores ainda não levam a lugar nenhum. Para adicionar as páginas de perfil, criamos um arquivo em pages/authors/[slug].js
. Como os autores não têm nenhum conteúdo de texto, tudo o que podemos adicionar por enquanto são seus nomes e fotos de perfil. Também precisamos de outro getStaticPaths()
para informar ao Next.js para quais slugs construir páginas.
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, }, })), } }
Com isso, agora temos uma página de perfil de autor básica que é muito leve em informações.
Neste ponto, autores e postagens ainda não estão conectados. Construiremos essa ponte a seguir para que possamos adicionar uma lista das postagens de cada autor às suas páginas de perfil.
Conectando postagens e autores
Para conectar duas partes de conteúdo, precisamos referenciar uma na outra. Como já identificamos postagens e autores por seus slugs, faremos referência a eles com isso. Poderíamos adicionar autores a posts e posts a autores, mas uma direção é suficiente para vinculá-los. Como queremos atribuir posts a autores, vamos adicionar o slug do autor ao frontmatter de cada post.
--- 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, …
Se continuarmos assim, executar o post através gray-matter
adiciona o campo de autor ao post como uma string:
const post = getPostBySlug("hello-world") const author = post.author console.log(author) // "adrian-webber"
Para obter o objeto que representa o autor, podemos usar esse slug e chamar getAuthorBySlug(slug)
com ele.
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" // }
Para adicionar o autor à página de um único post, precisamos chamar getAuthorBySlug(slug)
uma vez em 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), + }, }, } }
Observe como espalhamos ...post
em um objeto também chamado post
em getStaticProps()
. Ao colocar o author
após essa linha, acabamos substituindo a versão string do autor pelo seu objeto completo. Isso nos permite acessar as propriedades de um autor por meio de post.author.name
no componente Post
.
Com essa alteração, agora obtemos um link para a página de perfil do autor, completo com seu nome e foto, na página de uma postagem.
Adicionar autores à página de visão geral do post requer uma alteração semelhante. Em vez de chamar getAuthorBySlug(slug)
uma vez, precisamos mapear todos os posts e chamá-los para cada um deles.
+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), + })), } } }
Isso adiciona os autores a cada postagem na visão geral da postagem:
Não precisamos adicionar uma lista de postagens de um autor ao arquivo JSON. Em suas páginas de perfil, primeiro obtemos todas as postagens com getAllPosts()
. Podemos então filtrar a lista completa para aqueles atribuídos a este autor.
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() { … }
Isso nos dá uma lista de artigos na página de perfil de cada autor.
Na página de visão geral do autor, adicionaremos apenas quantas postagens eles escreveram para não sobrecarregar a interface.
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), + })), } } }
Com isso, a página de visão geral dos autores mostra quantas postagens cada autor contribuiu.
E é isso! Posts e autores estão completamente ligados agora. Podemos ir de uma postagem para a página de perfil de um autor e de lá para as outras postagens.
Resumo e Perspectivas
Neste artigo, conectamos dois tipos de conteúdo relacionados por meio de seus slugs exclusivos. Definir o relacionamento da postagem com o autor permitiu uma variedade de cenários. Agora podemos mostrar o autor em cada postagem e listar suas postagens em suas páginas de perfil.
Com esta técnica, podemos adicionar muitos outros tipos de relacionamentos. Cada postagem pode ter um revisor em cima de um autor. Podemos configurar isso adicionando um campo de reviewer
ao frontmatter de uma postagem.
--- 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, …
No sistema de arquivos, o revisor é outro autor do diretório _authors/
. Podemos usar getAuthorBySlug(slug)
para obter suas informações também.
export function getStaticProps({ params }) { const post = getPostBySlug(params.slug) return { props: { post: { ...post, author: getAuthorBySlug(post.author), + reviewer: getAuthorBySlug(post.reviewer), }, }, } }
Poderíamos até apoiar coautores nomeando dois ou mais autores em uma postagem em vez de apenas uma única pessoa.
--- 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, …
Nesse cenário, não poderíamos mais procurar um único autor em getStaticProps()
de uma postagem. Em vez disso, mapeamos essa matriz de autores para obter todos eles.
export function getStaticProps({ params }) { const post = getPostBySlug(params.slug) return { props: { post: { ...post, - author: getAuthorBySlug(post.author), + authors: post.authors.map(getAuthorBySlug), }, }, } }
Também podemos produzir outros tipos de cenários com esta técnica. Permite qualquer tipo de relacionamento um-para-um, um-para-muitos ou mesmo muitos-para-muitos. Se o seu projeto também inclui boletins informativos e estudos de caso, você também pode adicionar autores a cada um deles.
Em um site sobre o universo Marvel, poderíamos conectar os personagens e os filmes em que eles aparecem. Nos esportes, poderíamos conectar os jogadores e os times em que jogam atualmente.
Como as funções auxiliares ocultam a fonte de dados, o conteúdo pode vir de sistemas diferentes. Poderíamos ler artigos do sistema de arquivos, comentários de uma API e mesclá-los em nosso código. Se algum conteúdo estiver relacionado a outro tipo de conteúdo, podemos conectá-lo a esse padrão.
Recursos adicionais
Next.js oferece mais informações sobre as funções que usamos em sua página de busca de dados. Inclui links para projetos de amostra que buscam dados de diferentes tipos de fontes.
Se você quiser levar este projeto inicial ainda mais, confira estes artigos:
- Criando um clone de site com truques de CSS com Strapi e Next.js
Substitua os arquivos no sistema de arquivos local por um backend com tecnologia Strapi. - Comparando métodos de estilo no Next.js
Explore diferentes maneiras de escrever CSS personalizado para alterar o estilo deste inicial. - Markdown/MDX com Next.js
Adicione MDX ao seu projeto para que você possa usar os componentes JSX e React em seu Markdown.