Gatsby Workshop

Workshop - Paso 3

Bueno, empecemos a pensar y hacer nuestro blog, qué emoción 😱🎉

Para nuestro blog vamos a escribir los posts en archivos MD (Markdown), porque es un formato bastante amigable y se puede traducir a HTML de forma fácil. De más está decir que para transformar archivos MD a HTML tenemos un plugin, no? 😎 Así que empecemos por ahí, vamos a generar archivos .md, crear la fuente de datos usando un plugin source (el mismo que se usa ahora para las imágenes 😉) para que luego con el plugin de tipo transformer podamos acceder al contenido del archivo y nos convierta la data en algo amigable (HTML) mientras estamos consultando en el frontend usando GraphQL 😊

Si algo no se entendió, recomiendo releer estos últimos renglones, y hasta podrían intentar hacerlo solos si se sienten confiados. Igual, si siguen leyendo, lo hacemos! 💪 Empecemos por buscar un plugin para manipular los archivos Markdown y lo instalamos, encontramos este plugin gatsby-transformer-remark, frenemos el servidor local y lo instalamos así:

npm i gatsby-transformer-remark

Mientras se instala, preparemos nuestro plugin plugin-source-filesystem para levantar nuestros posts de una carpeta local y que sean consultables. Creemos una carpeta dentro de /src, a la misma altura que components, pages, etc. y llamémosla posts por ejemplo. Ahora configuremos nuestro plugin igual a como está para las imágenes, pero para posts y ya que estamos, podemos agregar gatsby-transformer-remark:

//En gatsby-config.js
{
  resolve: `gatsby-source-filesystem`,
  options: {
    name: `posts`,
    path: `${__dirname}/src/posts`,
  },
},
`gatsby-transformer-remark`,

Genial! Con esto ya tenemos configurados los plugins, el de tipo source nos permite ver qué archivos están en esa carpeta posts y automáticamente el de tipo transformer se va a encargar de agarrar los archivos .md y nos va a dejar manipularlos y acceder a su contenido, transformándolo en HTML para manipularlo en el frontend 😁

Les propongo algo, armemos un primer post y después la página donde va a estar el listado de nuestros posts (de cómo mostrar cada post nos encargamos después 😉), así que empecemos por crear en /posts un archivo nuevo con alguna fecha anterior a hoy y un nombre, por ejemplo primer-post.md y llenémoslo con texto, a mi me gusta mucho este lorem ipsum:

---
title: 'Mi primer post'
date: '2019-10-20'
---

# Mi primer post!

