Creación de un blog de varios autores con Next.js

Publicado: 2022-03-10
Resumen rápido ↬ Este artículo explica cómo podemos conectar diferentes tipos de contenido en una aplicación Next.js. Con esta técnica, podemos agregar cualquier tipo de relación uno a uno, uno a muchos o incluso muchos a muchos a nuestros proyectos.

En este artículo, vamos a crear un blog con Next.js que admita dos o más autores. Atribuiremos cada publicación a un autor y mostraremos su nombre e imagen con sus publicaciones. Cada autor también obtiene una página de perfil, que enumera todas las publicaciones que contribuyeron. Se verá algo como esto:

A la izquierda: el índice del blog terminado que vamos a construir. A la derecha: la página de una publicación individual, que enlaza con la página de perfil de su autor.
A la izquierda: el índice del blog terminado que vamos a construir. A la derecha: la página de una publicación individual, que enlaza con la página de perfil de su autor. (Vista previa grande)
La página de perfil de un autor, con enlaces a todas sus publicaciones.
La página de perfil de un autor, con un enlace a todas sus publicaciones. (Vista previa grande)

Vamos a mantener toda la información en archivos en el sistema de archivos local. Los dos tipos de contenido, publicaciones y autores, utilizarán diferentes tipos de archivos. Las publicaciones con mucho texto usarán Markdown, lo que permitirá un proceso de edición más fácil. Debido a que la información sobre los autores es más liviana, la mantendremos en archivos JSON. Las funciones auxiliares facilitarán la lectura de diferentes tipos de archivos y la combinación de su contenido.

Next.js nos permite leer datos de diferentes fuentes y de diferentes tipos sin esfuerzo. Gracias a su enrutamiento dinámico y next/link , podemos crear y navegar rápidamente a las distintas páginas de nuestro sitio. También obtenemos optimización de imágenes de forma gratuita con el paquete next/image .

Al elegir las "baterías incluidas" Next.js, podemos centrarnos en nuestra propia aplicación. No tenemos que perder tiempo en el trabajo preliminar repetitivo que a menudo vienen con los nuevos proyectos. En lugar de construir todo a mano, podemos confiar en el marco probado y probado. La comunidad grande y activa detrás de Next.js hace que sea fácil obtener ayuda si nos encontramos con problemas en el camino.

Después de leer este artículo, podrá agregar muchos tipos de contenido a un solo proyecto Next.js. También podrás crear relaciones entre ellos. Eso le permite vincular cosas como autores y publicaciones, cursos y lecciones, o actores y películas.

Este artículo asume una familiaridad básica con Next.js. Si no lo ha usado antes, es posible que desee leer primero cómo maneja las páginas y obtiene datos para ellas.

No cubriremos el estilo en este artículo y nos centraremos en hacer que todo funcione. Puede obtener el resultado en GitHub. También hay una hoja de estilo que puede colocar en su proyecto si desea seguir este artículo. Para obtener el mismo marco, incluida la navegación, reemplace sus pages/_app.js con este archivo.

¡Más después del salto! Continúe leyendo a continuación ↓

Configuración

Comenzamos configurando un nuevo proyecto usando create-next-app y cambiando a su directorio:

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

Tendremos que leer los archivos Markdown más tarde. Para hacerlo más fácil, también agregamos algunas dependencias más antes de comenzar.

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

Una vez que se completa la instalación, podemos ejecutar el dev de desarrollo para iniciar nuestro proyecto:

 multiauthor-blog$ yarn dev

Ahora podemos explorar nuestro sitio. En su navegador, abra https://localhost:3000. Debería ver la página predeterminada agregada por create-next-app.

La página predeterminada creada por create-next-app.
Si ve esto, su configuración funciona. (Vista previa grande)

En un momento, necesitaremos una navegación para llegar a nuestras páginas. Podemos agregarlos en pages/_app.js incluso antes de que existan las páginas.

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

A lo largo de este artículo, agregaremos estas páginas faltantes a las que apunta la navegación. Primero agreguemos algunas publicaciones para que tengamos algo con lo que trabajar en una página de descripción general del blog.

Creación de publicaciones

Para mantener nuestro contenido separado del código, pondremos nuestras publicaciones en un directorio llamado _posts/ . Para facilitar la escritura y la edición, crearemos cada publicación como un archivo Markdown. El nombre de archivo de cada publicación servirá como slug en nuestras rutas más adelante. Se podrá acceder al archivo _posts/hello-world.md en /posts/hello-world , por ejemplo.

