Crear un blog con Gatsby.js

25 de julio 2021ComentariosjavascriptDavid Poza SuárezComentarios

Llevaba ya un tiempo queriendo probar Gastby, asi que ni corto ni perezoso he preparado un remake del blog, que antes corría sobre wordpress.

Gatsby es un generador de sitios estáticos escrito en javascript sobre node.js, que nos permite escribir componentes React y gestionar el modelo de datos mediante graphQL. De este modo se obtiene una experiencia muy grata como desarrollador, a la vez que unos sitios bastante optimizados en cuanto a performance, y que podremos servir desde cualquier alojamiento, sin necesidad de grandes prestaciones, por ejemplo github pages o netlify. A este concepto de generación de páginas estáticas donde toda la lógica está en el cliente, y que habitualmente se despligan automáticamente, se le ha llamado arquitectura https://en.wikipedia.org/wiki/Jamstack y podemos decir que está de moda. Otra tecnología equivalente, que también pertenece a este stack, es Jekyll escrito en ruby.

Este software es adecuado para tu proyecto si su contenido no se actualiza con mucha frecuencia o si el mismo no cambia dinámicamente con la interacción del usuario. Para estos casos sería mejor usar Next.js, que también trabaja con React pero que si nos proporciona un SSR propiamente dicho que es dinámico.

Gatsby nos ofrece una serie de configuraciones iniciales o plantillas para comenzar ya con una estructura mínima/scafolding, de hecho existe una para blogs: https://github.com/gatsbyjs/gatsby-starter-blog. No obstante lo que nos interesa aquí es conocer un poco las tripas del sistema, asi que vamos a optar como comenzar con un proyecto totalmente vacío e ir añadiendo todas las configuraciones necesarias hasta lograr un blog totalmente funcional como el que estas leyendo.

Un poco de CLI

Lo primero será enumerar los comandos esenciales que nos provee el comando gatsby.

  • gatsby new: Inicial un proyecto nuevo. Admite como parámetro la url de un repositorio con un starter pack.
  • gatsby develop: Levanta el servidor de desarrollo, por defecto en el puerto 8080
  • gatsby build: Compila el código listo para poner en producción
  • gatsby serve: Sirve una compilación hecha con el comando anterior.
  • gatsby clean: Limpia la cache.
  • gatsby info: Imprime el listado de paquetes npm instalados en el proyecto

Estructura de directorios

Asi es como lo tengo organizado yo:

- src
  - components
    - componente1
      - index.jsx
      - styles.module.scss
  - fonts
  - pages
    - 404.jsx
    - archivo.jsx
    - contacta.jsx
    - index.jsx
  - posts
    - post-slug
      - images
        - image1.jpg
        - image2.jpg
      - index.md
  - shared
    - config
    - hooks
    - utils
  - styles
- static
  - images

Como puedes ver, los componentes que situamos en src/pages, se convierten automáticamente en páginas a las que podemos acceder mediante https://dominio/pagina.

Por otro lado tenemos el directorio src/posts, con el contenido del blog, que he decidido que quiero escribir en markdown, ya que es como más disfruto del proceso de redacción. No obstante, para sitios que requieran una actualización más constante, se podría adaptar cualquier CMS como fuente de contenido, como puede ser Strapi o el mismo Wordpress.

Cada post tendrá su directorio, con un index.md y un subdirectorio "images" dentro del mismo.

Para servir estáticos tenemos el directorio /static, donde colocaremos las imágenes que no sean dinámicas (veremos más adelante de qué se trata).

Y por último un directorio src/shared, con configuración general del proyecto, funciones compartidas entre componentes, hooks personalizados para facilitar el acceso a graphql, etc.

Ficheros de configuración de Gatsby

