Markdown y MDX

Markdown es un lenguaje de marcado ligero utilizado para formatear texto. Permite escribir usando sintaxis de texto plano y convertirlo a HTML estructuralmente válido. Es comúnmente usado para escribir contenido en sitios web y blogs.

Escribes...

I **love** using [Next.js](https://nextjs.org/)

Salida:

<p>I <strong>love</strong> using <a href="https://nextjs.org/">Next.js</a></p>

MDX es un superconjunto de markdown que te permite escribir JSX directamente en tus archivos markdown. Es una forma poderosa de agregar interactividad dinámica e incrustar componentes React dentro de tu contenido.

Next.js puede soportar tanto contenido MDX local dentro de tu aplicación, como archivos MDX remotos obtenidos dinámicamente en el servidor. El plugin de Next.js maneja la transformación de markdown y componentes React a HTML, incluyendo soporte para su uso en Componentes del Servidor (el predeterminado en App Router).

@next/mdx

El paquete @next/mdx se usa para configurar Next.js y que pueda procesar markdown y MDX. Obtiene datos de archivos locales, permitiéndote crear páginas con extensión .mdx, directamente en tus directorios /pages o /app.

Veamos cómo configurar y usar MDX con Next.js.

Comenzando

Instala los paquetes necesarios para renderizar MDX:

Terminal
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

Actualiza el archivo next.config.js en la raíz de tu proyecto para configurarlo y que use MDX:

next.config.js
const withMDX = require('@next/mdx')()

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Configura `pageExtensions` para incluir archivos MDX
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
  // Opcionalmente, agrega cualquier otra configuración de Next.js aquí
}

module.exports = withMDX(nextConfig)

Luego, crea una nueva página MDX dentro del directorio /pages:

  tu-proyecto
  ├── pages
  │   └── mi-pagina-mdx.mdx
  └── package.json

Ahora puedes usar markdown e importar componentes React directamente dentro de tu página MDX:

import { MiComponente } from 'mis-componentes'

# ¡Bienvenido a mi página MDX!

Este es un texto en **negrita** y _cursiva_.

Esta es una lista en markdown:

- Uno
- Dos
- Tres

Mira mi componente React:

<MiComponente />

Navegar a la ruta /mi-pagina-mdx debería mostrar tu MDX renderizado.

MDX Remoto

Si tus archivos o contenido markdown/MDX están en otro lugar, puedes obtenerlos dinámicamente en el servidor. Esto es útil para contenido almacenado en una carpeta local separada, CMS, base de datos o cualquier otro lugar. Un paquete popular de la comunidad para este caso es next-mdx-remote.

Nota importante: Proceda con precaución. MDX se compila a JavaScript y se ejecuta en el servidor. Solo debes obtener contenido MDX de una fuente confiable, de lo contrario esto puede llevar a ejecución remota de código (RCE).

El siguiente ejemplo usa next-mdx-remote:

import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote'

interface Props {
  mdxSource: MDXRemoteSerializeResult
}

export default function PaginaMdxRemota({ mdxSource }: Props) {
  return <MDXRemote {...mdxSource} />
}

export async function getStaticProps() {
  // Texto MDX - puede ser de un archivo local, base de datos, CMS, fetch, etc...
  const res = await fetch('https:...')
  const mdxText = await res.text()
  const mdxSource = await serialize(mdxText)
  return { props: { mdxSource } }
}
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

export default function PaginaMdxRemota({ mdxSource }) {
  return <MDXRemote {...mdxSource} />
}

export async function getStaticProps() {
  // Texto MDX - puede ser de un archivo local, base de datos, CMS, fetch, etc...
  const res = await fetch('https:...')
  const mdxText = await res.text()
  const mdxSource = await serialize(mdxText)
  return { props: { mdxSource } }
}

Navegar a la ruta /mi-pagina-mdx-remota debería mostrar tu MDX renderizado.

Diseños

Para compartir un diseño alrededor de páginas MDX, crea un componente de diseño:

export default function DisenoMdx({ children }: { children: React.ReactNode }) {
  // Crea cualquier diseño compartido o estilos aquí
  return <div style={{ color: 'blue' }}>{children}</div>
}
export default function DisenoMdx({ children }) {
  // Crea cualquier diseño compartido o estilos aquí
  return <div style={{ color: 'blue' }}>{children}</div>
}

Luego, importa el componente de diseño en la página MDX, envuelve el contenido MDX en el diseño y expórtalo:

import DisenoMdx from '../components/diseno-mdx'

# ¡Bienvenido a mi página MDX!

export default function PaginaMDX({ children }) {
  return <DisenoMdx>{children}</DisenoMdx>

}

Plugins Remark y Rehype

Opcionalmente puedes proporcionar plugins remark y rehype para transformar el contenido MDX.

Por ejemplo, puedes usar remark-gfm para soportar GitHub Flavored Markdown.