Cierta información, como el título completo y un breve extracto, va en el frontmatter al comienzo del archivo.

 --- 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, …

Agregue algunos archivos más como este para que el blog no comience vacío:

 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/ └─ …

Puede agregar los suyos propios o tomar estas publicaciones de muestra del repositorio de GitHub.

Listado de todas las publicaciones

Ahora que tenemos algunas publicaciones, necesitamos una forma de incluirlas en nuestro blog. Comencemos agregando una página que los enumere a todos, que sirva como índice de nuestro blog.

En Next.js, se podrá acceder a un archivo creado en pages/posts/index.js como /posts en nuestro sitio. El archivo debe exportar una función que servirá como el cuerpo de esa página. Su primera versión se parece a esto:

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

No llegamos muy lejos porque todavía no tenemos una forma de leer los archivos de Markdown. Ya podemos navegar hasta https://localhost:3000/posts, pero solo vemos el encabezado.

Una página vacía con un encabezado que dice "Publicaciones".
Podemos acceder a nuestra página y podemos empezar a llenarla de vida. (Vista previa grande)

Ahora necesitamos una forma de publicar nuestras publicaciones allí. Next.js usa una función llamada getStaticProps() para pasar datos a un componente de página. La función pasa los props en el objeto devuelto al componente como accesorios.

Desde getStaticProps() , vamos a pasar las publicaciones al componente como un accesorio llamado posts . Codificaremos dos publicaciones de marcador de posición en este primer paso. Al comenzar de esta manera, definimos en qué formato queremos recibir las publicaciones reales más tarde. Si una función auxiliar las devuelve en este formato, podemos cambiar a ella sin cambiar el componente.

La descripción general de la publicación no mostrará el texto completo de las publicaciones. Para esta página, el título, el extracto, el enlace permanente y la fecha de cada publicación son suficientes.

 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 la conexión, podemos tomar las publicaciones de los accesorios y mostrarlas en el componente Posts . Incluiremos el título, la fecha de creación, un extracto y un enlace a la publicación. Por ahora, ese enlace no llevará a ninguna parte todavía.

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

Después de volver a cargar la página en el navegador, ahora muestra estas dos publicaciones:

Una lista de nuestras dos publicaciones de marcador de posición.
La conexión funciona. Ahora podemos trabajar para poner publicaciones reales aquí. (Vista previa grande)

No queremos codificar todas nuestras publicaciones de blog en getStaticProps() para siempre. Después de todo, es por eso que creamos todos estos archivos en el directorio _posts/ anteriormente. Ahora necesitamos una forma de leer esos archivos y pasar su contenido al componente de la página.

Hay algunas maneras en que podríamos hacer eso. Podríamos leer los archivos directamente en getStaticProps() . Debido a que esta función se ejecuta en el servidor y no en el cliente, tenemos acceso a los módulos nativos de Node.js como fs . Podríamos leer, transformar e incluso manipular archivos locales en el mismo archivo que mantenemos como componente de la página.

Para mantener el archivo corto y enfocado en una tarea, vamos a mover esa funcionalidad a un archivo separado. De esa forma, el componente Posts solo necesita mostrar los datos, sin tener que leerlos también. Esto agrega algo de separación y organización a nuestro proyecto.

Por convención, vamos a colocar funciones que leen datos en un archivo llamado lib/api.js . Ese archivo contendrá todas las funciones que capturan nuestro contenido para los componentes que lo muestran.

Para la página de resumen de publicaciones, queremos una función que lea, procese y devuelva todas las publicaciones. Lo llamaremos getAllPosts() . En él, primero usamos path.join() para construir la ruta al directorio _posts/ . Luego usamos fs.readdirSync() para leer ese directorio, lo que nos da los nombres de todos los archivos que contiene. Mapeando estos nombres, luego leemos cada archivo a su 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 }) }

Después de leer el archivo, obtenemos su contenido como una cadena larga. Para separar el frontmatter del texto de la publicación, ejecutamos esa cadena a través gray-matter . También vamos a tomar el slug de cada publicación eliminando el .md del final de su nombre de archivo. Necesitamos ese slug para crear la URL desde la cual se podrá acceder a la publicación más adelante. Dado que no necesitamos el cuerpo Markdown de las publicaciones para esta función, podemos ignorar el contenido 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 cómo distribuimos ...data en el objeto devuelto aquí. Eso nos permite acceder a los valores de su materia prima como {post.title} en lugar de {post.data.title} más adelante.

De vuelta en nuestra página de resumen de publicaciones, ahora podemos reemplazar las publicaciones de marcador de posición con esta nueva función.

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