_Este_ es un post **BUENISIMO** y lleno de texto de
[este lorem ipsum](https://jeffsum.com/).

God help us, we're in the hands of engineers. Eventually, you do plan to have
dinosaurs on your dinosaur tour, right? Must go faster. Remind me to thank John
for a lovely weekend. You really think you can fly that thing? Remind me to
thank John for a lovely weekend.

Life finds a way. Life finds a way. You know what? It is beets. I've crashed
into a beet truck. Must go faster... go, go, go, go, go! Checkmate... This thing
comes fully loaded. AM/FM radio, reclining bucket seats, and... power windows.
Eventually, you do plan to have dinosaurs on your dinosaur tour, right?

Todo muy lindo pero, qué son esos cosos --- ahí arriba? Eso se llama frontmatter y todo lo que esté ahí adentro nos sirve como una especie de metadata de cada post. En un ratito lo usamos y creo que se va a entender un poco más. Por ahora volvamos a levantar nuestro servidor local de desarrollo (gatsby develop) y vamos a ver si podemos consultar en GraphiQL el post que acabamos de hacer 😁

Si prestamos atención, vemos que se agregaron dos campos nuevos para consultar nuestro markdown, allMarkdownRemark y markdownRemark. Como viene siendo, el que tiene all es para traer todos y el otro es para filtrar uno solo. Nosotros queremos listar todos! Así que listemos toda la data que podamos que nos parezca útil:

Markdown in GraphiQL

Recuerdan que edges representaba la colección y node cada uno de los objetos de la colección, no? Es bueno refrescarlo por las dudas 😊 Además, el plugin nos facilita un excerpt (extracto) como para mostrar en una preview, ahora lo vamos a usar!

FANTASTICO, ya tenemos el backend configurado, la consulta para traer nuestros posts, qué nos faltaría? Una página donde listarlos, no? Creemos entonces en /pages un archivo nuevo que se llame blog.js y completemos como hicimos para mostrar nuestros repos. La estructura y la idea del archivo sería muy parecida, así que les dejo cómo lo hice yo en estas pestañas así pueden intentarlo ustedes 😁

Para ver la solución hacé click en la pestaña 👆
import React from 'react'
import { Link, useStaticQuery, graphql } from 'gatsby'

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

const BlogPage = () => {
  const posts = useStaticQuery(graphql`
    query {
      allMarkdownRemark {
        totalCount
        edges {
          node {
            id
            frontmatter {
              title
              date(formatString: "DD MMMM, YYYY", locale: "es")
            }
            excerpt
          }
        }
      }
    }
  `)
  return (
    <Layout>
      <SEO title="Blog" />
      <h1>Hola</h1>
      <p>Este es mi blog :)</p>
      <p>Tengo en total {posts.allMarkdownRemark.totalCount} post(s)</p>
      {posts.allMarkdownRemark.edges.map(({ node }) => (
        <article
          style={{
            border: '1px solid grey',
            borderRadius: '5px',
            margin: '24px',
            padding: '12px',
          }}
        >
          <h2>{node.frontmatter.title}</h2>
          <i>{node.frontmatter.date}</i>
          <p>{node.excerpt}</p>
        </article>
      ))}
    </Layout>
  )
}

export default BlogPage

Una vez que lo tengamos todo listo, guardamos el archivo, esperamos un poquito a que se haga el rebuild en nuestro servidor local y vamos a nuestra página recién creada a ver qué tan lindo quedó todo! http://localhost:8000/blog

Listo ya tenemos nuestro blog, chau nos vemos (?) jaja nah, todavía falta ver cómo hacemos para ver cada post por separado 😜 Además, no vamos a tener un blog con un solo post, armemos otro! Esta vez usemos la fecha de hoy:

Nombre de ejemplo: segundo-post.md

---
title: 'Segundo post'
date: '2019-10-26'
---

Vengo a hacerle compañía al primer post, que cree que tiene data interesante
pero son palabras sin sentido, pobrecito 😢

Guardamos y debería sumarse a la lista de nuestros posts automáticamente ✨ La magia de Gatsby y GraphQL y todo esto 😍 Pero... El orden de esos posts me parece que está MAL, yo quiero el más nuevo arriba de todo... Ah, sí, tal como usamos filtros para traer nuestros repos antes, tenemos una forma de ordenar los posts (se acuerdan del orderBy que usamos?), cambiemos en la query de nuestro archivo blog.js donde dice solamente allMarkdownRemark por:

allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC })

Sí, entre frontmatter y date hay 3 (tres) guiones bajos, no me miren mal

Y ahora vamos a tener nuestros posts ordenados debidamente, con el más nuevo arriba de todo 😉 Hicimos que se ordene (sort) y le indicamos que use el campo (field) fecha del frontmatter (frontmatter___date) en orden descendiente (order: DESC).

Phew~ cansador esto, no? Pero falta lo mejor! (?) Mostrar los posts uno por uno, que por lo que sabemos hasta ahora, para hacer páginas creamos un archivo en /pages y ya está, aunque crear una página por cada post no suena muy conveniente, no? 🤔 Necesitamos una forma de crear las páginas de manera programática, y para eso tenemos APIs en nuestro backend 😎 Vamos a usar onCreateNode y createPages.