Dado que el ecosistema remark y rehype es solo ESM, necesitarás usar next.config.mjs como archivo de configuración.

next.config.mjs
import remarkGfm from 'remark-gfm'
import createMDX from '@next/mdx'

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Configura `pageExtensions` para incluir archivos MDX
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
  // Opcionalmente, agrega cualquier otra configuración de Next.js aquí
}

const withMDX = createMDX({
  // Agrega plugins markdown aquí, como desees
  options: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [],
  },
})

// Envuelve MDX y la configuración de Next.js entre sí
export default withMDX(nextConfig)

Frontmatter

Frontmatter es un par clave/valor similar a YAML que puede usarse para almacenar datos sobre una página. @next/mdx no soporta frontmatter por defecto, aunque hay muchas soluciones para agregar frontmatter a tu contenido MDX, como:

Para acceder a metadatos de página con @next/mdx, puedes exportar un objeto metadata desde dentro del archivo .mdx:

export const metadata = {
  author: 'John Doe',
}

# Mi página MDX

Elementos Personalizados

Uno de los aspectos agradables de usar markdown, es que se mapea a elementos HTML nativos, haciendo que escribir sea rápido e intuitivo:

Esta es una lista en markdown:

- Uno
- Dos
- Tres

Lo anterior genera el siguiente HTML:

<p>Esta es una lista en markdown:</p>

<ul>
  <li>Uno</li>
  <li>Dos</li>
  <li>Tres</li>
</ul>

Cuando quieras diseñar tus propios elementos para un aspecto personalizado en tu sitio web o aplicación, puedes usar shortcodes. Estos son tus propios componentes personalizados que se mapean a elementos HTML.

Para hacer esto, crea un archivo mdx-components.tsx en la raíz de tu aplicación (la carpeta padre de pages/ o src/) y agrega elementos personalizados:

import type { MDXComponents } from 'mdx/types'
import Image, { ImageProps } from 'next/image'

// Este archivo te permite proporcionar componentes React personalizados
// para usar en archivos MDX. Puedes importar y usar cualquier
// componente React que desees, incluyendo estilos en línea,
// componentes de otras bibliotecas, y más.

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    // Permite personalizar componentes integrados, ej. para agregar estilos.
    h1: ({ children }) => <h1 style={{ fontSize: '100px' }}>{children}</h1>,
    img: (props) => (
      <Image
        sizes="100vw"
        style={{ width: '100%', height: 'auto' }}
        {...(props as ImageProps)}
      />
    ),
    ...components,
  }
}
import Image from 'next/image'

// Este archivo te permite proporcionar componentes React personalizados
// para usar en archivos MDX. Puedes importar y usar cualquier
// componente React que desees, incluyendo estilos en línea,
// componentes de otras bibliotecas, y más.

export function useMDXComponents(components) {
  return {
    // Permite personalizar componentes integrados, ej. para agregar estilos.
    h1: ({ children }) => <h1 style={{ fontSize: '100px' }}>{children}</h1>,
    img: (props) => (
      <Image
        sizes="100vw"
        style={{ width: '100%', height: 'auto' }}
        {...props}
      />
    ),
    ...components,
  }
}

Análisis Profundo: ¿Cómo se transforma markdown a HTML?

React no entiende nativamente markdown. El texto plano markdown necesita primero transformarse a HTML. Esto se puede lograr con remark y rehype.

remark es un ecosistema de herramientas alrededor de markdown. rehype es lo mismo, pero para HTML. Por ejemplo, el siguiente fragmento de código transforma markdown a HTML:

import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeSanitize from 'rehype-sanitize'
import rehypeStringify from 'rehype-stringify'

main()

async function main() {
  const file = await unified()
    .use(remarkParse) // Convertir en AST de markdown
    .use(remarkRehype) // Transformar a AST de HTML
    .use(rehypeSanitize) // Sanitizar entrada HTML
    .use(rehypeStringify) // Convertir AST a HTML serializado
    .process('¡Hola, Next.js!')

  console.log(String(file)) // <p>¡Hola, Next.js!</p>
}

El ecosistema remark y rehype contiene plugins para resaltado de sintaxis, enlaces a encabezados, generar tabla de contenidos, y más.

Cuando usas @next/mdx como se mostró arriba, no necesitas usar remark o rehype directamente, ya que se maneja por ti. Lo describimos aquí para un entendimiento más profundo de lo que el paquete @next/mdx está haciendo internamente.

Usando el compilador MDX basado en Rust (Experimental)

Next.js soporta un nuevo compilador MDX escrito en Rust. Este compilador es aún experimental y no se recomienda para uso en producción. Para usar el nuevo compilador, necesitas configurar next.config.js cuando lo pasas a withMDX:

next.config.js
module.exports = withMDX({
  experimental: {
    mdxRs: true,
  },
})

Enlaces Útiles