Después de recargar el navegador, ahora vemos nuestras publicaciones reales en lugar de los marcadores de posición que teníamos antes.

Una lista de nuestras publicaciones de blog reales.
Gracias a la función de ayuda, esta página ahora muestra nuestras publicaciones reales. (Vista previa grande)

Agregar páginas de publicaciones individuales

Los enlaces que agregamos a cada publicación aún no conducen a ninguna parte. Todavía no hay ninguna página que responda a URL como /posts/hello-world . Con el enrutamiento dinámico, podemos agregar una página que coincida con todas las rutas como esta.

Un archivo creado como pages/posts/[slug].js coincidirá con todas las URL que se parecen a /posts/abc . El valor que aparece en lugar de [slug] en la URL estará disponible para la página como parámetro de consulta. Podemos usar eso en getStaticProps() de la página correspondiente como params.slug para llamar a una función auxiliar.

Como contraparte de getAllPosts() , llamaremos a esa función auxiliar getPostBySlug(slug) . En lugar de todas las publicaciones, devolverá una sola publicación que coincida con el slug que le pasamos. En la página de una publicación, también debemos mostrar el contenido Markdown del archivo subyacente.

La página para publicaciones individuales se parece a la de la descripción general de la publicación. En lugar de pasar posts a la página en getStaticProps() , solo pasamos una sola post . Primero hagamos la configuración general antes de ver cómo transformar el cuerpo Markdown de la publicación en HTML utilizable. Vamos a omitir la publicación de marcador de posición aquí, usando la función de ayuda que agregaremos en el siguiente paso de inmediato.

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

Ahora tenemos que agregar la función getPostBySlug(slug) a nuestro archivo auxiliar lib/api.js . Es como getAllPosts() , con algunas diferencias notables. Debido a que podemos obtener el nombre de archivo de la publicación del slug, no necesitamos leer todo el directorio primero. Si el slug es 'hello-world' , vamos a leer un archivo llamado _posts/hello-world.md . Si ese archivo no existe, Next.js mostrará una página de error 404.

Otra diferencia con getAllPosts() es que esta vez, también necesitamos leer el contenido Markdown de la publicación. Podemos devolverlo como HTML listo para renderizar en lugar de Markdown sin procesar procesándolo primero con un 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, + } +}

En teoría, podríamos usar la función getAllPosts() dentro getPostBySlug(slug) . Primero obtendríamos todas las publicaciones con él, que luego podríamos buscar una que coincida con el slug dado. Eso significaría que siempre tendríamos que leer todas las publicaciones antes de poder obtener una sola, lo cual es un trabajo innecesario. getAllPosts() tampoco devuelve el contenido Markdown de las publicaciones. Podríamos actualizarlo para hacer eso, en cuyo caso haría más trabajo del que necesita actualmente.

Debido a que las dos funciones auxiliares hacen cosas diferentes, las mantendremos separadas. De esa manera, podemos enfocar las funciones exactamente y solo en el trabajo que necesitamos que haga cada uno de ellos.

Las páginas que usan enrutamiento dinámico pueden proporcionar un getStaticPaths() junto a su getStaticProps() . Esta función le dice a Next.js para qué valores de los segmentos de ruta dinámica debe crear páginas. Podemos proporcionarlos usando getAllPosts() y devolviendo una lista de objetos que definen el slug de cada publicación.

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

Dado que analizamos el contenido de Markdown en getPostBySlug(slug) , ahora podemos representarlo en la página. Necesitamos usar dangerouslySetInnerHTML SetInnerHTML para este paso para que Next.js pueda representar el HTML detrás de post.body . A pesar de su nombre, es seguro usar la propiedad en este escenario. Debido a que tenemos control total sobre nuestras publicaciones, es poco probable que inyecten 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() { … }

Si seguimos uno de los enlaces de la descripción general de la publicación, ahora llegamos a la página de esa publicación.

La página de una publicación individual.
Podemos mostrar el contenido de la publicación, pero aún no sabemos quién la escribió. (Vista previa grande)

Adición de autores

Ahora que tenemos las publicaciones conectadas, debemos repetir los mismos pasos para nuestros autores. Esta vez, usaremos JSON en lugar de Markdown para describirlos. Podemos mezclar diferentes tipos de archivos en un mismo proyecto como este siempre que tenga sentido. Las funciones auxiliares que usamos para leer los archivos se encargan de cualquier diferencia por nosotros. Las páginas pueden usar estas funciones sin saber en qué formato almacenamos nuestro contenido.