A grandes rasgos tenemos la opción de crear los siguientes ficheros en la raíz del proyecto:

  • gatsby-config.js: Este fichero lo procesa node y principalmente sirve para configurar los metadatos y los plugins. Tienes miles aquí.
  • gatsby-node.js: Este fichero también lo procesa node, y sirve para configurar aspectos que afectan al proceso de build. La api que nos ofrece gastby para el proceso de build es la siguiente: https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/. Se nos ofrecen eventos a los que podemos enganchar una función, como onCreateNode, y funciones que se ejecutan durante el build, como es createPages. Para cualquiera de los casos que ofrece, simplemente tenemos que exportar una función con el nombre adecuado. En seguida veremos el proceso en detalle.
  • gatsby-browser.js: Al contrario que el anterior fichero, aquí vendremos a configurar aspectos que tienen lugar en el cliente y no durante el render (build). https://www.gatsbyjs.com/docs/browser-apis/
  • gatsby-ssr.js: Para configurar todo los relacionado con el server side render: https://www.gatsbyjs.com/docs/reference/config-files/gatsby-ssr. De modo que puedes intervenir en los componentes renderizados en servidor, que posteriormente se hidratan en cliente.

Los plugins

Para instalar un plugin simplemente tenemos que instalar el paquete correspondiente con npm y después "dar de alta" el mismo en el array plugins del fichero gatsby-config.js. Tenemos dos opciones para registrar el plugin en dicho array:

  1. Si no precisamos de configuración para el plugin entonces añadimos un string.
  2. Pero si necesitamos añadir configuración entonces añadimos un objeto del tipo:
{
  resolve: 'gatsby-source-filesystem',
  options: {
    name: 'src',
    path: `${__dirname}`
  }
},

Estos son los que yo estoy usando:

  • gatsby-plugin-sitemap: Permite generar un mapa del sitio para que sea indexado por buscadores.
  • gatsby-plugin-react-helmet: Adaptador para usar la libreria react "helmet", que provee una interfaz para generar la etiqueta <head>.
  • gatsby-plugin-image: Un componente de gatsby para mostrar imágenes optimizadas mediante srcset y con lazy loading.
  • gatsby-source-filesystem: Gracias a este adaptador podremos hacer que la fuente de contenido sean ficheros locales, en nuestro caso markdown.
  • gatsby-plugin-sass: Habilita el uso de sass, siempre preferible a css básico.
  • gatsby-plugin-root-import: Siempre resulta más limpio trabajar con rutas absolutas, lo que además hace más sencillo cualquier refactor.
  • gatsby-plugin-sharp: Se encarga de optimizar la resolución, calidad y formato de nuestras imágenes. Gracias a la librería sharp, el estándar el node.
  • gatsby-transformer-sharp
  • gatsby-transformer-remark: Remark es la archiconocida librería que nos permite interpretar ficheros con sintáxis markdown. A su vez dispone de muchísimos subplugins:

    • gatsby-remark-abbr: Nos permite definir acrónimos.
    • gatsby-remark-embed-youtube: Para embeber videos de youtube.
    • gatsby-remark-responsive-iframe
    • gatsby-remark-autolink-headers: Interpreta las cabeceras markdown y la envuelve con un ancla, útil para la tabla de contenido.
    • gatsby-remark-relative-images: Nos permite trabajar con las imágenes de los posts separadas en su propio directorio images individual para cada uno de ellos, de modo que nos referimos a ellas con rutas relativas desde el fichero md.
    • gatsby-remark-gifs: Soporte de imagenes de tipo gif (no son procesadas por sharp)
    • gatsby-remark-images: Gracias a este paquete logramos que las imagenes referencias en los ficheros markdown sean procesadas por sharp.
    • gatsby-remark-prismjs: Permite colorear la sintáxis de los bloques de código.
    • gatsby-remark-custom-blocks: Es genial para definir algunos bloques personalizados que se escapan del estándar markdown. Es un punto intermedio entre markdown a pelo y mdx: markdown con componentes React, siendo esto muy heavy para mi gusto😝. Se pervierte demasiado el origen de datos.

Añadiendo campos a los nodos

En gatsby todo gira en torno a los nodos. Los nodos son la pieza fundamental del modelo de datos. De modo que un post o una imagen tendrán su nodo asociado. Los plugins pueden añadir nuevos nodos o añadir campos a los nodos ya existentes. Y del mismo modo, nosotros también podemos añadir campos a los nodos que deseemos. En el caso del blog, como hemos añadido los plugins gatsby-source-filesystem y gatsby-transformer-remark, disponemos de las queries: markdownRemark y allMarkdownRemark que devuelven todos los nodos creados para cada uno de los ficheros markdown de nuestro proyecto, que tendrán type === 'MarkdownRemark'.

