Next.js

Cómo Crear un Blog con Next.js y Markdown

Claudia Valdivieso
Claudia Valdivieso

Por el día internacional de la mujer fui invitada a dar un taller en el evento "Nevertheless, She Codes" organizado por la comunidad de Ruby Perú y Tech Talks. Este artículo es cuasi una transcripción actualizada a la fecha de ese taller. Te dejo el video a continuación por si eres más de videos.

Aquí puedes revisar la charla

¿Por qué bloguear?

Para aprender nuevas cosas

Tener un blog nació de mi, fue por el 2012 más o menos y la verdad es que siempre leía artículos en inglés porque no había mucho contenido en español y recuerdo que los traducía con Google Translate que en ese tiempo no era tan bueno como ahora, y a veces traducía medio raro las cosas, entonces lo que yo hacía luego era escribir con mis propias palabras lo que entendía, y no solo eso traducía artículos sino que si yo experimentaba con alguna tecnología o hacía algo nuevo en el trabajo lo ponía en un blog para compartirlo.

Te puede ayudar a conseguir trabajo

Por ejemplo mi primer trabajo en planilla lo conseguí en parte porque vieron mi blog y a través de él vieron mi potencial para aprender cosas, enseñar y mentorear.

Puede ser tu trabajo

Existen empresas que buscan personas que puedan escribir artículos para sus webs y les pagan por ello. Uno mismo puede crear su propio emprendimiento de ello, como Ali Spittel o Kent C. Dodds. También, bloguear te puede llevar a crear cursos en video ya que al aprender nuevas cosas y tomarte el tiempo de explicarlas vas creciendo en expertise tanto en tus habilidades de programación como en las blandas.

Ahora quizá te hagas esta pregunta

¿Tienes suficiente experiencia para escribir sobre X tema?

La respuesta es ¡Sí!

No importa si estás aprendiendo algo nuevo y no tienes experiencia en tal tema, siempre puedes compartir de tu aprendizaje y la forma en que afrontas las cosas.

Y lo resumiría así:

Bloguea para...

  • Para escribir el post que te habría ayudado en el pasado. Al que puedes regresar a revisar en el futuro.
  • Para reforzar y estructurar tu aprendizaje. Probando así que puedes mejorar tus habilidades.
  • Para establecerte como un experto, para el futuro de tu carrera. Mucha gente piensa que eres un experto por escribir algo y no, pero quizás sí, eso depende de cada uno.
  • Para hacer amigos y enseñar. Sí, tener un blog te permite crecer en comunidad, tal y como lo estamos haciendo ahora.

⚠️ Puedes ser tan serio o tan informal acerca de bloguear. ¡Sé razonable contigo mismo! Pero, como dice Ali Spittel, recuerda que escribir un artículo es mejor que no escribir ninguno 😉

Escoger una plataforma

Un consejo es que no construyas tu sitio para bloguear al menos que sepas que te gusta hacerlo. En mi caso yo empecé usando WordPress.com, BlogSpot, luego pasé a pagar hosting para tener mi WordPress customizado. Cuando ya no tenía dinero pasé a Medium, luego me dije, voy a construir algo custom para mi y que no me cueste 😅 y probé con Jekyll, Gatsby deployando el GitHub Pages y ahora estoy con Next.js deployando en Vercel.

Lo que nos lleva a la carnesita del artículo!

Crear un blog con Next.js

Lo primero que vamos a hacer es crear nuestro proyecto. Para ello utilizaremos el siguiente comando:

npx create-next-app blog-with-next-js
├── README.md
├── next.config.js
├── node_modules
│   ├── ...
├── package.json
├── pages
│   ├── _app.js
│   ├── api
│   │   └── hello.js
│   └── index.js
├── public
│   ├── favicon.ico
│   └── vercel.svg
├── styles
│   ├── Home.module.css
│   └── globals.css
└── yarn.lock

Luego, en la raíz de nuestro proyecto, creamos nuestra carpeta articles donde estarán nuestros artículos.

Puedes crear allí tus propios artículos o encontrar los artículos de prueba en el repositorio de GitHub.