Primero, cree un directorio llamado _authors/ y agréguele algunos archivos de autor. Como hicimos con las publicaciones, nombra los archivos por el slug de cada autor. Lo usaremos para buscar autores más tarde. En cada archivo, especificamos el nombre completo de un autor en un objeto JSON.

 { "name": "Adrian Webber" }

Por ahora, tener dos autores en nuestro proyecto es suficiente.

Para darles un poco más de personalidad, agreguemos también una imagen de perfil para cada autor. Pondremos esos archivos estáticos en el directorio public/ . Al nombrar los archivos con el mismo slug, podemos conectarlos usando solo la convención implícita. Podríamos agregar la ruta de la imagen al archivo JSON de cada autor para vincular los dos. Al nombrar todos los archivos por slugs, podemos administrar esta conexión sin tener que escribirla. Los objetos JSON solo necesitan contener información que no podemos compilar con código.

Cuando haya terminado, el directorio de su proyecto debería verse así.

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

Al igual que con las publicaciones, ahora necesitamos funciones auxiliares para leer todos los autores y obtener autores individuales. Las nuevas funciones getAllAuthors() y getAuthorBySlug(slug) también van en lib/api.js . Hacen casi exactamente lo mismo que sus contrapartes posteriores. Debido a que usamos JSON para describir a los autores, no necesitamos analizar ningún Markdown con remark aquí. Tampoco necesitamos gray-matter frontal. En su lugar, podemos usar el JSON.parse() incorporado de JavaScript para leer el contenido de texto de nuestros archivos en 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" }

Con ese conocimiento, nuestras funciones auxiliares se ven así:

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

Con una forma de leer autores en nuestra aplicación, ahora podemos agregar una página que los enumere a todos. Crear una nueva página en pages/authors/index.js nos da una página de /authors en nuestro sitio.

Las funciones auxiliares se encargan de leer los archivos por nosotros. Este componente de página no necesita saber que los autores son archivos JSON en el sistema de archivos. Puede usar getAllAuthors() sin saber dónde o cómo obtiene sus datos. El formato no importa siempre que nuestras funciones auxiliares devuelvan sus datos en un formato con el que podamos trabajar. Abstracciones como esta nos permiten mezclar diferentes tipos de contenido en nuestra aplicación.

La página de índice para autores se parece mucho a la de las publicaciones. Obtenemos todos los autores en getStaticProps() , que los pasa al componente Authors . Ese componente mapea sobre cada autor y enumera alguna información sobre ellos. No necesitamos construir ningún otro enlace o URL desde el slug. La función auxiliar ya devuelve a los autores en un formato utilizable.

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

Si visitamos /authors en nuestro sitio, vemos una lista de todos los autores con sus nombres e imágenes.

La lista de autores.
Podemos enumerar a todos los autores, pero no tenemos forma de saber cuántos artículos contribuyeron. (Vista previa grande)

Los enlaces a los perfiles de los autores aún no conducen a ninguna parte. Para agregar las páginas de perfil, creamos un archivo en pages/authors/[slug].js . Debido a que los autores no tienen ningún contenido de texto, todo lo que podemos agregar por ahora son sus nombres y fotos de perfil. También necesitamos otro getStaticPaths() para decirle a Next.js para qué slugs debe crear 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, }, })), } }

Con esto, ahora tenemos una página de perfil de autor básica que es muy ligera en información.

La página de perfil de un autor, que muestra su nombre y foto de rostro.
La página de perfil de un autor está casi vacía en este momento. (Vista previa grande)

En este punto, los autores y las publicaciones aún no están conectados. Construiremos ese puente a continuación para que podamos agregar una lista de las publicaciones de cada autor a sus páginas de perfil.

Conexión de publicaciones y autores

Para conectar dos piezas de contenido, necesitamos hacer referencia a una en la otra. Dado que ya identificamos las publicaciones y los autores por sus slugs, los haremos referencia con eso. Podríamos agregar autores a publicaciones y publicaciones a autores, pero una dirección es suficiente para vincularlos. Como queremos atribuir las publicaciones a los autores, vamos a agregar el slug del autor al frente de cada publicación.

 --- 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, …

Si lo mantenemos así, ejecutar la publicación a través gray-matter agrega el campo de autor a la publicación como una cadena:

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

