Introducción/Guías/MDX

Cómo usar markdown y MDX en Next.js

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. Se usa comúnmente 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).

Bueno saber: Consulta la Plantilla Portfolio Starter Kit para un ejemplo completo funcional.

Instalar dependencias

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

Instala estos paquetes para renderizar MDX con Next.js:

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

Configurar next.config.mjs

Actualiza el archivo next.config.mjs en la raíz de tu proyecto para configurarlo para usar MDX:

next.config.mjs
import createMDX from '@next/mdx'

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

const withMDX = createMDX({
  // Agrega plugins de markdown aquí, si lo deseas
})

// Combina la configuración MDX con la configuración de Next.js
export default withMDX(nextConfig)

Esto permite que los archivos .mdx actúen como páginas, rutas o imports en tu aplicación.

Manejo de archivos .md

Por defecto, next/mdx solo compila archivos con extensión .mdx. Para manejar archivos .md con webpack, actualiza la opción extension:

next.config.mjs
const withMDX = createMDX({
  extension: /\.(md|mdx)$/,
})

Bueno saber: Turbopack actualmente no soporta la opción extension y por lo tanto no soporta archivos .md.

Agregar un archivo mdx-components.tsx

Crea un archivo mdx-components.tsx (o .js) en la raíz de tu proyecto para definir Componentes MDX globales. Por ejemplo, al mismo nivel que pages o app, o dentro de src si aplica.

import type { MDXComponents } from 'mdx/types'

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    ...components,
  }
}

Bueno saber:

Renderizar MDX

Puedes renderizar MDX usando el enrutamiento basado en archivos de Next.js o importando archivos MDX en otras páginas.

Usando enrutamiento basado en archivos

Al usar enrutamiento basado en archivos, puedes usar páginas MDX como cualquier otra página.

En aplicaciones con App Router, eso incluye poder usar metadata.

Crea una nueva página MDX dentro del directorio /app:

  my-project
  ├── app
  │   └── mdx-page
  │       └── page.(mdx/md)
  |── mdx-components.(tsx/js)
  └── package.json

Puedes usar MDX en estos archivos, e incluso importar componentes React, directamente dentro de tu página MDX:

import { MyComponent } from 'my-component'

# ¡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:

<MyComponent />

Navegar a la ruta /mdx-page debería mostrar tu página MDX renderizada.

Usando imports

Crea una nueva página dentro del directorio /app y un archivo MDX donde prefieras:

  .
  ├── app/
  │   └── mdx-page/
  │       └── page.(tsx/js)
  ├── markdown/
  │   └── welcome.(mdx/md)
  ├── mdx-components.(tsx/js)
  └── package.json

Puedes usar MDX en estos archivos, e incluso importar componentes React, directamente dentro de tu página MDX:

import { MyComponent } from 'my-component'

# ¡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:

<MyComponent />

Importa el archivo MDX dentro de la página para mostrar el contenido:

import Welcome from '@/markdown/welcome.mdx'

export default function Page() {
  return <Welcome />
}

Navegar a la ruta /mdx-page debería mostrar tu página MDX renderizada.

Usando imports dinámicos

Puedes importar componentes MDX dinámicos en lugar de usar enrutamiento basado en archivos para archivos MDX.

Por ejemplo, puedes tener un segmento de ruta dinámico que cargue componentes MDX desde un directorio separado:

Segmentos de ruta para componentes MDX dinámicos

generateStaticParams puede usarse para prerenderizar las rutas proporcionadas. Al marcar dynamicParams como false, acceder a una ruta no definida en generateStaticParams resultará en un 404.

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const { default: Post } = await import(`@/content/${slug}.mdx`)

  return <Post />
}

export function generateStaticParams() {
  return [{ slug: 'welcome' }, { slug: 'about' }]
}

export const dynamicParams = false

Bueno saber: Asegúrate de especificar la extensión .mdx en tu import. Si bien no es obligatorio usar alias de rutas de módulo (ej., @/content), simplifica tu ruta de importación.

Usar estilos y componentes personalizados

Markdown, al renderizarse, se mapea a elementos HTML nativos. Por ejemplo, escribir el siguiente markdown:

## Este es un encabezado

Esta es una lista en markdown:

- Uno
- Dos
- Tres

Genera el siguiente HTML:

<h2>Este es un encabezado</h2>

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

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

Para estilizar tu markdown, puedes proporcionar componentes personalizados que se mapeen a los elementos HTML generados. Los estilos y componentes pueden implementarse globalmente, localmente y con diseños compartidos.

Estilos y componentes globales

Agregar estilos y componentes en mdx-components.tsx afectará a todos los archivos MDX en tu aplicación.

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={{ color: 'red', fontSize: '48px' }}>{children}</h1>
    ),
    img: (props) => (
      <Image
        sizes="100vw"
        style={{ width: '100%', height: 'auto' }}
        {...(props as ImageProps)}
      />
    ),
    ...components,
  }
}

Estilos y componentes locales

Puedes aplicar estilos y componentes locales a páginas específicas pasándolos a componentes MDX importados. Estos se fusionarán y sobrescribirán los estilos y componentes globales.

import Welcome from '@/markdown/welcome.mdx'

function CustomH1({ children }) {
  return <h1 style={{ color: 'blue', fontSize: '100px' }}>{children}</h1>
}