Por defecto Next.js no puede leer estos archivos y mostrarlos. Para ello vamos a hacer uso de un paquete llamado contentlayer.

yarn add contentlayer next-contentlayer

Contentlayer es un SDK de contenido que valida y transforma el contenido en data tipo JSON con tipado para que lo importemos fácilmente en nuestra aplicación.

Como vemos en la documentación tenemos que modificar nuestro next.config.js envolviendo nuestra configuración con la utilidad withContentlayer

/** @type {import('next').NextConfig} */
const { withContentlayer } = require("next-contentlayer");
const nextConfig = {
reactStrictMode: true,
}
module.exports = withContentlayer(nextConfig)

Luego toca crear el archivo contentlayer.config.js donde especificaremos nuestros tipos de documentos o archivos.

import { defineDocumentType, makeSource } from "contentlayer/source-files";
const computedFields = {
slug: {
type: "string",
resolve: (doc) => doc._raw.sourceFileName.replace(/\.mdx/, ""),
},
wordCount: {
type: "number",
resolve: (doc) => doc.body.raw.split(/\\s+/gu).length,
},
};
const Article = defineDocumentType(() => ({
name: "Article",
filePathPattern: `**/*.mdx`,
fields: {
title: { type: "string", required: true },
date: { type: "string", required: true },
summary: { type: "string" },
banner: { type: "string" },
},
computedFields,
}));
export default makeSource({
contentDirPath: "articles",
documentTypes: [Article],
});

También le vamos a agregar una propiedad readingTime para tener el tiempo de lectura, a lo Medium.

Vamos a usar un paquete llamado reading-time que nos va a calcular ese tiempo de acuerdo al contenido de nuestro artículo.

yarn add reading-time -D

y lo vamos a importar en contentlayer.config.js

import readingTime from "reading-time";

y vamos a añadir el campo en computedFields

const computedFields = {
...
readingTime: { type: "json", resolve: (doc) => readingTime(doc.body.raw) },
}

Luego vamos a ir a index.js para pintar los artículos.

import Link from "next/link";
import { allArticles } from ".contentlayer/generated";
import { pick } from "@contentlayer/client";
export default function Home({ articles }) {
return (
<section>
<ul>
{articles.map(({ slug, readingTime, date, title }) => (
<li key={slug}>
<Link href={`/blog/${slug}`}>
<a>{title}</a>
</Link>
<br />
<small>{readingTime.text}</small>
{" - "}
<small>{date}</small>
</li>
))}
</ul>
</section>
);
}
export async function getStaticProps() {
const articles = allArticles.map((article) => pick(article, ["title", "date", "readingTime", "slug"]));
return { props: { articles } };
}

Ahora vamos a correr nuestra aplicación

npm run dev

Y, nos muestra un error 😅

Este error es porque no encuentra el módulo que estamos importando, esto es porque el archivo index.js está dentro de la carpeta pages.

Lo que podemos hacer es importarlo así "../.contentlayer/generated"

O crear un archivo jsconfig.json para decirle al compilador que nuestros imports empiezan en la carpeta base.

Entonces creamos nuestro archivo jsconfig.json con lo siguiente

{
"compilerOptions": {
"baseUrl": ".",
},
}

Ahora, detenemos el servidor y volvemos a probar ejecutando

npm run dev

Y ¡ya podemos ver el listado de nuestros artículos!

Pero todavía nos falta ver su contenido, así que para ello vamos a ver cómo funcionan las rutas dinámicas en Next.js.

Para esto vamos a crear una carpeta dentro de pages llamada blog y dentro un archivo que se llame [slug].js con lo siguiente

import { allArticles } from '.contentlayer/generated'
export default function Article({ article }) {
return (
<article>
<h1>{article.title}</h1>
<div>{article.date}</div>
<div dangerouslySetInnerHTML={{ __html: article.body.html }} />
</article>
)
}
export async function getStaticPaths() {
return {
paths: allArticles.map((a) => ({ params: { slug: a.slug } })),
fallback: false,
}
}
export async function getStaticProps({ params }) {
const article = allArticles.find((article) => article.slug === params?.slug)
return { props: { article } }
}

Detenemos el servidor y ejecutamos nuevamente

npm run dev

