wandres.dev
MDX A FONDO · componentes y expresiones

El prop components al renderizar

Mapear los elementos HTML que genera el Markdown —h1, a, pre— a componentes propios pasando un objeto al prop components al renderizar un documento MDX: cómo reemplazar de forma global la presentación sin tocar el contenido, desde una colección con render y Content o importando el módulo, y la frontera entre definir el mapa dentro del fichero o desde fuera.

⏱ 14 min

El Markdown de un documento genera etiquetas HTML anónimas: una almohadilla produce un <h1>, un enlace produce un <a>, un bloque de código produce un <pre>. El prop components te deja interceptar esa generación y sustituir cada etiqueta por un componente propio, de forma global y sin tocar una coma del contenido. Es el mecanismo con el que un mismo texto se viste de mil maneras: el autor escribe la estructura, y quien lo renderiza decide cómo se ve cada pieza.

🎯 Al terminar esta lección sabrás
  • Entender el prop components como un mapa de etiquetas HTML a componentes propios.
  • Reemplazar h1, a y pre por componentes que añaden comportamiento o estilo.
  • Pasar ese mapa al renderizar desde una colección con render y Content, o importando el módulo.
  • Discernir cuándo conviene definir el mapa dentro del fichero MDX y cuándo desde fuera.

El prop components: reemplazar el HTML generado

Cuando Astro compila el Markdown de un documento, cada construcción se traduce a su etiqueta HTML canónica. Ese <h1> o ese <a> son genéricos: no saben de tu diseño ni de tu comportamiento. El prop components cambia eso. Al renderizar el documento, le pasas un objeto cuyas claves son nombres de etiquetas HTML y cuyos valores son componentes tuyos; a partir de ahí, cada vez que el Markdown produzca esa etiqueta, Astro renderiza tu componente en su lugar.

---
import { Content } from '../content/nota.mdx';
import Enlace from '../components/Enlace.astro';
---
<Content components={{ a: Enlace }} />

Con ese único mapa, todos los enlaces del documento —cada [texto](url) del Markdown— pasan a renderizarse con tu componente Enlace, que recibe las mismas props que habría recibido el <a> nativo: su href, su contenido como slot, sus atributos. El contenido no cambió ni una letra; cambió quién dibuja sus enlaces. Esa es la esencia del mecanismo: una intervención global en la presentación, declarada desde fuera del texto.

Conviene apreciar lo que esto evita. Sin el prop components, personalizar cómo se ven los enlaces de un documento obligaría a editar el documento mismo —reemplazar cada enlace de Markdown por una etiqueta de componente— y a repetir esa cirugía en cada fichero y cada vez que cambiara el diseño. El mapa externo hace innecesaria esa intromisión: la prosa sigue siendo prosa limpia, con su sintaxis de Markdown intacta, y toda la decisión de presentación se concentra en un objeto que vive fuera. El autor no tiene que saber que sus enlaces se están enriqueciendo, y quien los enriquece no tiene que tocar una línea de contenido.

Mapear h1, a y pre a componentes propios

El mapa admite tantas entradas como etiquetas quieras interceptar. Las tres más útiles en la práctica son el encabezado, el enlace y el bloque de código, porque cada una gana algo concreto al convertirse en componente: el h1 puede añadir un ancla enlazable, el a puede distinguir enlaces externos, y el pre puede envolver el código en un botón de copiar.

---
import { getEntry, render } from 'astro:content';
import Titulo from '../components/Titulo.astro';
import Enlace from '../components/Enlace.astro';
import Bloque from '../components/Bloque.astro';

const entrada = await getEntry('docs', Astro.params.slug);
const { Content } = await render(entrada);
---
<Content components={{ h1: Titulo, a: Enlace, pre: Bloque }} />

Cada componente del mapa recibe las props del elemento que sustituye y su contenido por el slot. Un Titulo recibe el texto del encabezado y puede generar un id para enlazarlo; un Enlace recibe el href y decide si abrir en pestaña nueva según sea externo; un Bloque recibe el código ya resaltado y lo rodea de su interfaz. Las claves del mapa son los nombres de etiqueta en minúscula —h1 a h6, a, p, pre, code, img, blockquote, ul, ol, li, table— y cubren toda la salida del Markdown.

