Renderiza archivos Markdown con Next
En esta ocasión te voy a enseñar a renderizar el contenido de tus archivos Markdown usando Next JS 13 de una manera rápida y sencilla.
Empezamos creando nuestro proyecto de Next usando el comando
yarn create next-app --typescript
Para este proyecto estaré usando Tailwind CSS para dar estilos a nuestra aplicación, Typescript, y Eslint para analizar o detectar problemas en el código de nuestra aplicación.
Una vez que nuestro proyecto ha sido creado, vamos a crear un directorio que contendrá nuestro o nuestros ficheros Markdown. En mi caso he creado un directorio en la raíz del proyecto llamado content/
Ahora vamos a crear un par de funciones utilitarias que nos ayudarán a leer el contenido de los archivos usando Typescript. La primera de las funciones nos permite obtener todos los archivos de un directorio específico, esta función utilitaria usa recursividad para examinar directorios y subdirectorios de ficheros.
// src/utils/files.ts
import { readdirSync, statSync } from "fs";
import path from "path";
/**
* Gets files from nested directories and subdirectories.
* @param dirPath Directory where the file search will be performed.
* @param arrayOfFiles Parameter used in recursion.
* @returns {string[]} list of files obtained.
*
* @example
* getAllFiles('content');
* * // returns [ 'content/example-1.md', 'content/example-2.md' ]
*/
const getAllFiles = function (dirPath: string, arrayOfFiles?: string[]): string[] {
const files = readdirSync(dirPath);
arrayOfFiles = arrayOfFiles || [];
files.forEach(function (file: string): void {
if (statSync(dirPath + '/' + file).isDirectory()) {
arrayOfFiles = getAllFiles(dirPath + '/' + file, arrayOfFiles);
} else {
arrayOfFiles && arrayOfFiles.push(path.join(dirPath, '/', file));
}
});
return arrayOfFiles;
};
Podemos usar esta función de la siguiente manera:
const files = getAllFiles('content');
console.log(files); // [ 'content/example-1.md', 'content/example-2.md' ]
La segunda función utilitaria nos permite leer cada uno de los archivos y obtener su contenido. Personalmente, uso metadatos en mis ficheros Markdown para otorgar parámetros relevantes a la hora de identificar cada fichero o su contenido. Podrá sonar confuso, pero es muy sencillo definir estos parámetros, a continuación te presento un ejemplo:
Para nuestra función haremos uso del paquete llamado gray-matter este paquete nos ayudará a obtener tanto los metadatos de nuestros ficheros Markdown. La misma documentación de Next nos recomienda usar este paquete. Instalamos y hacemos uso de este paquete
yarn add gray-matter
// src/utils/files.ts
import { readFileSync } from "fs";
import matter from "gray-matter";
/**
* Read a list of files from a directory and get their contents
* @param folder Directory where the file search will be performed
* @returns T[]
*/
export const getFromContent = <T>(folder: string): T[] => {
const files = getAllFiles(folder);
const contentFiles = files.map((file): T => {
const fileContents = readFileSync(file, 'utf-8');
const matterResult = matter(fileContents);
return { ...matterResult.data, content: matterResult.content } as T;
});
return contentFiles;
};
He usado genéricos con el objetivo de tipar nuestros objetos cuando hagamos uso de esta función, más adelante te mostraré como usar esta función.
Hasta ahora nuestro proyecto luce de esta manera
Uso el archivo src/utils/index.ts
simplemente como un archivo de barril que me permita exponer todas mis funciones u otros archivos utilitarios.
Ahora mismo podríamos utilizar nuestra función utilitaria de esta manera:
const contentFiles = getFromContent('content');
El resultado de esta ejecución será una lista de objetos que a primera vista no tienen una buena manera de identificar los parámetros en cada uno de estos objetos.
Vamos a crear una interfaz que nos ayude a tipar el resultado de esta función. Ten en cuenta que nuestra interfaz tendrá los mismos atributos que definimos en los metadatos de nuestros archivos Markdown (Figura Metadatos en ficheros Markdown). Además de estos atributos, se adiciona un nuevo atributo que es el contenido en texto plano (string
) del archivo.
// src/data/types.ts
export interface Content {
title: string;
description: string;
slug: string;
tags: string[];
content: string;
}
Con nuestra interfaz podemos llamar nuestra función utilitaria de la siguiente manera
// src/app/page.tsx
import { getFromContent } from '../utils'
import { Content } from '../data/types';
export default function Home() {
const contentFiles: Content[] = getFromContent<Content>('content');
return (
<main className="p-24">
<h1>Your files</h1>
{
contentFiles.map((contentFile:Content) => (
<div key={contentFile.slug} className='p-10'>
<h2>{contentFile.title}</h2>
<p>{contentFile.content}</p>
</div>
))
}
</main>
)
}
Ahora podemos identificar e interactuar fácilmente con las propiedades de la interfaz que creamos. Seguramente te estarás preguntando como vamos a visualizar de manera correcta el contenido de nuestros archivos Markdown, ya que si ejecutamos nuestra aplicación, notaremos que el contenido se muestra como un string
que contiene la sintaxis de Markdown.
Para visualizar el formato correcto de nuestros ficheros (Markdown → HTML) tenemos dos opciones: Usando @tailwindcss/typography y markdown-to-jsx o usando solo markdown-to-jsx. En cualquiera de los casos debes instalar sus paquetes respectivos:
yarn add markdown-to-jsx
or
yarn add -D @tailwindcss/typography
Usando @tailwindcss/typography
La primera de las opciones es usando estos paquetes en conjunto, la gran ventaja es que este paquete tiene una buena integración con Tailwindcss, podrás establecer estilos para cada uno de los elementos HTML usando clases utilitarias de css
.
La documentación de este paquete es muy sencilla, únicamente debes agregar el plugin
a tu configuración de Tailwind (tailwind.config.js
)
// tailwind.config.js
module.exports = {
theme: {
// ...
},
plugins: [
require('@tailwindcss/typography'),
// ...
],
}
Finalmente, podemos visualizar el contenido usando la clase utilitaria prose
como te lo muestro a continuación
// src/app/page.tsx
import Markdown from 'markdown-to-jsx';
import { getFromContent } from '../utils'
import { Content } from '../data/types';
export default function Home() {
const contentFiles: Content[] = getFromContent<Content>('content');
return (
<main className="p-24">
<h1>Your files</h1>
{
contentFiles.map((contentFile:Content) => (
<div key={contentFile.slug} className='p-10'>
<h2>{contentFile.title}</h2>
<article className='prose max-w-none'>
<Markdown>
{contentFile.content}
</Markdown>
</article>
</div>
))
}
</main>
)
}
🚧 Nota que los elementos se comportan como HTML normal. Puedes usar tantas clases utilitarias como necesites para formatear cada uno de los elementos dentro de tu Markdown. Puedes revisarlo en su documentación.
Usando markdown-to-jsx
El resultado obtenido será el mismo, sin embargo, la forma en la que podrás estilizar los elementos de tu contenido Markdown es un poco distinta, esta opción es recomendable cuando no estés usando Tailwind en tu proyecto.
import { getFromContent } from '../utils'
import { Content } from '../data/types';
import Markdown from 'markdown-to-jsx';
export default function Home() {
const contentFiles: Content[] = getFromContent<Content>('content');
return (
<main className="p-24">
<h1>Your files</h1>
{
contentFiles.map((contentFile:Content) => (
<div key={contentFile.slug} className='p-10'>
<h2>{contentFile.title}</h2>
<Markdown>
{contentFile.content}
</Markdown>
</div>
))
}
</main>
)
}
Nota que en esta ocasión, si bien los elementos se renderizan “bien”, estos elementos no cuentan con estilos que los diferencian claramente. Para resolver esto, este paquete nos brinda la opción de agregar estilos a cada uno de los elementos como te muestro a continuación
import { getFromContent } from '../utils'
import { Content } from '../data/types';
import Markdown from 'markdown-to-jsx';
export default function Home() {
const contentFiles: Content[] = getFromContent<Content>('content');
return (
<main className="p-24">
<h1>Your files</h1>
{
contentFiles.map((contentFile:Content) => (
<div key={contentFile.slug} className='p-10'>
<h2>{contentFile.title}</h2>
<Markdown options={{
overrides: {
pre: {
props: {
className: 'bg-black text-white p-5 m-5'
}
},
}
}} >
{contentFile.content}
</Markdown>
</div>
))
}
</main>
)
}
🚧 De esta manera podrás agregar clases o estilos a todos y cada uno de los elementos HTML que renderices en tus archivos Markdown, puedes conocer más sobre estas personalizaciones en su documentación oficial
Con estos dos métodos podrás renderizar el contenido de tus archivos Markdown, esto es muy útil a la hora de generar sitios estáticos como Blogs, o si tienes información que resulte tediosa manejar mediante variables o contantes.
Otras consideraciones
Mostrar el contenido de tus archivos ordenados
Algo que me resultó muy útil a la hora de trabajar con archivos Markdown es la posibilidad de poder ordenar el contenido por algún atributo de los metadatos en nuestro caso
Supongamos que queremos renderizar el contenido de mis ficheros ordenados ascendente o descendentemente por alguno de estos parámetros, en este caso esta función utilitaria puede ser de ayuda
/**
* Sorts a list of objects by some property within the object.
* @param values Object list
* @param orderType Property for which to order
* @returns Ordered list of objects
*/
export function OrderArrayBy<T, K extends keyof T>(values: T[], orderType: K): T[] {
return values.sort((a, b): 1 | -1 | 0 => {
if (a[orderType] < b[orderType]) return 1;
if (a[orderType] > b[orderType]) return -1;
return 0;
});
}
Puedes usar esta función de la siguiente manera, nota que cuando usas la lista de objetos que tipamos anteriormente, el método nos restringe a ordenar la lista por alguno de los atributos de la interfaz, gracias a las propiedades de Typescript.
Finalmente, nuestro proyecto tiene una apariencia similar a esta
Renderizar el contenido de ficheros Markdown puede ser de gran ayuda cuando queremos crear sitios de contenido estático como Blogs, sitios de Noticias, Portafolios, Sitios de documentación, etc. El lenguaje de marcado Markdown, es un lenguaje cuya sintaxis es simple, por lo que es ampliamente utilizado en muchas tecnologías actualmente.
Código fuente de este proyecto
Puedes encontrar este y otros en mi repositorio de Github. No olvides visitar mi página web.
¡Gracias por leer este artículo!
Si deseas hacerme alguna pregunta, no lo dudes!. Mi bandeja de entrada siempre estará abierta. Ya sea que tengas una pregunta o simplemente me quieras saludar, ¡haré todo lo posible para responderle!