Saltar al contenido

Plugins rehype: transformar el HTML resultante

La segunda mitad del pipeline: una vez que el mdast se convierte en hast, el árbol del HTML, los plugins rehype intervienen sobre la salida. Añadir id a los encabezados con rehype-slug, enlazarlos con rehype-autolink-headings, y escribir un plugin propio que inyecta atributos recorriendo el hast con unist-util-visit; todo registrado en rehypePlugins.

Del mdast al hast: el cambio de plano

El pipeline no salta del Markdown al texto HTML de golpe. Entre medias, un puente llamado remark-rehype convierte el mdast en otro árbol, el hast —el HTML Abstract Syntax Tree—. Donde el mdast tenía nodos de tipo heading o link, el hast tiene nodos de tipo element con un tagName como h2 o a y un objeto properties con sus atributos. Es el mismo documento, contado en el vocabulario del HTML.

Ese cambio de vocabulario define para qué sirve cada fase. En el mdast razonas sobre la semántica del contenido: esto es un encabezado de nivel dos, esto es un enlace. En el hast razonas sobre la forma del HTML: esta etiqueta es un h2, este a tiene tal href. Cuando tu transformación se expresa mejor en términos de etiquetas y atributos —añadir un class, un id, un rel— el hast, y por tanto rehype, es el lugar correcto.

TypeScript
// Forma simplificada de un nodo element del hasttype HastElement = {  type: 'element';  tagName: string;                       // 'h2', 'a', 'img'...  properties: Record<string, unknown>;   // { id, className, href... }  children: HastElement[];};

El puente remark-rehype no es una traducción trivial de uno a uno. Algunos conceptos del Markdown no tienen equivalente directo en HTML y el puente decide cómo representarlos —las notas al pie, por ejemplo, se convierten en una lista al final con enlaces de ida y vuelta—. Por eso el paso de mdast a hast es también un punto de configuración, y no solo de transformación: lo que el puente elige por defecto puede ajustarse, algo que retomarás cuando veas remarkRehype. De momento basta con retener que cruzar de un árbol a otro implica decisiones, no una copia.

El caso de uso más común de rehype es hacer enlazables los encabezados. Requiere dos pasos y dos plugins. Primero, rehype-slug recorre el hast y asigna a cada encabezado un id derivado de su texto. Después, rehype-autolink-headings añade dentro de cada encabezado un <a> que apunta a ese mismo id, de modo que cada título se convierte en un enlace a sí mismo.

TypeScript
// astro.config.mjsimport rehypeSlug from 'rehype-slug';import rehypeAutolinkHeadings from 'rehype-autolink-headings';export default {  markdown: {    rehypePlugins: [      rehypeSlug,      [rehypeAutolinkHeadings, { behavior: 'wrap' }],    ],  },};

El orden aquí no es negociable: rehype-autolink-headings necesita que el id ya exista para poder apuntar a él, así que rehype-slug debe ir antes. Invertirlos produce enlaces que apuntan a anclas vacías. Es el ejemplo canónico de una dependencia entre plugins: uno prepara el terreno, el otro lo aprovecha.

La opción behavior decide cómo se coloca ese enlace, y cada valor sirve a una intención distinta. Con wrap, el enlace envuelve el texto del título, de modo que todo el encabezado es clicable. Con append o prepend, se inserta un pequeño ancla —a menudo un símbolo de eslabón— después o antes del texto, dejando el título intacto y añadiendo un asidero visible al pasar el ratón. La elección no es cosmética: wrap conviene cuando quieres que el título entero sea un enlace; append cuando prefieres un ancla discreta que no compita con la lectura.

Un plugin rehype propio: inyectar atributos

Cuando la transformación no la cubre un plugin existente, la escribes. El patrón es idéntico al de remark —una función que devuelve un transformer(tree)— pero el árbol es el hast y lo que buscas son etiquetas. Para recorrerlo sin escribir la recursión a mano se usa unist-util-visit, una utilidad que visita cada nodo que coincida con un criterio.

El ejemplo típico: marcar los enlaces externos para que abran en otra pestaña de forma segura. Visitas cada element de tipo a, compruebas si su href sale del sitio y, si es así, añades target y rel a sus properties.

TypeScript
// rehype-enlaces-externos.mjsimport { visit } from 'unist-util-visit';export function rehypeEnlacesExternos() {  return function (tree) {    visit(tree, 'element', (node) => {      if (node.tagName !== 'a') return;      const href = node.properties?.href ?? '';      if (href.startsWith('http')) {        node.properties.target = '_blank';        node.properties.rel = 'noopener noreferrer';      }    });  };}

unist-util-visit es el caballo de batalla de estas transformaciones. Le pasas el árbol, el tipo de nodo que te interesa y una función que se ejecuta por cada coincidencia; dentro puedes leer y mutar properties con total libertad. Añadir una clase a cada img, un atributo de carga diferida, un data-* a cada tabla: todo es la misma coreografía de visitar y modificar.

El segundo argumento de visit admite más que un nombre de tipo. Puedes pasarle una función de prueba que decida, nodo a nodo, si te interesa —por ejemplo, solo los a cuyo href empieza por http— y así estrechar el recorrido sin comprobar la condición dentro. También importa saber que mutar properties en el sitio es seguro: el árbol es tuyo durante esta fase y los cambios se propagan al HTML final tal cual los dejes. No devuelves un árbol nuevo; modificas el que recibes, y esa mutación es el efecto del plugin.

TypeScript
import { visit } from 'unist-util-visit';// El test como función: filtra en el propio recorridoexport function rehypeClaseExternos() {  const esExterno = (n) =>    n.tagName === 'a' && String(n.properties?.href).startsWith('http');  return (tree) => {    visit(tree, esExterno, (node) => {      node.properties.className = ['externo'];    });  };}

Registrar en rehypePlugins y el orden

Los plugins rehype se enchufan en markdown.rehypePlugins, hermano de remarkPlugins pero para la fase de HTML. Un plugin sin opciones se registra por su nombre; uno con opciones, como una tupla de dos elementos donde el segundo es el objeto de configuración.

TypeScript
// astro.config.mjsimport rehypeSlug from 'rehype-slug';import rehypeAutolinkHeadings from 'rehype-autolink-headings';import { rehypeEnlacesExternos } from './rehype-enlaces-externos.mjs';export default {  markdown: {    rehypePlugins: [      rehypeSlug,      [rehypeAutolinkHeadings, { behavior: 'append' }],      rehypeEnlacesExternos,    ],  },};

Fíjate en la simetría con remark: los mismos tres huecos —el nombre suelto, la tupla con opciones, el plugin propio— y la misma semántica de orden. Quien haya configurado remarkPlugins ya sabe configurar rehypePlugins; solo cambia el árbol sobre el que actúan. Esa uniformidad no es casual, sino la señal de que ambos son la misma maquinaria de unified aplicada a dos planos distintos del mismo documento, con idéntico contrato de registro.

Como en remark, los plugins se aplican en el orden del array, cada uno sobre el hast que dejó el anterior. Esa secuencialidad es la que convierte el orden en una decisión de diseño: primero se ponen los id, luego se enlazan, después se retocan atributos. Pensar el array como una tubería —no como un conjunto— es lo que evita el error de encadenar plugins que dependen de un trabajo aún no hecho.