📝
Tu componente recibe las mismas props que la etiqueta nativa

Un componente que mapea a recibe href y el contenido del enlace; uno que mapea img recibe src y alt; uno que mapea pre recibe el bloque de código ya procesado. Astro te entrega exactamente lo que habría recibido la etiqueta HTML, así que tu componente debe reenviar esas props al elemento real que emita —un <a href={href}> con su slot— para no perder el atributo por el camino. Interceptar no es inventar: es envolver.

Un Enlace que distingue enlaces externos ilustra el patrón completo. Recibe el href que el Markdown generó, decide por su forma si apunta fuera del sitio y reenvía todo a un <a> real, añadiendo los atributos que quiera:

---
// src/components/Enlace.astro
interface Props { href: string; }
const { href } = Astro.props;
const externo = href.startsWith('http');
---
<a href={href} target={externo ? '_blank' : undefined} rel={externo ? 'noopener' : undefined}>
  <slot />
</a>

El componente no rompe nada de lo que el autor escribió: cada [texto](url) sigue siendo un enlace con su destino y su contenido. Solo añade comportamiento —abrir fuera, marcar la relación— que el <a> genérico no tenía. Ese es el equilibrio que busca un buen interceptor: enriquecer sin alterar la semántica que el documento declaró.

Desde una colección o desde un import

Hay dos escenarios donde pasas el prop components, y ambos comparten la misma idea. El primero es renderizar una entrada de una colección: recuperas la entrada, la conviertes en un componente Content con render, y le pasas el mapa. El segundo es importar directamente un .mdx como módulo, que expone un componente Content al que igualmente pasas components. En los dos casos, el mapa viaja como prop en el punto de renderizado.

---
// Importando el módulo MDX directamente
import { Content } from '../content/guia.mdx';
import Enlace from '../components/Enlace.astro';
import Bloque from '../components/Bloque.astro';
---
<Content components={{ a: Enlace, pre: Bloque }} />
flowchart TD
MD[markdown del documento] --> GEN[genera h1 a y pre]
GEN --> MAP[prop components mapea etiquetas]
MAP --> C1[h1 pasa a Titulo propio]
MAP --> C2[a pasa a Enlace propio]
MAP --> C3[pre pasa a Bloque propio]
C1 --> OUT[HTML final personalizado]
C2 --> OUT
C3 --> OUT
style MAP fill:#89b4fa,color:#11111b
style OUT fill:#a6e3a1,color:#11111b

La ventaja de pasar el mapa desde el punto de renderizado es el alcance: un solo objeto, definido una vez en la ruta que pinta el contenido, gobierna la presentación de todas las entradas que pasen por ella. Cambiar cómo se ven los enlaces de un blog entero es editar una línea en la plantilla que renderiza las entradas, no revisar cientos de documentos. La presentación se centraliza en el borde, y el contenido permanece limpio de decisiones de estilo.

Cuando ese mapa crece —encabezados con ancla, enlaces externos, bloques de código con botón de copiar, imágenes optimizadas— conviene extraerlo a un módulo compartido y reutilizarlo en cada ruta que renderice contenido. Un fichero que exporte el objeto components y se importe donde haga falta convierte la presentación del contenido en una sola fuente de verdad: un cambio de política —por ejemplo, marcar todos los enlaces externos con un icono— se hace una vez y se propaga a cada página que use ese mapa, sin duplicar el objeto por las plantillas. La misma disciplina que aplicas a los estilos globales se aplica aquí al mapa de componentes.

Global frente a local: dónde definir el mapa

Existe una alternativa a pasar el mapa desde fuera: dentro de un .mdx, puedes importar componentes y usarlos con nombre en el propio texto, o incluso reasignar etiquetas localmente. La diferencia es de alcance y de responsabilidad. El prop components desde fuera es global y externo: una decisión de quien renderiza, que se aplica a todo el documento sin que este se entere. Usar componentes con nombre dentro del .mdx es local y explícito: una decisión del autor, visible en el texto.

🌐

Mapa externo con components

Quien renderiza decide la presentación de todo el documento sin editarlo. Ideal para aplicar un mismo estilo a muchas entradas de una colección desde un único punto.

✍️

Componentes en el texto