Cada nodo markdown tiene muchos campos, algunos de ellos son:

  • html: El contenido de markdown transformado en html listo para pintarse en la web.
  • excerpt: Extracto del contenido
  • fileAbsolutePath: El path del fichero md.
  • wordCount
  • timeToRead
  • tableOfContents: El html de la tabla de contenido creada en base a las cebeceras encontradas en el documento.
  • frontmatter: El frontmatter es la cabecera en formato yaml que puede llevar un documento markdown, y es muy interesante porque nos permite añadir los campos que queramos.

    • title
    • category

Lo que vamos a hacer es añadir un campo extra a esos nodos. Queremos añadir el slug, que será el nombre del fichero, eliminando la extensión. De modo que para el fichero /home/davidpoza/dev/blog-gatsby/src/posts/crear-blog-gatsby/index.md del artículo que estoy escribiendo ahora, queremos extraer como slug la cadena "crear-blog-gatsby".

Para lograrlo vamos a partir del campo fileAbsolutePath y vamos a limpiar el path y la extensión, para luego modificar el nodo añadiendo esta información en un campo con nombre slug. Este proceso lo vamos a realizar durante el evento onCreateNode, desde la función que vamos a enganchar al mismo en el fichero gatsby-node.js:

exports.onCreateNode = ({ node, actions }) => {
  const { createNodeField } = actions;
  if (node.internal.type === 'MarkdownRemark') {
    const slug = path.dirname(node.fileAbsolutePath).split('/').pop();
    createNodeField({
	    node,
		  name: 'slug',
		  value: slug,
	  });
  }
}

Vemos que la función onCreateNode recibe como parámetro un objeto con las propiedades (puede recibir más pero no nos interesan ahora):

  • node: el nodo actual que se está procesando
  • actions: contiene varias funciones disponibles, ahora nos interesa createNodeField, que se encarga de añadir un campo a un nodo, indicando el nodo, el nombre del campo y el valor del mismo.

Con el código anterior hemos logrado que cada nodo, del tipo markdown únicamente, tenga un campo personalizado llamado slug, que contiene el nombre del fichero markdown, sin extensión, que usaremos con clave única para seleccionarlos posteriormente en una page query.

Los campos personalizados que añadimos con **createNodeField** se añaden al documento a un segundo nivel, colgando de la prop "**fields**". Deberás recordarlo cuando efectúes una query.

Los campos personalizados que añadimos con createNodeField se añaden al documento a un segundo nivel, colgando de la prop "fields". Deberás recordarlo cuando efectúes una query.

Estilos css/scss

Una de las primeras cosas que vas a hacer cuando comiences a crear componentes React es darles estilo, y para ello podrías crear un css/scss en el directorio del mismo e importarlo, pero rápidamente te darías cuenta de que repites los nombres de las clases en diferentes componentes, es decir, será frecuente que quieras una clase "container", "content" en muchos componentes, por poner un ejemplo.

Para que no entren en conflicto será útil que comiences a usar css modules, que por supuesto no es algo exclusivo de Gatsby. Con los css modules se logra que los nombres de las clases se transformen a identificadores aleatorios y únicos en el proceso de compilación. De modo que podrás tener las típicas clases "container" y "content" que poníamos de ejemplo repetidas en todos los componentes que necesites, sin que entren en conflicto porque al transpilar se convierten en otros identificadores únicos.

En Gatsby es muy sencillo usar módulos css, simplemente debes llamar xxx.module.scss a tu fichero de estilos e importarlo como import * as styles from './styles.module.scss'; de modo que podrás usarlo así:

  <Componente className={styles.clase}>

Modelo de datos con graphQL

GraphQL es una lenguaje de consulta y manipulación de APIs, desarrollado por Facebook y que se presenta como alternativa a REST. Gatbsy nos proporciona dos herramientas para visualizar las queries, la primera es graphiQL que está disponible recién instalamos gatbsy en http://localhost:8000/___graphql. La segunda, y algo más completa, es graphQL playground y estará disponible en la misma url en el momento en que establezcamos la variable de entorno GATSBY_GRAPHQL_IDE=playground. Si estamos en linux podemos hacerlo directamente modificando el comando develop en el package.json:

"develop": "GATSBY_GRAPHQL_IDE=playground gatsby develop",

Así se ve graphql playground. En el navegador de documentos podemos ver todos los que tenemos disponibles en este momento, y que se van ampliando con el uso de plugins

Así se ve graphql playground. En el navegador de documentos podemos ver todos los que tenemos disponibles en este momento, y que se van ampliando con el uso de plugins

Consultas estáticas

Se usan mediante el hook useStaticQuery o el componente StaticQuery de la librería gatsby. Y no admiten parámetros y por tanto no cambian nunca. Por ello serán adecuadas para mostrar el listado completo de categorias del blog, por ejemplo, pero no para mostrar un post que dependerá del slug como parámetro.

import { graphql, useStaticQuery } from 'gatsby'

const data = useStaticQuery(graphql`
	query {
	  site {
		siteMetadata {
			title
		  }
	  }
	}
`);

Lo único que puede llamar la atención es que el hook useStaticQuery recibe un string que preparamos mediante el uso de una template tag function llamada graphql.

Una template tag function nos permite parsear un template literal usando una sintáxis compacta. Gracias a esta sintáxis, la función recibe automáticamente como parámetros los diferentes trozos que componen la string.

Consultas dinámicas o page queries

En este caso tenemos que exportar una template tag string en el fichero donde tengamos nuestro componente, sin importar el nombre que le demos a la variable. Y automáticamente el resultado de la query le llegaría al componente como prop data.

import * as React from 'react';
import { graphql } from 'gatsby';

const HomePage = ({data}) => {
  return (
    <div>
     {data.site.siteMetadata.description}
    </div>
  )
};