Empecemos por ver para qué usamos cada uno, así no nos mandamos y tenemos que volver para atrás:

  • createPages nos sirve para crear páginas de forma programática, a las cuales se les genera un contexto y nosotros por ese contexto podemos pasarle data de cada post, con qué podríamos traer data de cada post, se les ocurre? 😉 Cuando tengamos eso, nos falta una forma de generar una URL para poder acceder a cada page creada, para eso usamos onCreateNode.
  • onCreateNode vamos a usarlo para que cuando se cree un Nodo, tome ciertos datos únicos de cada post y nos genere algo que se llama slug, que va a ser lo que defina la URL del post.

Creando las URLs

Empecemos por crear la URL para ya tenerla en el contexto cuando creemos cada page, así que entremos a gatsby-node.js e implementemos las APIs, que se implementan exportando funciones con el nombre de la API a utilizar:

exports.onCreateNode = ({ node }) => {
  console.log('Tipo de nodo: ', node.internal.type)
}

Ahora cortemos y volvamos a correr el servidor local, y miremos la consola, si prestamos atención se van a crear distintos tipos de nodos: SitePage, SitePlugin, Directory, File... Pero hay uno que nos interesa más que otros, dijimos que íbamos a trabajar con nuestros posts que son de tipo Markdown, si vemos en la consola lo más parecido es MarkdownRemark! Ya tenemos cómo filtrar nuestros posts entre los nodos creados durante el build, ahora a trabajarlos!

Para generar el slug con algo propio de cada post, podemos utilizar el nombre de archivo, lo cual sería único de cada nodo. Pero para poder acceder a esto, necesitamos movernos en el grafo de nodos (suena de una peli de ciencia ficción 😂) e ir al nodo padre, que es File, el tipo de nodo que contiene la data de nuestros archivos en disco, modifiquemos en nuestra función recién creada:

exports.onCreateNode = ({ node, getNode }) => {
  if (node.internal.type === `MarkdownRemark`) {
    const fileNode = getNode(node.parent)
    console.log(`\n`, fileNode.relativePath)
  }
}

Como estamos modificando los archivos gatsby-algo, por cada modificación que hagamos, si queremos ver los resultados, debemos frenar y volver a correr gatsby develop en la consola. Y vamos a ver los nombres de nuestros archivos flotando por ahí! YAY 🎉🥳

Sigamos, como la gente de Gatsby es buena y nos quiere, nos dice que la lógica para crear estos slugs es complicada, así que el plugin gatsby-source-filesystem ya viene con una función para ayudarnos para "crear el path del archivo", ergo createFilePath:

//Acuerdense que la sintaxis acá es 'node-style'
const { createFilePath } = require(`gatsby-source-filesystem`)

exports.onCreateNode = ({ node, getNode }) => {
  if (node.internal.type === `MarkdownRemark`) {
    console.log(createFilePath({ node, getNode }))
  }
}

Y si reiniciamos el server, vemos los strings! Ya tenemos el dato necesario, lo que tenemos que hacer ahora es sumarlo a la información del nodo, extender el nodo, para que podamos consultar el slug en el frontend. Cuando implementamos una API, recibe como parámetros una colección de actions (acciones) que podemos usar para manipular state, en nuestro caso nos sirve cierta action para extender nuestros nodos: createNodeField.

const { createFilePath } = require(`gatsby-source-filesystem`)

exports.onCreateNode = ({ node, getNode, actions }) => {
  // Conseguimos la action necesaria mediante destructuring
  const { createNodeField } = actions
  if (node.internal.type === `MarkdownRemark`) {
    const slug = createFilePath({ node, getNode })
    // Generamos un nuevo field que extiende nuestro nodo
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
  }
}

Reiniciemos el servidor y vayamos a GraphiQL, deberíamos tener un valor nuevo adentro de cada allMarkdownRemark -> edges -> node que se llame fields! Prueben si trae los slugs que recién creamos y sigamos con la creación de las páginas con createPages! 😁😁

Creando páginas de forma programática