El autor inserta piezas concretas con nombre allí donde las quiere. Ideal para un componente puntual que solo tiene sentido en ese documento.

El criterio para elegir es la naturaleza de la decisión. Si quieres que todos los enlaces de todos los documentos se comporten igual, eso es una política de presentación y vive en el mapa externo. Si quieres una alerta concreta en el segundo párrafo de un documento concreto, eso es contenido y vive en el texto. Confundirlos lleva a duplicar: mapear a mano en cada fichero lo que un solo mapa externo resolvería, o esconder en un mapa global lo que era una pieza única de un documento.

💡
El mapa también cubre nombres de componente, y el fichero manda sobre lo externo

El prop components no se limita a etiquetas HTML: sus claves también pueden ser nombres de componente que el MDX usa sin importar, de modo que un documento puede escribir <Nota> y dejar que quien lo renderice decida qué componente encarna ese nombre. Y cuando un mismo nombre aparece en el mapa externo y a la vez se importa dentro del .mdx, gana el import local del fichero: lo explícito en el documento tiene prioridad sobre lo inyectado desde fuera. Esa regla evita sorpresas —un mapa global nunca pisa lo que un autor eligió a conciencia— y te deja combinar una política general con excepciones locales sin conflicto.

El prop components es una inversión de control sobre la presentación

Bajo la mecánica de mapear etiquetas a componentes late uno de los principios más profundos del diseño de software: la inversión de control. En el modelo ingenuo, el documento manda sobre su presentación —decide que sus enlaces son <a> azules y subrayados— y quien lo consume se limita a mostrarlo tal cual. El prop components da la vuelta a esa relación: el documento renuncia a decidir cómo se ve cada pieza y solo declara qué es cada pieza —esto es un enlace, esto es un encabezado, esto es un bloque de código—, mientras que quien lo renderiza toma el control de cómo se dibuja. El contenido se queda con la semántica; el anfitrión se queda con la presentación. Esa separación es exactamente la que persiguió durante décadas la web con el ideal de separar estructura y estilo, y aquí reaparece llevada un paso más allá: no solo separas el CSS del HTML, separas la decisión misma de qué componente encarna cada elemento semántico, y la mueves fuera del documento, al lugar que conoce el contexto. Las consecuencias son grandes y todas apuntan en la misma dirección: el contenido se vuelve portable. Un mismo documento MDX, sin cambiar una coma, se puede renderizar con un mapa de componentes en la web pública, con otro en la documentación interna y con un tercero en un correo, y en cada contexto sus enlaces, sus títulos y su código adoptan la forma que ese contexto necesita. El autor escribe una vez y no ata su texto a una apariencia; el anfitrión decide la apariencia y no toca el texto. Es el mismo patrón que hace poderosos a los sistemas de plantillas, a la inyección de dependencias y a las interfaces frente a las implementaciones: en todos, una pieza declara lo que necesita y otra decide cómo satisfacerlo, y esa grieta entre el qué y el cómo es la que permite que las dos evolucionen por separado. Dominar el prop components es, por eso, algo más que saber personalizar un enlace. Es entender que la presentación de un contenido no tiene por qué vivir dentro del contenido, y que sacarla fuera —dársela a quien conoce el contexto de renderizado— es lo que convierte un documento en un activo reutilizable en lugar de una página atada a un único aspecto. El día que ves el prop components como una inversión de control y no como un truco de estilo, empiezas a diseñar contenido que sobrevive a los rediseños, porque nunca supo cómo se veía.

⚔️ Toma el control de la presentación desde fuera
  1. Escribe un componente Enlace.astro que reciba href, abra en pestaña nueva los enlaces externos y reenvíe el resto de props a un <a>; mapéalo con components={{ a: Enlace }} al renderizar un .mdx.
  2. Añade al mapa un Titulo para h1 que genere un id a partir del texto y un Bloque para pre con un botón de copiar; comprueba que se aplican a todo el documento.
  3. Renderiza una entrada de una colección con render y Content, pasa el mapa desde la ruta, y verifica que todas las entradas heredan la misma presentación sin editarlas.
  4. Contrasta el mapa externo con insertar un componente con nombre dentro del .mdx, y decide para dos casos reales cuál corresponde a una política global y cuál a una pieza local.