const overrideComponents = {
  h1: CustomH1,
}

export default function Page() {
  return <Welcome components={overrideComponents} />
}

Diseños compartidos

Para compartir un diseño entre páginas MDX, puedes usar el soporte de diseños integrado con App Router.

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

Uso del plugin de tipografía de Tailwind

Si está utilizando Tailwind para dar estilo a su aplicación, el plugin @tailwindcss/typography le permitirá reutilizar su configuración y estilos de Tailwind en sus archivos markdown.

El plugin añade un conjunto de clases prose que pueden usarse para aplicar estilos tipográficos a bloques de contenido provenientes de fuentes como markdown.

Instale Tailwind typography y utilícelo con diseños compartidos para añadir el estilo prose que desee.

export default function MdxLayout({ children }: { children: React.ReactNode }) {
  // Cree aquí cualquier diseño o estilo compartido
  return (
    <div className="prose prose-headings:mt-8 prose-headings:font-semibold prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg dark:prose-headings:text-white">
      {children}
    </div>
  )
}

Frontmatter

Frontmatter es un conjunto de pares clave/valor similar a YAML que puede usarse para almacenar datos sobre una página. @next/mdx no soporta frontmatter por defecto, aunque existen muchas soluciones para añadirlo a su contenido MDX, como:

@next/mdx permite usar exportaciones como cualquier otro componente JavaScript:

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

# Entrada de blog

Ahora se puede hacer referencia a los metadatos fuera del archivo MDX:

import BlogPost, { metadata } from '@/content/blog-post.mdx'

export default function Page() {
  console.log('metadata: ', metadata)
  //=> { author: 'John Doe' }
  return <BlogPost />
}

Un caso de uso común es cuando se desea iterar sobre una colección de MDX y extraer datos. Por ejemplo, crear una página de índice de blog a partir de todas las entradas. Puede usar paquetes como el módulo fs de Node o globby para leer un directorio de publicaciones y extraer los metadatos.

Nota importante:

  • El uso de fs, globby, etc. solo puede realizarse en el lado del servidor.
  • Consulte el Portfolio Starter Kit para ver un ejemplo completo funcional.

Plugins de remark y rehype

Opcionalmente, puede proporcionar plugins de remark y rehype para transformar el contenido MDX.

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

Dado que el ecosistema de remark y rehype solo es compatible con ESM, deberá usar next.config.mjs o next.config.ts como archivo de configuración.

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

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Permitir extensiones .mdx para archivos
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
  // Opcionalmente, añada cualquier otra configuración de Next.js aquí
}

const withMDX = createMDX({
  // Añada plugins de markdown aquí, según desee
  options: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [],
  },
})

// Combine la configuración de MDX y Next.js
export default withMDX(nextConfig)

Uso de plugins con Turbopack

Para usar plugins con Turbopack, actualice a la última versión de @next/mdx y especifique los nombres de los plugins usando una cadena:

next.config.mjs
import createMDX from '@next/mdx'

/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
}

const withMDX = createMDX({
  options: {
    remarkPlugins: [],
    rehypePlugins: [['rehype-katex', { strict: true, throwOnError: true }]],
  },
})

export default withMDX(nextConfig)

Nota importante:

Los plugins de remark y rehype sin opciones serializables aún no pueden usarse con Turbopack, debido a la incapacidad de pasar funciones JavaScript a Rust

MDX remoto

Si sus archivos o contenido MDX se encuentran en otro lugar, puede recuperarlos dinámicamente en el servidor. Esto es útil para contenido almacenado en un CMS, base de datos o cualquier otro lugar. Un paquete comunitario para este caso de uso es next-mdx-remote-client.

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

El siguiente ejemplo utiliza next-mdx-remote-client:

import { MDXRemote } from 'next-mdx-remote-client/rsc'

export default async function RemoteMdxPage() {
  // Texto MDX - puede provenir de una base de datos, CMS, fetch, etc...
  const res = await fetch('https://...')
  const markdown = await res.text()
  return <MDXRemote source={markdown} />
}

Navegar a la ruta /mdx-page-remote debería mostrar su MDX renderizado.

Análisis profundo: ¿Cómo se transforma markdown en HTML?

React no entiende markdown de forma nativa. El texto plano markdown primero debe transformarse en HTML. Esto puede lograrse 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 en 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 en HTML serializado
    .process('Hello, Next.js!')

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

El ecosistema de remark y rehype incluye plugins para resaltado de sintaxis, enlaces a encabezados, generación de tablas de contenido, y más.

Cuando usa @next/mdx como se mostró anteriormente, no necesita usar remark o rehype directamente, ya que se maneja por usted. Lo describimos aquí para un entendimiento más profundo de lo que hace el paquete @next/mdx internamente.

Uso del compilador MDX basado en Rust (experimental)

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

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

mdxRs también acepta un objeto para configurar cómo transformar archivos mdx.

next.config.js
module.exports = withMDX({
  experimental: {
    mdxRs: {
      jsxRuntime?: string            // Runtime JSX personalizado
      jsxImportSource?: string       // Fuente de importación JSX personalizada,
      mdxType?: 'gfm' | 'commonmark' // Configurar qué tipo de sintaxis MDX se usará para analizar y transformar
    },
  },
})

Enlaces útiles