Loading Light/Dark Toggle
 All publications

<h1>

Renderiza archivos Markdown con Next 

</h1>

Updated on May 1, 2023 . 8 min read

<article>

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
Ejecución inicial
Configuración de creación del proyecto

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/

Ejecución inicial
Estructura de directorios

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:

Ejecución inicial
Metadatos en ficheros Markdown

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

Estructura de directorios
Estructura de directorios

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.

Contenido de ficheros
Contenido de ficheros

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; categories: 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.

Renderizado del contenido de ficheros

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> ); }

Renderizado del contenido de ficheros

🚧 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> ); }

Renderizado del contenido de ficheros

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> ); }

Renderizado del contenido de ficheros

🚧 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

Metadatos de ficheros
Metadatos de ficheros

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.

Uso de función utilitaria
Uso de función utilitaria

Finalmente, nuestro proyecto tiene una apariencia similar a esta

Estructura de directorios
Estructura de directorios

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


You can find this and other posts on my Medium profile some of my projects on my Github or on my LinkedIn profile.

¡Thank you for reading this article!

If you want to ask me any questions, don't hesitate! My inbox will always be open. Whether you have a question or just want to say hello, I will do my best to answer you!

</article>