Guía básica para empezar con Next.js - Parte 2

Guía básica para empezar con Next.js - Parte 2

Si llegaste hasta aquí significa que disfrutaste la primera parte de la guía básica para comenzar con Next.Js y deseas profundizar su conocimiento, ¿verdad?

La aplicación que construimos es un blog. Ya tenemos lista la estructura de diseño, pero aún no tiene contenido. Por ende, ahora aprenderás cómo obtener datos externos, implementarlos en tu aplicación y crear algunas rutas dinámicas.

¡Empecemos!

Pre-rendering y Data fetching

Antes de hablar sobre data fetching (que es la solicitud que hacemos en la API para consumir ciertos datos), hablemos sobre pre-rendering.

Pre-rendering significa que Next renderizará previamente el HTML de todas las páginas de tu aplicación, en lugar de permitir que JavaScript renderice todo del client-side.

Cada página genera un HTML con la menor cantidad posible de JavaScript asociado y luego, cuando se carga la página, este código JavaScript se ejecuta y hace que la página sea completamente interactiva. Este proceso se llama hidratación.

Como beneficio tendremos un mayor rendimiento y SEO.

Hay dos formas de pre-renderizado que nos ofrece Next.js:

  • Static Generation: es el método de representación previa que genera el HTML en el momento de la compilación del proyecto, y luego el HTML se reutiliza en cada solicitud. Es el método más recomendado, pero hay algunas excepciones, como sitios que necesitan actualizaciones de datos instantáneas.
  • Server-side Rendering: es el método de representación previa que genera el HTML en cada solicitud.

Es interesante decir que Next nos permite elegir cuál método queremos usar o construir una aplicación híbrida usando ambos. Para nuestra aplicación, utilizaremos generación estática o Static Generation.

Static Generation

El método de Static Generation o generación estática puede aplicarse cuando se usan datos de API externos (y cuando no también).

Para este proceso hay una función asíncrona en Next llamada getStaticProps que se ejecuta durante la compilación. Dentro de ella hacemos una llamada a la API para obtener los datos necesarios. Con este resultado en la mano, lo pasaremos como props  a la página donde se utilizará.

¡Usemos esta función en nuestro proyecto y veamos cómo funciona realmente! Pero primero, debemos realizar una configuración arquitectónica.

Crear la arquitectura del blog

Lo haremos un poco diferente al "común": en lugar de consumir las publicaciones de alguna API externa, almacenaremos las publicaciones en un markdown local en nuestra aplicación.

Primero, creemos una carpeta llamada posts en la raíz del proyecto (ten cuidado de no confundirla con la carpeta que tenemos dentro de pages/posts). Dentro de ésta crearemos los archivos:

  • pre-rendering.md
  • ssg-ssr.md

Tus carpetas deberían verse así:

Ahora, dentro de posts/pre-rendering.md inserta el siguiente contenido (en portugués):

---
title: 'Duas formas de pré-renderização'
date: '2022-10-25'
---

O Next.js tem duas formas de pré-renderização: **Static Generation** e **Server-side Rendering**. 
A diferença está em **quando** ele gera o HTML para uma página.

- **Static Generation** é o método de pré-renderização que gera o HTML no **tempo de compilação**. O HTML pré-renderizado é então _reutilizado_ em cada solicitação.
- **Renderização do lado do servidor** é o método de pré-renderização que gera o HTML em **cada solicitação**.

É importante ressaltar que o Next.js permite que você **escolha** qual formulário de pré-renderização usar para cada página.
Você pode criar um aplicativo Next.js "híbrido" usando geração estática para a maioria das páginas e renderização do lado do servidor para outras.

Y dentro de posts/ssg-ssr.md (en portugués):

---
title: 'Quando usar geração estática vs.s. Renderização do lado do servidor'
date: '2020-01-02'
---

Recomendamos usar **Static Generation** (com e sem dados) sempre que possível, pois sua página pode ser criada uma vez e servida por CDN, o que torna muito mais rápido do que ter um servidor para renderizar a página em cada solicitação.

Você pode usar a geração estática para muitos tipos de páginas, incluindo:

- Páginas de marketing
- Postagens no blog
- Listas de produtos de comércio eletrônico
- Ajuda e documentação

Você deve se perguntar: "Posso pré-renderizar esta página **antes** da solicitação de um usuário?" Se a resposta for sim, você deve escolher Geração Estática.