Para obtener el objeto que representa al autor, podemos usar ese slug y llamar a getAuthorBySlug(slug) con él.

 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 agregar el autor a la página de una sola publicación, debemos llamar a getAuthorBySlug(slug) una vez en 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 cómo propagamos ...post en un objeto también llamado post en getStaticProps() . Al colocar author después de esa línea, terminamos reemplazando la versión de cadena del autor con su objeto completo. Eso nos permite acceder a las propiedades de un autor a través de post.author.name en el componente Post .

Con ese cambio, ahora tenemos un enlace a la página de perfil del autor, completo con su nombre y foto, en la página de una publicación.

La página de la publicación finalizada, que ahora incluye el nombre del autor y una foto de su rostro.
La publicación ahora se atribuye correctamente al autor. (Vista previa grande)

Agregar autores a la página de descripción general de la publicación requiere un cambio similar. En lugar de llamar a getAuthorBySlug(slug) una vez, necesitamos mapear todas las publicaciones y llamarlo para cada una de ellas.

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

Eso agrega los autores a cada publicación en la descripción general de la publicación:

Una lista de publicaciones de blog, incluidos los nombres y fotografías de sus autores.
Esto parece un blog real ahora. (Vista previa grande)

No necesitamos agregar una lista de las publicaciones de un autor a su archivo JSON. En sus páginas de perfil, primero obtenemos todas las publicaciones con getAllPosts() . Luego podemos filtrar la lista completa por los atribuidos 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() { … }

Esto nos da una lista de artículos en la página de perfil de cada autor.

La página de perfil de un autor, que ahora incluye una lista de enlaces a sus publicaciones.
Ahora podemos enumerar y vincular las publicaciones de cada autor. (Vista previa grande)

En la página de descripción general del autor, solo agregaremos cuántas publicaciones han escrito para no saturar la interfaz.

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

Con eso, la página de resumen de Autores muestra cuántas publicaciones ha contribuido cada autor.

La lista de autores con su número de publicaciones.
Ahora podemos poner su número de publicaciones aportadas con la entrada de cada autor. (Vista previa grande)

¡Y eso es! Las publicaciones y los autores están completamente vinculados ahora. Podemos pasar de una publicación a la página de perfil de un autor y de ahí a sus otras publicaciones.

Resumen y perspectiva

En este artículo, conectamos dos tipos de contenido relacionados a través de sus slugs únicos. Definir la relación entre la publicación y el autor permitió una variedad de escenarios. Ahora podemos mostrar el autor en cada publicación y enumerar sus publicaciones en sus páginas de perfil.

Con esta técnica, podemos añadir muchos otros tipos de relaciones. Cada publicación puede tener un revisor además de un autor. Podemos configurar eso agregando un campo de reviewer al frente de una publicación.

 --- 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, …

En el sistema de archivos, el revisor es otro autor del directorio _authors/ . También podemos usar getAuthorBySlug(slug) para obtener su información.

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

Incluso podríamos apoyar a los coautores nombrando a dos o más autores en una publicación en lugar de solo a una persona.

 --- 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, …

En este escenario, ya no podíamos buscar un solo autor en getStaticProps() de una publicación. En su lugar, haríamos un mapa sobre esta matriz de autores para obtenerlos a todos.

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

También podemos producir otro tipo de escenarios con esta técnica. Permite cualquier tipo de relación uno a uno, uno a muchos o incluso muchos a muchos. Si su proyecto también presenta boletines y estudios de casos, también puede agregar autores a cada uno de ellos.

En un sitio sobre el universo Marvel, podríamos conectar personajes y las películas en las que aparecen. En los deportes, podríamos conectar jugadores y los equipos en los que juegan actualmente.

Debido a que las funciones auxiliares ocultan la fuente de datos, el contenido podría provenir de diferentes sistemas. Podríamos leer artículos del sistema de archivos, comentarios de una API y fusionarlos en nuestro código. Si algún contenido se relaciona con otro tipo de contenido, podemos conectarlos con este patrón.

Más recursos

Next.js ofrece más antecedentes sobre las funciones que usamos en su página sobre Obtención de datos. Incluye enlaces a proyectos de muestra que obtienen datos de diferentes tipos de fuentes.

Si desea llevar más lejos este proyecto inicial, consulte estos artículos:

  • Creación de un clon de sitio web de trucos CSS con Strapi y Next.js
    Reemplace los archivos en el sistema de archivos local con un backend impulsado por Strapi.
  • Comparación de métodos de estilo en Next.js
    Explore diferentes formas de escribir CSS personalizado para cambiar el estilo de este iniciador.
  • Markdown/MDX con Next.js
    Agregue MDX a su proyecto para que pueda usar los componentes JSX y React en su Markdown.