Bueno, venimos bien, agregamos un valor a los nodos de Markdown (nuestros posts), lo cual nos habilita a poder consultar ese nuevo valor y mandarlo al front en cada página mediante createPages. Tengan a mano esto que está en negrita para entender lo que estemos haciendo. El tema es que cada página creada necesita un template (plantilla) donde mostrar la data que consigamos de nuestros posts, así que empecemos por ahí, creemos una carpeta /templates adentro de /src, y en /templates creemos un archivo blog-post.js con lo siguiente:

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

export default () => {
  return (
    <Layout>
      <div>Acá va un post!</div>
    </Layout>
  )
}

Ahora que tenemos una plantilla (que por ahora no hace nada mas que mostrar ese texto siempre), sigamos con lo nuestro, nosotros sabemos que para usar una API en nuestro backend, tenemos que exportar la función con el mismo nombre que la API a usar, así que igual que antes, agregamos exports.createPages pero con una salvedad: entre las cosas que nuestras APIs reciben por parámetro (de las cuales antes usamos node, getNode, actions), vamos a usar graphql, la cual es una función que retorna una Promise,
las cuales se pueden resolver de dos formas, con .then() y .catch() o con async/await. Vamos con la segunda opción ya que de otra forma tendríamos algunos problemas de asincronismo con las funciones que nos provee Gatsby, así que agreguemos esto:

//En gatsby-node.js, abajo de nuestra función exports.createNode:
exports.createPages = async ({ graphql, actions }) => {
  const result = await graphql(`
    query {
      allMarkdownRemark {
        edges {
          node {
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  console.log(JSON.stringify(result, null, 2))
}

Y vamos a ver en la consola el resultado de nuestra query, son los slugs que generamos antes! 😄

Slug in Command Line

Ya tenemos el template y los slugs, ahora unamos todo con una action de createPages que nos permite configurar y pasar data por medio de un context a cada página creada: createPage 😏

Vamos a mandarle en path, la dirección que va a tener cada post, o sea, el slug; también vamos a mandarle en component una referencia al componente template que creamos antes, y por context vamos a mandar el slug, que la data enviada por context va a estar disponible en queries de página como variables de GraphQL. Ah, y para referenciar al componente ahora que estamos desde el backend, necesitamos importar path:

//En gatsby-node.js
//Arriba de todo agregamos esta línea
const path = require(`path`)

//Luego nuestro createPages debería quedar así
exports.createPages = async ({ graphql, actions }) => {
  //Conseguimos nuestra action
  const { createPage } = actions
  const result = await graphql(`
    query {
      allMarkdownRemark {
        edges {
          node {
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  result.data.allMarkdownRemark.edges.forEach(({ node }) => {
    //Configuramos cada página con la data de cada nodo
    createPage({
      path: node.fields.slug,
      component: path.resolve(`./src/templates/blog-post.js`),
      context: {
        slug: node.fields.slug,
      },
    })
  })
}

Nuestro gatsby-node.js debería quedar algo así:

Para ver la solución hacé click en la pestaña 👆
const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)

exports.onCreateNode = ({ node, getNode, actions }) => {
  // Conseguimos la action necesaria mediante destructuring
  const { createNodeField } = actions
  if (node.internal.type === `MarkdownRemark`) {
    const slug = createFilePath({ node, getNode })
    // Generamos un nuevo field que extiende nuestro nodo
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
  }
}

exports.createPages = async ({ graphql, actions }) => {
  //Conseguimos nuestra action
  const { createPage } = actions
  const result = await graphql(`
    query {
      allMarkdownRemark {
        edges {
          node {
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  result.data.allMarkdownRemark.edges.forEach(({ node }) => {
    //Configuramos cada página con la data de cada nodo
    createPage({
      path: node.fields.slug,
      component: path.resolve(`./src/templates/blog-post.js`),
      context: {
        slug: node.fields.slug,
      },
    })
  })
}

Si reiniciamos el servidor local se van a crear nuestras páginas, VAMO! Pero, cómo accedemos? A mano? 🤔 Una forma de ver las páginas creadas es forzar un 404, así que vayamos a una dirección random http://localhost:8000/hn934b3 y vamos a verlas en el listado, háganle click y verán que carga el template que hicimos, que mucho como plantilla no estaría funcionando 🤣 Arreglemos eso!

Lo que podemos hacer ahora es consultar la data de cada post filtrando por su slug! Eso lo hacemos en nuestro template con una query de página:

import React from 'react'
import { graphql } from 'gatsby'
import Layout from '../components/layout'

export default ({ data }) => {
  const post = data.markdownRemark
  return (
    <Layout>
      <div>
        <h1>{post.frontmatter.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.html }} />
      </div>
    </Layout>
  )
}

export const query = graphql`
  query($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      frontmatter {
        title
      }
    }
  }
`

Eh? Query de página??? Sí, en Gatsby hay dos formas de consultar data:

  • Normal (o Page) Queries (Query de página)
  • Static Queries (Query estática)

Una de las principales diferencias entre las dos es que la Query de página puede usarse solamente en los componentes de página, y era la única forma de consultar data en Gatsby v1. Luego desarrollaron la Query estática, que apareció en Gatsby v2 y fue fundamental para eliminar lo que se llama prop drilling, que es pasar data (o state) mediante props de padre a hijos en múltiples niveles, suponiendo que a veces esa data se pasaba unas 3 ó 4 (o más) veces, lo cual volvía el código inmantenible. Las Static Queries pueden utilizarse tanto en componentes comunes como en componentes de página.

Y la última gran diferencia es que la Query de página acepta variables, las cuales se reciben mediante el contexto de página o pageContext, el cual creamos recién en gatsby-node.js en la función createPage. La estática, tal como su nombre indica, no acepta variables porque no tiene acceso al contexto de página.

En la query de página de arriba vemos que se indica que esa query recibe una variable (indicada con el signo $) de nombre slug y de tipo String, el signo de exclamación indica que esa variable es requerida.

Bueno, sigamos con el blog! 😛

Ya casi no nos queda nada! Nos falta vincular cada post del listado con su página creada! Vayamos a blog.js en pages y agreguemos lo que nos falta, se los dejo a ustedes y la solución acá abajo, no pispeen sin intentarlo eh!!! 😝

Para ver la solución hacé click en la pestaña 👆
import React from 'react'
import { Link, useStaticQuery, graphql } from 'gatsby'

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

const BlogPage = () => {
  const posts = useStaticQuery(graphql`
    query {
      allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
        totalCount
        edges {
          node {
            id
            frontmatter {
              title
              date(formatString: "DD MMMM, YYYY", locale: "es")
            }
            fields {
              slug
            }
            excerpt
          }
        }
      }
    }
  `)
  return (
    <Layout>
      <SEO title="Blog" />
      <h1>Hola</h1>
      <p>Este es mi blog :)</p>
      <p>Tengo en total {posts.allMarkdownRemark.totalCount} post(s)</p>
      {posts.allMarkdownRemark.edges.map(({ node }) => (
        <Link
          to={node.fields.slug}
          style={{ textDecoration: 'none', color: '#2f2f2f' }}
        >
          <article
            style={{
              border: '1px solid grey',
              borderRadius: '5px',
              margin: '24px',
              padding: '12px',
            }}
          >
            <h2>{node.frontmatter.title}</h2>
            <i>{node.frontmatter.date}</i>
            <p>{node.excerpt}</p>
          </article>
        </Link>
      ))}
    </Layout>
  )
}

export default BlogPage

Terminado nuestro blog! Ya nos queda ponerlo lindo pero la funcionalidad está! 😀😀😀😀 Espero que hayan disfrutado este workshop, cualquier duda o sugerencia escríbanme y con gusto ayudo en lo que pueda, pueden encontrarme en Twitter como @AgustinDMulet, nos vemos gente!!!

© 2021 - MIT License