Por outro lado, a geração estática **não** é uma boa ideia se você não puder pré-renderizar uma página antes da solicitação de um usuário. Talvez sua página mostre dados atualizados com frequência e o conteúdo da página mude a cada solicitação.

Nesse caso, você pode usar **Renderização do lado do servidor**. Será mais lento, mas a página pré-renderizada estará sempre atualizada. Ou você pode pular a pré-renderização e usar JavaScript do lado do cliente para preencher os dados.

Es posible que hayas notado que en ambos archivos tenemos una sección de metadatos que contiene title y date. Esto se llama YAML Front Matter. Estos metadatos pueden analizarse utilizando la biblioteca de gray-matter.

💡YAML: se coloca en la parte superior de la página y se usa para asignar metadatos para la página y su contenido.

Para instalar esta biblioteca, ejecuta en la terminal:

yarn add gray-matter

A continuación, vamos a crear una función que:

- Analice cada archivo markdown y devuelva el título, la fecha y el nombre del archivo.

- Muestre los datos de la página ordenados por fecha.

Cree una carpeta llamada lib en la raíz del proyecto y dentro de un archivo posts.js.

Y dentro de ese archivo, tendremos las siguientes configuraciones (en portugués):

import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';

const postsDirectory = path.join(process.cwd(), 'posts');

export function getSortedPostsData() {
  // Pegue o nome dos arquivos dentro de /posts
  const fileNames = fs.readdirSync(postsDirectory);
  const allPostsData = fileNames.map((fileName) => {
    // Remova ".md" do nome do arquivo para pegar o seu id
    const id = fileName.replace(/\\.md$/, '');

    // Leia o arquivo markdown como string
    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');

    // Use gray-matter para analisar os metadados da seção
    const matterResult = matter(fileContents);

    // Combine os dados com o id
    return {
      id,
      ...matterResult.data,
    };
  });
  // Organize os posts pela data
  return allPostsData.sort(({ date: a }, { date: b }) => {
    if (a < b) {
      return 1;
    } else if (a > b) {
      return -1;
    } else {
      return 0;
    }
  });
}

💡 Consejo: ¡no te asustes! ¡No necesitas entender todo en detalle sobre esta configuración para aprender Next.js! Centrémonos en lo esencial :)

Ahora que tenemos nuestros datos de la manera que los necesitamos, los usaremos en el blog junto con la función getStaticProps() de la que hablamos anteriormente.

Implementando getStaticProps()

En el archivo /pages/index.js tenemos que importar la función de configuración getSortedPostsData del archivo que creamos arriba.

Después de abrir el archivo, agregue el siguiente bloque de código al principio del archivo antes de la función Home:

import { getSortedPostsData } from '../lib/posts';

export async function getStaticProps() {
  const allPostsData = getSortedPostsData();
  return {
    props: {
      allPostsData,
    },
  };
}

Aquí, devolvemos allPostsData dentro de un objeto props, que se pasará al componente Home:

export default function Home({ allPostsData }) {...}

Ahora, para acceder a las publicaciones dentro de Home, agreguemos debajo la etiqueta <section> con su presentación:

import Head from 'next/head'
import Layout, { siteTitle } from '../components/layout'
import utilStyles from '../styles/utils.module.css'
import { getSortedPostsData } from '../lib/posts'

export async function getStaticProps() {
  const allPostsData = getSortedPostsData()
  return {
    props: {
      allPostsData,
    },
  }
}

export default function Home({ allPostsData }) {
  return (
    <Layout home>
      <Head>
        <title>{siteTitle}</title>
      </Head>

      // sessão já existente
      <section className={utilStyles.headingMd}>
        <p>[Quem é você]</p>
        <p>
          (Esse é um website de exemplo - você pode ver mais detalhes na própria
          documentação do <a href="<https://nextjs.org/learn>"> Next.js</a>.)
        </p>
      </section>

      // nova sessão
      <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
        <h2 className={utilStyles.headingLg}>Blog</h2>
        <ul className={utilStyles.list}>
          {allPostsData.map(({ id, date, title }) => (
            <li className={utilStyles.listItem} key={id}>
              {title}
              <br />
              {id}
              <br />
              {date}
            </li>
          ))}
        </ul>
      </section>
    </Layout>
  )
}

¡Felicidades! Acabas de realizar una solicitud de un archivo de datos externo y renderizaste previamente la página con esos datos 🥳.

Accede a tu navegador en http://localhost:3000 y ve el resultado:

Este fue el uso de getStaticProps recordando que, además de consumir datos del system (markdown), también podemos solicitar la API externa y la base de datos. Esto solo es posible porque la solicitud ocurre en el lado del servidor.

Recuerda:

  • En un ambiente de desarrollo (yarn dev), getStaticProps se ejecuta en todos los request.
  • En producción, getStaticProps se ejecuta al momento de build.

💡Consejo: getStaticProps solo se puede exportar por página. No puedes exportarlo a ningún archivo que no sea una página.

Hasta ahora ya hemos creado nuestra arquitectura y nuestra página de inicio para el blog, pero aún no tenemos la página para cada publicación.

En el siguiente bloque aprenderemos cómo generar páginas estáticas con rutas dinámicas usando getStaticPaths, cómo conectar rutas dinámicas y algunos detalles más.

¿Vamos?

Rutas dinámicas

Anteriormente creamos una página que depende de datos externos y usamos getStaticProps para solicitar esos datos. Ahora vamos a crear una página donde la ruta también dependerá de datos externos.

La idea es crear una ruta donde path sea /posts/<id>, al tiempo que <id> será el nombre del archivo markdown que creamos, es decir, tendremos /posts/ssg-ssr y /posts/pre-rendering.

Para generar estas páginas donde el <id> será dinámico, seguiremos estos pasos:

  • Crea una página dentro de /pages/posts/[id].js.
  • Esa página deve conter:

1) Un componente React para renderizar la página.

2) getStaticPaths que retorna un array con los ID posibles.

3) getStaticProps para solicitar los datos de los posts con ID.

Vayamos juntos paso a paso para comprender mejor estos conceptos.

Cómo crear páginas estáticas con rutas dinámicas

Para comenzar, creemos un archivo llamado [id].js dentro de /pages/posts y eliminemos el archivo que creamos en el artículo anterior llamado first-post.js.

Dentro de ese archivo coloca la estructura inicial:

import Layout from '../../components/layout'

export default function Post() {
  return <Layout>...</Layout>
}

Ahora, ve al archivo /lib/posts.js y agrega la función getAllPostIds al final del archivo. Esta función devolverá una matriz de objetos con los nombres de los archivos que creamos sin el .md al final:

export function getAllPostsIds() {
  const fileNames = fs.readdirSync(postsDirectory);

  return fileNames.map((fileName) => {
    return {
      params: {
        id: fileName.replace(/\\.md$/, ''),
      }
    }
  })
}

// O resultado deverá ser:
  // [
  //   {
  //     params: {
  //       id: 'ssg-ssr'
  //     }
  //   },
  //   {
  //     params: {
  //       id: 'pre-rendering'
  //     }
  //   }
  // ]

Ahora importemos la función getAllPostsIds y usémosla dentro de getStaticPaths en el archivo [id].js.

Al principio del archivo, antes de la función Post poner:

import { getAllPostIds } from '../../lib/posts';

export async function getStaticPaths() {
  const paths = getAllPostsIds();
  return {
    paths,
    fallback: false,
  };
}

Dentro de las rutas constantes tendremos el resultado que esperamos de getAllPostIds(). Sobre el respaldo, hablaremos más tarde.

¡Casi listo! Ahora implementemos getStaticProps.

Necesitamos hacer una solicitud para obtener los datos necesarios de la publicación con el id suministrado. Para hacer esto, vayamos al archivo /lib/posts.js una vez más y agreguemos la función getPostData al final del archivo. Esta función nos traerá la publicación en función de tu id.

export function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  // Use gray-matter para analisar a seção de metadados
  const matterResult = matter(fileContents);

  // Aqui fazemos a junção dos dados com o id
  return {
    id,
    ...matterResult.data,
  };
}

Y luego, en pages/posts/[id].js llamaremos a esta nueva función y cambiaremos el cuerpo del componente con la información que obtuvimos.

Tu página debería verse así:

import Layout from '../../components/layout'

import { getAllPostsIds, getPostData } from '../../lib/posts'

export async function getStaticPaths() {
  const paths = getAllPostsIds()
  return {
    paths,
    fallback: false,
  }
}

export async function getStaticProps({ params }) {
  const postData = getPostData(params.id)
  return {
    props: {
      postData,
    },
  }
}

export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
    </Layout>
  )
}

Ve en tu navegador a:

El resultado debería ser éste:

¡Excelente! ¡Acabas de construir tu primera ruta dinámica! 😊

Pero aún no hemos renderizado el contenido de la página. ¡Hagámoslo!

