Fecha publicación: 24 jun 2025

10. Añadiendo soporte a markdown

Ya tenemos la página que muestra el detalle de un curso "casi bien", si te fijas el campo que muestra la guía que acompaña a cada lección se ve raro, ¿Por qué? porque está en formato Markdown y no lo estamos procesando. Vamos a arreglarlo.

Vamos a instalar una librería que nos permita procesar Markdown, para ello vamos a la terminal y ejecutamos:

npm install marked

Y vamos a instalar otra librería que haga syntax highlighting para cuando peguemos código en el Markdown.

npm install highlight.js

Ahora en el layout del proyecto vamos a importar un tema de CSS de highlight.js, para que se vea bien el código (en este caso el tema que elegiremos es el github-dark).

./src/layouts/layout.astro

import "../styles/global.css";
+ import 'highlight.js/styles/github-dark.css';
import Header from "@/components/header.astro";

Tambíen podríamos haber tirado de CDN para importarlo.

Vamos al componente lesson-content.astro y vamos a procesar el contenido de la lección con marked.

Primero importamos marked.

src/components/training/lesson-content.astro

---
+ import { marked } from 'marked';
+
export interface Props {
  content: string;
}

Y ahora, pasamos el content a html:

src/components/training/lesson-content.astro

const { content } = Astro.props;
+ const htmlContent = marked(content);
---

Y en la parte de renderizado, vamos a usar la directiva de Astro set:html para inyectar el HTML generado por marked dentro de un div.

src/components/training/lesson-content.astro

---

<h3 class="text-xl font-bold mb-4 text-gray-800">Contenido de la Lección</h3>
- <p id="lessonContent" class="text-gray-700 leading-relaxed text-base">
-  {content}
- </p>
+ <div set:html={htmlContent}></div>

set:html es una directiva de Astro que permite inyectar HTML directamente en el DOM, es algo parecido a dangerouslySetInnerHTML de React, o a usar en JavaScript plano innerHTML.

Aquí podemos ver que se ve ya con el formato que esperamos, peeerooo si te fijas lo snippets de código que se muestran en la guía, no tienen el syntax highlighting, vamos a arreglarlo.

Vamos a importar highlightjs en el componente lesson-content.astro y vamos a usarlo para resaltar el código.

  import { marked } from 'marked';
+ import hljs from 'highlight.js';

Y toca configurar marked para que se integre con highlightjs vamos a traer el motor que renderiza el markdown a HTML y le vamos a indicar que cuando se encuentre snippets de código en Markdown, que utilize highlightjs para resaltar el código.

src/components/training/lesson-content.astro

  import { marked } from 'marked';
  import hljs from 'highlight.js';

+ const renderer = new marked.Renderer();
+ renderer.code = ({ text, lang }) => {
+  const validLang = hljs.getLanguage(lang || 'plaintext') ? lang || 'plaintext' : 'plaintext';
+  const highlighted = hljs.highlight(text, { language: validLang }).value;
+  return `<pre><code class="hljs ${validLang}">${highlighted}</code></pre>`;
+ };
+
+ marked.setOptions({ renderer });

Y ahora si navegamos al detalle del curso de zustand, se debería de ver bien el snippet de código que hay en la guía.

Vamos a por un pequeño detalle, si te fijas sale todo el texto muy junto, y eso que marked cuando hay saltos de línea los convierte en en parrafos, vamos a añadir una clase para que se vea mejor:

  • En este caso, no queremos cambiar el estilo de los parrafos en global, así que vamos a añadir una clase específica para el contenido de las lecciones.

  • Y para que lo pille en el set:html nos hace falta que sea global.

Así pues en Astro, vamos a definir un estilo global para los párrafos dentro del contenido de las lecciones (si no usamos el :global, los estilso dentro del componente se quedan como locales (están en un ambito local) y no se aplican al HTML inyectado).

Añadir al final del fichero

src/components/training/lesson-content.astro

<style>
  :global(.markdown-content p) {
    margin-bottom: 1em;
  }
</style>

Y ahora la usamos en el div que inyectamos:

src/components/training/lesson-content.astro

- <div set:html={htmlContent} />
+ <div class="markdown-content" set:html={htmlContent} />

¿Ya estamos listos? ... Va ser que no, nos queda un detalle de seguridad: resulta que set:html es una directiva que inyecta HTML directamente en el DOM, lo cual puede ser peligroso si el contenido no es de confianza, ya que puede permitir ataques XSS (Cross-Site Scripting), si no confiamos en el contenido que podamos leer de nuestro Headless CMS, lo ideal es sanitizar el HTML antes de inyectarlo ¿Saniti queeee? Es decir limpiar el HTML de cualquier código malicioso, para ello hay librerías que nos ayudan a hacerlo, como sanitize-html.

En esta librería podemos definir qué etiquetas y atributos queremos permitir en el HTML, y así evitar que se inyecte código malicioso, cuantos más restrictivos seamos, mejor.

Vamos a usarlo:

Instalamos la librería sanitize-html:

npm install sanitize-html

y sus tipos

npm install --save-dev @types/sanitize-html

Vamos a usarlo en el componente lesson-content.astro, así que lo importamos.

src/components/training/lesson-content.astro

  import { marked } from 'marked';
  import hljs from 'highlight.js';
+ import sanitizeHtml from 'sanitize-html';

Y ahora:

  • Hacemos un sanitize del HTML que nos devuelve marked.
  • Sólo permitimos las etiquetas y atributos que consideremos seguros.
  • Para que soporte syntax highlighting, vamos a permitir las etiquetas pre, code y span (esta última la usa highlightjs para colorear el código), así como los atributos class para que se apliquen los estilos de highlightjs.
- const htmlContent = marked(content);
+ const dangerousHtmlContent = await marked(content);

+ const htmlContent = sanitizeHtml(dangerousHtmlContent, {
+  allowedTags: sanitizeHtml.defaults.allowedTags.concat([
+    "pre", // Allow for multiline block code
+    "code", // Allow inline code
+    "span", // Used by highlight.js to color keyword, comments...
+  ]),
+  allowedAttributes: {
+    ...sanitizeHtml.defaults.allowedAttributes, // Default allowed attributes
+    "*": ["class"], // Allow class in all element (used by hljs)
+  },
+ });

Con esto evitaríamos inyectar código malicioso en nuestro HTML, y ya tenemos el contenido de las lecciones renderizado correctamente.

En el próximo video veremos como mejorar la experiencia de navegación del usuario, haciendo una transición fluida cuando cambiemos de lección, para ello usaremos el modo de navegación SPA de Astro.