export const query = graphql`
  query HomePageQuery {
    site {
      siteMetadata {
        description
      }
    }
  }

export default HomePage;

Como hemos dicho, la gracia de una page query es usar consultas parametrizadas, para lo cual necesitamos crear un contexto con las variables, y vincularlo a la page, y esto lo podemos hacer cuando creamos la page de forma dinámica mediante la API createPage en el fichero gatsby-node.js. En el siguiente ejemplo vemos cómo:

La idea es que todos los artículos del blog están creados en base al mismo componente, pero usando como origen de datos un fichero markdown distinto. Lo primero es añadir el contexto en el momento de crear la página dinámica. En este bloque de código estamos recorriendo todos los posts y creando un página dinámica para cada uno. En el momento de llamar a createPage estamos pasando la variable slug en el contexto, ya que vamos a considerar que el slug será único, y por tanto nos permitirá seleccionar unívocamente el documento, usando el slug como key en la consulta.

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions;
  const res = await graphql(`
	query {
	    allMarkdownRemark {
				edges {
					node {
						fields {
					    slug
							gitAuthorTime
					  }
						frontmatter {
							category
							tags
						}
					}
				}
			}
		}
		`);

	const posts = res?.data?.allMarkdownRemark.edges;

  // create post pages
	const blogTemplate = path.resolve('./src/components/blogEntryTemplate/index.jsx');
  posts.forEach((edge) => {
		const { slug } = edge.node.fields;
		createPage({
			component: blogTemplate,
			path: `${URL_TAG_FOR_BLOG}${slug}`,
			context: {
				slug
			}
		});
	});
}

El siguiente paso es ir al componente que hemos indicado que se use como template, en nuestro caso el ./src/components/blogEntryTemplate/index.jsx. En dicho componente vamos a exportar la query como template tag string. Y gracias a que hemos declarado el contexto al crear la página dinámica, ahora dicha query puede declarar un parámetro con el nombre slug, que definimos cuando llamamos a createPages. Después sencillamente estamos indicando que la query seleccione aquellos documentos que cumplen slug eq $slug (asumimos que el slug es único).

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

Metadatos

El fichero gatsby-config.js se usa para más cosas a parte de almacenar la configuración de los plugins: contiene un nodo siteMetadata que podremos usar para guardar la información de las etiquetas de metadatos que llevará el html, como son el title o el meta description.

En el ejemplo que nos ocupa tengo:

siteMetadata: {
  defaultTitle: 'Creatividad digital. Mi blog personal',
  titleTemplate: '%s - David Poza',
  defaultDescription: 'Me llamo David Poza Suárez y soy desarrollador web. Mi blog gira en torno a la creatividad digital.',
  defaultImage: '....',
  siteUrl: 'https://davidinformatico.com',
  footer: 'David Poza Suárez © 2021',
  author: 'David Poza Suárez',
  twitterUsername: '@enformatico',
},

Podríamos crear nuestro propio componente para formatear las etiquetas de metadatos estándar, pero para no reinventar la rueda vamos a usar react-helmet, una librería para React que se encarga de esto, y su plugin gatsby-plugin-react-helmet correspondiente para integrarlo en gatsby.

Helmet es muy sencillo de usar, simplemente es un componente react que admite una serie metas bien como props o como children. Podemos ver un listado en https://github.com/nfl/react-helmet:

  • title
  • titleTemplate: Ya que habitualmente el title de una web se construye en base a una plantilla que contiene textos que siempre se repiten. De modo que tomamos la template y reemplazamos el wildcard %s por la prop title, pero mantenemos el resto del template.
  • base
  • meta
  • link
  • script
  • noscript
  • style
  • bodyAttributes
  • htmlAttributes

Helmet genera el siguiente html:

<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
    <head>
        ${helmet.title.toString()}
        ${helmet.meta.toString()}
        ${helmet.link.toString()}
    </head>
    <body ${helmet.bodyAttributes.toString()}>
        <div id="content">
            // React stuff here
        </div>
    </body>
</html>

Podemos añadir Helmet en nuestro layout y se encargará de inyectar el html anterior en la salida.

<Helmet title={seo.title} titleTemplate={titleTemplate}>
  <meta name="description" content={seo.description} />
  <meta name="image" content={seo.image} />
</Helmet>

Servir fuentes

Para las fuentes lo mejor es servirlas desde nuestro propio servidor, y hacer uso del swap en css, de forma que definiremos las fuentes en nuestro fichero global de estilos:

@font-face {
  font-family: "Merriweather";
  src: url("../fonts/merriweather-light.woff2");
  font-weight: 200;
  font-display: swap;
}

Modo oscuro

Personalmente me encanta usar el modo oscuro en todas las aplicaciones que puedo porque lo encuentro beneficioso para el cansancio de la vista, asi que he decidido añadirlo también en el blog.

Cuando desarrollamos esta opción en React necesitamos definir un estado global (con Redux o usando hooks) donde guardar el theme actual y persistirlo entre navegaciones usando el localStorage por ejemplo. Sin embargo en este caso tenemos un plugin que nos ahorra todo esto: gatsby-plugin-dark-mode.

Este nos proporciona un componente llamado ThemeToggler, gracias al cual el child que queda envuelto recibirá las props theme y toggleTheme, siendo la primera el theme actual y la segunda una función que permite efectuar el cambio.

<ThemeToggler>
  {
    ({ theme, toggleTheme }) => (
      <div>
        <FontAwesomeIcon icon={theme === 'dark' ? faMoon : faSun } onClick={() => {
          theme === 'dark' ? toggleTheme('light') : toggleTheme('dark')
        }} />
      </div>
    )
  }
</ThemeToggler>

Por debajo está almacenando esto en localStorage, en la key theme. Y está añadiendo el class correspondiente al body.

Una vez tenemos el body con la clase dark simplemente tenemos que trabajar nuestro css.

Lo primero es definir como variables css todos aquellos colores que vayan a cambiar entre modo claro y modo oscuro, algo que deberías hacer aunque no quieras dark mode, ya que supone una gran ventaja en tu código css, ya que reutilizarás código evitando repetir los colores cientos de veces.

El siguiente paso es redefinir estas variables dentro del selector body.dark que aplicará cuando el modo oscuro esté activo.

body {
  --primaryText: #222;
  --secondaryText: #4abda0;
  --primaryBgColor: #fff;
  --secondaryBgColor: #333333;
  --tertiaryBgColor: rgb(246, 246, 246);
  --cardShadowPrimaryColor: rgb(202, 201, 201);
  --cardShadowSecondaryColor: rgb(236, 236, 236);
  --infoBgColor: #e3ece2;
  --infoAccentColor: #00a77c;
  --dangerBgColor: #ece2e7;
  --dangerAccentColor: #c11766;
  --chipBgColor: #e3ece2;
  --chipTextColor: #222;
  margin: 0;
  font-family: 'Merriweather', serif;
  font-weight: 300;
  color: var(--primaryText);
  background-color: var(--primaryBgColor);
}

body.dark {
 --primaryText: rgb(211, 207, 201);
 --primaryBgColor: rgb(24, 26, 27);
 --secondaryBgColor: rgb(38, 42, 43);
 --tertiaryBgColor: #1d2021;
 --cardShadowPrimaryColor: #353a3c;
 --cardShadowSecondaryColor: #292d2e;
 --infoBgColor: rgb(37, 48, 32);
 --infoAccentColor: #00cd98;
 --dangerBgColor: #302028;
 --dangerAccentColor: #aa145a;
 --chipBgColor: rgb(37, 48, 32);
 --chipTextColor: rgb(211, 207, 201);
}

Te recuerdo que el acceso a las variables css se realiza mediante la siguiente sintáxis:

a:link, a:visited {
  color: var(--primaryText);
}

Optimización y pagespeed

Uno de los motivos de usar JamStack es obtener la máxima puntuación posible en pagespeed, y renderizar las paginas en servidor es forma más efectiva, siempre que tu proyecto se adapte a sus limitaciones obviamente.

Además he tenido en cuenta los siguientes aspectos:

  • sirvo las paginas en http2
  • indico una caché adecuada en las cabeceras
  • servimos comprimiendo las respuestas
  • comprimo las imágenes usando webp y un buen ratio de compresión (gracias a gatsby-plugin-image)
  • las cargamos haciendo uso de lazy load (gracias a gatsby-plugin-image)
  • y usando srcset (gracias a gatsby-plugin-image)
  • servimos las fuentes directamente en local y haciendo css swap para no bloquear el renderizado.
  • mostramos el contenido principal al comienzo para no perjudicar el FCP.
  • evitamos los saltos durante la carga para no perjudicar el CLS, para lo cual predefinimos el tamaño de todos los elementos antes de que carguen.

La puntuación en desktop es de 97 y de 91 en mobile para un artículo

La puntuación en desktop es de 97 y de 91 en mobile para un artículo

CI/CD

La gracia de Gatsby o de cualquier solución JamStack es que se despliegue automáticamente por medio de una pipeline al hacer un push a nuestro repo. Se trata de algo muy sencillo, yo estoy desplegando con github actions, usando la siguiente pipeline:

name: Deployment
on: [push]
jobs:
  deploy:
    if: github.ref == 'refs/heads/master'
    runs-on: ubuntu-latest
    steps:
      - name: Executing ssh commands
        uses: appleboy/ssh-action@master
        with:
          debug: true
          host: ${{ secrets.SSH_HOST }}
          port: ${{ secrets.SSH_PORT }}
          passphrase: ${{ secrets.SSH_PASSPHRASE }}
          username: ${{ secrets.SSH_USERNAME }}
          timeout: '180s'
          key: ${{ secrets.SSH_KEY }}
          script: |
            pwd
            export NVM_DIR="$HOME/.nvm"
            [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
            cd blog-gatsby
            git fetch
            git reset --hard origin/master
            npm i
            npm run build
            rm -r /var/www/davidinformatico.com/web/*
            cp -r public/* /var/www/davidinformatico.com/web/

Lo que estoy haciendo aquí es logarme en mi servidor via ssh, donde previamente he clonado el repo, mediante un usuario limitado a este menester. Además a este usuario le he instalado la versión 14 de node via nvm.

Después actualiza la rama, instala dependencias por si hubieran cambiado, hace un build y copia el contenido listo para producción en el directorio donde lo servirá un apache.

Para ello uso el action appleboy/ssh-action y defino todos los secrets necesarios.

Podríamos seguir otra estrategia y usar actions/checkout@v2 y subir la compilación hecha en el propio runner, pero he creído que esta es la mejor forma en mi caso, pues para el acceso ssh a mi servidor tengo que llevar a cabo algunas operaciones previas que no muestro en el ejemplo.

Referencias