Renderizar el contenido del blog

Para representar el contenido del blog que proviene de un markdown, usaremos una biblioteca llamada remark.

Inserta lo siguiente:

yarn add remark remark-html

Abre lib/posts.js y agrega en el tope de la página:

import { remark } from 'remark';
import html from 'remark-html';

Actualiza la función getPostData con el siguiente código:

export async function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')

  // Use gray-matter para analisar a seção de metadados
  const matterResult = matter(fileContents)

  // O remark converte o markdown em uma string HTML
  const processedContent = await remark()
    .use(html)
    .process(matterResult.content)
  const contentHtml = processedContent.toString()

  // Aqui fazemos a junção dos dados com o id e com o HTML
  return {
    id,
    contentHtml,
    ...matterResult.data,
  }
}

💡 Ten en cuenta que cambiamos la función a una función asíncrona usando async porque el comentario necesita usar await. ¡Async/Await nos permite solicitar datos de forma asíncrona!

Siendo así, necesitamos actualizar nuestra función getStatisProps en pages/posts/[id].js para usar también await al llamar a la función getPostData:

export async function getStaticProps({ params }) {
  const postData = **await** getPostData(params.id)
  return {
    props: {
      postData,
    },
  }
}

A continuación, actualiza el cuerpo del componente para renderizar contentHtml usando dangerouslySetInnerHTML (haz clic para obtener más información):

export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
      <br />
      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
    </Layout>
  );
}

Visitando la página de publicaciones, el resultado será:

¡Increíble! ¡Pocas líneas de código y un muy buen resultado! Pero aún podemos hacerlo mejor.

Mejorar la página de posts

  • Agregar el tag <title>

Como mencionamos en el artículo anterior, Next proporciona algunos componentes, como <Head> por ejemplo. Agreguemos este tag al título de nuestro blog en pages/posts/[id].js:

// Lembre-se de importar o componente
import Head from 'next/head';

export default function Post({ postData }) {
  return (
    <Layout>
      {/* Adicione a tag <Head> */}
      <Head>
        <title>{postData.title}</title>
      </Head>

			{/* Mantenha o restante do código */}
				{postData.title}
    </Layout>
  );
}

Anota el nombre de tu pestaña en el navegador. Además de esta mejora visual, el tag nos permite agregar otras metatags que ayudan con el rendimiento de SEO. Por si no lo recuerdas, hablamos de ello en el artículo anterior, que puedes ver aquí.

  • Formateando la fecha

En nuestro blog, mostramos la fecha de la siguiente manera: 2022-10-25. Un poco raro de leer, pero lo mejoraremos usando una biblioteca llamada date-fns.

Instala lo siguiente:

yarn add date-fns

Ahora, en la raíz del proyecto, cree una carpeta llamada utils. Generalmente, en esta carpeta creamos archivos para guardar algunas funciones o constantes que serán bien utilizadas en cada aplicación.

Crea date.js dentro de utils y agrega el código:

import { parseISO, format } from 'date-fns'

export default function Date({ dateString }) {
  const date = parseISO(dateString)
  return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
}

E importa dentro de pages/posts/[id].js la función que acabamos de crear, la cual usaremos donde tenemos la fecha de nuestro archivo:

// Import
import Date from '../../utils/date'

export default function Post({ postData }) {
  return (
    <Layout>
      {/* Mantenha o código existente */}

      {/* Substitua {postData.date} por isso */}
      <Date dateString={postData.date} />

      {/* Mantenha o restante do código */}
    </Layout>
  );
}

Si vas a http://localhost:3000/posts/pre-rendering, verás que la fecha ahora tiene el formato "25 de octubre de 2021".

💡Desafío: Ingresa a la documentación de date-fns y busca locale. Intenta integrar en su función y cambia el idioma de la fecha.

  • Añadir CSS

Finalmente, agreguemos algunos estilos a la página usando el archivo que creamos anteriormente en styles/utils.module.css.

Abre la página de publicaciones, importa el archivo CSS y reemplaza el contenido del componente con:

import utilStyles from '../../styles/utils.module.css'

export default function Post({ postData }) {
  return (
    <Layout>
      <Head>
        <title>{postData.title}</title>
      </Head>
      <article>
        <h1 className={utilStyles.headingXl}>{postData.title}</h1>
        <div className={utilStyles.lightText}>
          <Date dateString={postData.date} />
        </div>
        <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
      </article>
    </Layout>
  )
}