y si entramos a uno de nuestros artículos podremos ver que ya se muestra la información:

Ahora que ya podemos ver nuestros artículos vamos a crear nuestros componentes para que nuestra web se vea bonita.

Para estilar nuestros componentes vamos a usar TailwindCSS y vamos a hacer todo lo que dice en su guía "Install Tailwind CSS with Next.js"

Luego de haber realizado todo lo de la guía, vamos a agregar el paquete @tailwindcss/typography para hacer que nuestro artículo se vea muy bien.

yarn add @tailwindcss/typography -D

en nuestro config tenemos que agregar lo siguiente a nuestro tailwind.config.js

module.exports = {
...
plugins: [require('@tailwindcss/typography')]
}

y en nuestro globals.css

...
body,
html {
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
body {
font-size: 18px;
line-height: 1.667em;
font-weight: 500;
}

Luego pasamos a modificar el index.js

import { allArticles } from ".contentlayer/generated";
import { pick } from "@contentlayer/client";
import Article from "components/article";
export default function Home({ articles }) {
return (
<div className="px-8">
<main className="max-w-7xl mx-auto pt-32 pb-40">
<h1 className="mb-0 text-6xl">Claudia Valdivieso</h1>
<p className="font-bold">Software Engineer at Draftea</p>
<section className="pt-16">
<h2 className="text-4xl mb-4">Acerca de mi</h2>
<p className="mb-5">
¡Hola! Soy Claudia, front end trabajando con tecnologías JavaScript,
TypeScript y Web Performance. Me gusta compartir sobre lo que voy
aprendiendo en esta larga carrera de la programación a través de
artículos, los cuales puedes leer a continuación.
</p>
</section>
<section className="pt-16">
<h2 className="text-4xl mb-4">Artículos</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-9 w-full">
{articles.map((article) => (
<Article key={article.slug} article={article} />
))}
</div>
</section>
</main>
</div>
);
}
export async function getStaticProps() {
const articles = allArticles.map((article) =>
pick(article, ["title", "date", "readingTime", "slug"])
);
return { props: { articles } };
}

Creamos una carpeta components en la raíz donde estarán nuestros componentes, por ahora crearemos el componente Article en article.js

import Link from "next/link";
import { parseISO, format } from "date-fns";
export default function Article({ article }) {
const { slug, date, title, readingTime } = article;
return (
<div
className="pt-5 p-8 rounded-3xl border-2 shadow-lg border-purple-300 bg-purple-50 flex flex-col justify-between"
>
<Link href={`/blog/${slug}`}>
<a className="text-2xl">{title}</a>
</Link>
<div className="mt-5 flex justify-between align-center">
<small>{readingTime.text}</small>
<small>{format(parseISO(date), "MMM dd, yyyy")}</small>
</div>
</div>
);
}

Como vemos estamos usando la librería date-fns así que pasaremos a instalarla

yarn add date-fns -D

Ahora pasaremos a darle los últimos arreglos a nuestro archivo contentlayer.config.js, porque, si bien ahora estamos mostrando el HTML, quisiéramos mostrar la data markdown parseada y estilada, por ejemplo las partes de código se vieran con un formato bonito, que la sintaxis esté resaltada, que el markdown sea el formato de GitHub y que los headers estén linkeados.

Para ello vamos a utilizar estos paquetes.

yarn add remark-gfm rehype-slug rehype-code-titles rehype-autolink-headings rehype-prism-plus -D
  • remark-gfm es para tener un GitHub Flavored Markdown
  • rehype-slug para agregar ids a los headings
  • rehype-code-titles para agregar títulos al código, es decir que se vea el nombre de los archivos jsx:pages/_app.jsx
  • rehype-autolink-headings para agregar enlaces a los headings que tengan un id.
  • rehype-prism-plus es para resaltar el código con la librería usando la librería Prism.

Entonces, vamos a agregarlos a nuestro config

import remarkGfm from 'remark-gfm';
import rehypeSlug from 'rehype-slug';
import rehypeCodeTitles from 'rehype-code-titles';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypePrism from 'rehype-prism-plus';

Y vamos a modificar nuestro tipo Article para decirle que el tipo de contenido es MDX, y la función a exportar agregando los plugins.

const Article = defineDocumentType(() => ({
name: "Article",
filePathPattern: `**/*.mdx`,
contentType: 'mdx', // <--
...
}));
export default makeSource({
...
mdx: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
rehypeSlug,
rehypeCodeTitles,
rehypePrism,
[
rehypeAutolinkHeadings,
{
properties: {
className: ["anchor"],
},
},
],
],
},
});