¡Ahora sí, tenemos una página mucho más bonita!

Mejorar la página inicial

Hasta ahora hemos trabajado en la página de publicaciones, desde la solicitud de datos hasta su estilo. Ahora, concentrémonos en la página de inicio y hagámosla más hermosa y funcional.

  • Agregar el tag <Link>

Al igual que el componente <Head>, también hay un componente  que funcionará como una etiqueta de anclaje o anchor tag (<a>).

Importa este componente al principio del archivo en pages/index.js y agrégalo al título de la publicación. De esta forma podremos navegar entre páginas:

import Link from 'next/link'

<li className={utilStyles.listItem} key={id}>
  <Link href={`/posts/${id}`}>
    <a>{title}</a>
  </Link>
</li>
  • Formatea la fecha

¿Recuerdas la función que creamos en utils/date.js? ¡También será útil para esta página!

import Date from '../utils/date'

<li className={utilStyles.listItem} key={id}>
  <small className={utilStyles.lightText}>
    <Date dateString={date} />
  </small>
</li>

El código final quedará así:

import Head from 'next/head'
import Link from 'next/link'
import Layout, { siteTitle } from '../components/layout'
import utilStyles from '../styles/utils.module.css'
import { getSortedPostsData } from '../lib/posts'
import Date from '../utils/date'

export async function getStaticProps() {
  const allPostsData = getSortedPostsData()
  return {
    props: {
      allPostsData,
    },
  }
}

export default function Home({ allPostsData }) {
  return (
    <Layout home>
      <Head>
        <title>{siteTitle}</title>
      </Head>

      <section className={utilStyles.headingMd}>
        <p>[Quem é você]</p>
        <p>
          (Esse é um website de exemplo - você pode ver mais detalhes na própria
          documentação do <a href="<https://nextjs.org/learn>"> Next.js</a>.)
        </p>
      </section>

      <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
        <h2 className={utilStyles.headingLg}>Blog</h2>
        <ul className={utilStyles.list}>
          {allPostsData.map(({ id, date, title }) => (
            <li className={utilStyles.listItem} key={id}>
              <Link href={`/posts/${id}`}>
                <a>{title}</a>
              </Link>
              <br />
              <small className={utilStyles.lightText}>
                <Date dateString={date} />
              </small>
            </li>
          ))}
        </ul>
      </section>
    </Layout>
  )
}

Mira tu página:

Genial, ¿verdad?

Casi terminamos, pero primero quiero hablar sobre algunos puntos más de las rutas dinámicas en Next.

Detalles sobre rutas dinámicas

  • Requisición de datos desde API externa o base de datos

Al igual que la función getStaticProps, getStaticPaths también puede realizar solicitudes a cualquier tipo de fuente de datos.

En nuestro ejemplo, la función getAllPostsIds podría realizar una solicitud externa de la siguiente manera:

export async function getAllPostIds() {
  **const res = await fetch('..');**
  const posts = await res.json();
  return posts.map((post) => {
    return {
      params: {
        id: post.id,
      },
    };
  });
}

¡Muy simple! En tu próximo proyecto puedes usar este ejemplo 😊.

  • Fallback

¿Recuerdas que cuando creamos la función getStaticPaths, usamos un fallback: false? Esto quiere decir que, si accedes a una ruta que no existe dentro de getStaticPaths, el resultado será una página 404 que ya está lista por defecto con Next, pero también puedes crear una en pages/404.js.

Si el resultado del fallback fuera blocking, las nuevas rutas se representarán en el lado del servidor usando getStaticProps y se almacenarán en caché en caso de que esto suceda en el futuro.

Esto va un poco más allá de nuestra introducción a Next, pero si deseas conocer más, visita la documentación oficial aquí.

Si desea acceder al enrutador Next.js, puede importar el enlace useRouter desde next/router y usarlo para realizar funciones como:

const handleClick = (e) => {
    router.push('/posts/minha-pagina')
  }

Conclusión

¡Yeii! ¡Terminamos la parte II y lograste crear una aplicación en Next.js aplicando los conceptos principales y más importantes!

Espero que lo hayas disfrutado y que, a partir de ahora, quieras aprender aún más sobre este universo.

Si tienes alguna pregunta, contáctame por correo electrónico (malone.nykolle@gmail.com) o búscame en LinkedIn.

¡Muchas gracias por leerme!

¡Éxito! 😊

⚠️
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Revelo.

Revelo Content Network da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.