Para linkear los headers le estamos diciendo que vamos a usar la clase .anchor por lo que vamos a agregar el siguiente código a nuestro global.css

...
.prose .anchor {
@apply absolute invisible;
margin-left: -1em;
padding-right: 0.5em;
width: 80%;
max-width: 700px;
cursor: pointer;
}
.anchor:hover {
@apply visible no-underline;
}
.prose .anchor:after {
@apply text-gray-300 dark:text-gray-700;
content: "#";
}
.prose *:hover > .anchor {
@apply visible no-underline;
}

A continuación, vamos al archivo [slug].js y modificamos nuestro componente Article tal y como nos dice la documentación de Contentlayer.

import { allArticles } from ".contentlayer/generated";
import Image from "next/image";
import Link from "next/link";
import { useMDXComponent } from "next-contentlayer/hooks";
import { parseISO, format } from "date-fns";
const mdxComponents = {
Image,
};
export default function Article({ article }) {
const MDXContent = useMDXComponent(article.body.code);
return (
<main className="flex flex-col justify-center pt-32 pb-40">
<article className="flex flex-col justify-center items-start max-w-2xl mx-auto mb-16 w-full">
<small>
<Link href="/">
<a>👈 Back to home</a>
</Link>
</small>
<h1 className="font-bold text-3xl md:text-5xl tracking-tight mb-4">
{article.title}
</h1>
<div className="flex flex-col md:flex-row justify-between items-start md:items-center w-full mt-2">
<div className="flex items-center">
<Image
alt="Claudia Valdivieso"
height={24}
width={24}
src="/lavaldi.jpg"
className="rounded-full"
/>
<p className="text-sm ml-2 text-gray-500">
{"Claudia Valdivieso / "}
{format(parseISO(article.date), "MMMM dd, yyyy")}
</p>
</div>
<p className="text-sm text-gray-500 min-w-32 mt-2 md:mt-0">
{article.readingTime.text}
</p>
</div>
<div className="prose dark:prose-dark max-w-none w-full mt-5 mb-8">
<MDXContent components={mdxComponents} />
</div>
</article>
</main>
);
}
export async function getStaticPaths() {
return {
paths: allArticles.map((a) => ({ params: { slug: a.slug } })),
fallback: false,
};
}
export async function getStaticProps({ params }) {
const article = allArticles.find((article) => article.slug === params?.slug);
return { props: { article } };
}

y voila! ya tenemos nuestros artículos!

Pero el código no tiene mucho color que digamos 🤔 y es que falta que agreguemos algunos estilos.

Como yo soy muy fan del tema Shades of Purple de Ahmad Awais le vamos a poner esos estilos para resaltar el código.

Vamos a copiar este archivo prism-shades-of-purple.css del repo de temas para Prism en nuestro código y lo vamos a importar en _app.js

import "../styles/prism-shades-of-purple.css";

Ahora sí, voilà!

Puedes ver el preview del proyecto aquí 👉 https://blog-with-next-js-theta.vercel.app/

Desplegar en Vercel

Para desplegar tu blog en Vercel solo debes seguir los pasos de la guía "Deploying a Git Repository" o darle al botón Deploy del repositorio en GitHub

Resumen y perspectiva

En este artículo hemos pasado el contenido de nuestros artículos MDX como data JSON con Contentlayer y pronto podremos unificar nuestro contenido ya sea que venga de archivos locales como de algún CMS, todo en uno solo y con tipado!

Espero que hayas aprendido mucho! aunque nos faltaron muchas cosas como el SEO, generar un sitemap y un RSS para el blog de los cuales te dejo algunos recursos a continuación para que te inspires 😉

Recursos adicionales

¿Quieres mejorar tus habilidades de frontend?