wandres.dev
EL COMPONENTE .ASTRO · frontmatter y template

El template: HTML con expresiones

La plantilla de un .astro como superconjunto de HTML: interpolar valores de la valla con expresiones entre llaves, renderizar listas y condicionales con JavaScript puro, componer atributos dinámicos, y por qué el resultado es HTML estático sin JavaScript de cliente.

⏱ 13 min

Bajo la valla empieza la plantilla, y aquí Astro toma una decisión de diseño elegante: en lugar de inventar un lenguaje de plantillas con sus propias directivas, extiende el HTML con expresiones de JavaScript. Sabes bucles, condicionales y llamadas a métodos; pues eso es todo lo que necesitas. La plantilla es HTML que ha aprendido a leer los valores de la valla y a resolverlos, una sola vez, en marcado plano.

🎯 Al terminar esta lección sabrás
  • Interpolar valores del frontmatter en la plantilla con expresiones entre llaves.
  • Renderizar listas y condicionales con JavaScript puro, sin directivas propietarias.
  • Componer atributos dinámicos y manejar class:list, booleanos y set:html con criterio.
  • Comprender por qué la salida de un .astro es HTML estático sin JavaScript de cliente.

Expresiones: interpolar la valla en el HTML

El puente entre la valla y la plantilla son las expresiones: cualquier fragmento de JavaScript encerrado entre llaves que Astro evalúa e inserta en su lugar. La forma más simple es interpolar una variable, pero dentro de las llaves cabe cualquier expresión —una propiedad, una llamada a método, una operación aritmética—, porque no es una mini-sintaxis, es JavaScript de verdad.

---
const usuario = { nombre: "Ada", visitas: 42 };
---
<h1>Hola, {usuario.nombre}</h1>
<p>Llevas {usuario.visitas} visitas ({usuario.visitas * 2} puntos)</p>

La restricción es sutil pero importante: las llaves aceptan expresiones, no sentencias. Puedes poner un ternario, un map o un &&, porque todos evalúan a un valor; no puedes poner un if, un for o un const, porque son sentencias que no producen un valor insertable. Toda la lógica que requiera sentencias vive arriba, en la valla; la plantilla solo interpola resultados.

---
const precio = 1990;
const moneda = "EUR";
const enOferta = true;
---
<span>{(precio / 100).toFixed(2)} {moneda}</span>
{enOferta && <span class="etiqueta">Oferta</span>}

Dentro de las llaves cabe una operación aritmética, una llamada a un método como toFixed, un acceso a propiedad o un cortocircuito booleano, porque todo eso evalúa a un valor. Lo que no cabe es declarar, asignar o ramificar con if: esas son sentencias, y su sitio es la valla. La regla se resume en una frase: las llaves interpolan valores, no ejecutan programas.

Listas y condicionales con JavaScript

Astro no tiene un v-for ni un ngIf: usa las herramientas del propio lenguaje. Una lista se renderiza con .map, que transforma un array de datos en un array de elementos. Un condicional se expresa con && para mostrar-u-ocultar, o con un ternario para elegir entre dos ramas. Quien sabe JavaScript ya sabe la sintaxis de plantillas de Astro.

---
const enlaces = [
  { href: "/", texto: "Inicio" },
  { href: "/blog", texto: "Blog" },
];
const sesionIniciada = true;
---
<nav>
  {enlaces.map((e) => <a href={e.href}>{e.texto}</a>)}
</nav>
{sesionIniciada
  ? <button>Salir</button>
  : <a href="/entrar">Entrar</a>}
{enlaces.length === 0 && <p>No hay enlaces</p>}

Este enfoque tiene una virtud pedagógica enorme: no hay una capa de abstracción propietaria que aprender y desaprender. El día que cambies de framework, tu conocimiento de map, ternarios y cortocircuitos booleanos viaja contigo, porque nunca fue conocimiento de Astro, sino de JavaScript.

💡
Extrae la lógica compleja a la valla

Si una expresión de la plantilla crece hasta volverse ilegible, es señal de que pertenece a la valla. Calcula allí una variable con nombre claro —const destacados = posts.filter(...)— y deja en la plantilla solo {destacados.map(...)}. La plantilla se lee mejor cuando expresa qué se muestra; el cómo se calcula pertenece arriba. Esta división mantiene el HTML declarativo y concentra la lógica en un solo lugar.

Cuando el elemento repetido no debe envolverse en un contenedor, usas un Fragment —integrado en Astro, sin import— para agrupar varios nodos sin emitir uno extra. Astro no exige la prop key que pide React, pero conviene dar una clave estable si más tarde hidratas esa lista con un framework:

---
const filas = [
  { id: 1, nombre: "Ada" },
  { id: 2, nombre: "Alan" },
];
---
<dl>
  {filas.map((f) => (
    <Fragment>
      <dt>{f.id}</dt>
      <dd>{f.nombre}</dd>
    </Fragment>
  ))}
</dl>

Fíjate en que el cuerpo del map devuelve marcado directamente: en la plantilla de Astro, el HTML y las expresiones se funden, y una función flecha puede retornar elementos sin ceremonia. Es el mismo patrón mental que en React, salvo que aquí su resultado se congela en el build en vez de vivir en el navegador.

Atributos dinámicos y casos especiales

Los atributos también aceptan expresiones: <a href={url}> inserta el valor de url. Sobre esta base, Astro añade unas pocas comodidades para los casos que en HTML plano resultan tediosos, sin apartarse de su filosofía de no reinventar demasiado.

🎨

class:list

Construye la clase desde un array u objeto: class:list={["btn", { activo }]}. Descarta los valores falsos y une el resto.

🔘

Atributos booleanos

<input disabled={estaBloqueado} /> emite el atributo solo si el valor es verdadero, respetando la semántica de HTML.

⚠️

set:html

Inserta HTML crudo sin escapar: <div set:html={contenido} />. Potente y peligroso; úsalo solo con contenido de confianza.

---
const activo = true;
const contenidoConfiable = "<strong>Ya renderizado</strong>";
---
<button class:list={["btn", { activo }]}>Enviar</button>
<article set:html={contenidoConfiable} />

Por defecto Astro escapa todas las expresiones: si una variable contiene <script>, se imprime como texto inofensivo, no como etiqueta ejecutable. Esa es tu red de seguridad frente a la inyección. set:html es la puerta que la desactiva a propósito, y por eso solo debe recibir contenido que tú controlas —Markdown ya procesado, HTML sanitizado—, nunca texto crudo de un usuario.

⚠️
set:html es una puerta a la inyección

Pasar a set:html una cadena que provenga, aunque sea en parte, de un usuario o de una fuente externa sin sanear abre la puerta a un ataque de inyección de scripts. El escapado por defecto es la red de seguridad de Astro; set:html la retira a conciencia. Trátalo como tratarías dangerouslySetInnerHTML en React: con el nombre de advertencia bien presente y su uso reservado a contenido de origen confiable.

Por qué el resultado es HTML sin JS

Aquí está el corazón de la lección. Cuando Astro procesa la plantilla, evalúa cada expresión en el momento del build o de la petición y la sustituye por su valor. El map se ejecuta y produce las etiquetas; el ternario se resuelve y elige una rama; la variable se reemplaza por su texto. Lo que queda es HTML plano, sin rastro de las llaves ni del JavaScript que las llenó.

flowchart LR
FM[valla: variables y datos] --> EXPR[plantilla: expresiones entre llaves]
EXPR --> EVAL[Astro las evalua en el build]
EVAL --> HTML[HTML estatico final]
HTML --> NOJS[cero JavaScript de cliente]
style FM fill:#89b4fa,color:#11111b
style NOJS fill:#a6e3a1,color:#11111b

Esta es la razón mecánica de que un .astro no envíe JavaScript: sus expresiones no son código que corra en el navegador, sino código que corrió antes para producir el marcado. El visitante recibe el HTML ya resuelto, sin la lógica que lo generó, del mismo modo que recibes un plato cocinado sin la cocina. Ningún runtime acompaña a la página porque no hace falta ninguno: no hay nada que reejecutar.

Merece subrayar el contraste con el renderizado del lado del cliente. Allí el servidor envía datos y una plantilla, y es el navegador quien, ejecutando JavaScript, combina ambos para producir el DOM; la primera pintura depende de que ese código descargue, se analice y corra. En un .astro ese trabajo ya se hizo antes de tiempo: el navegador recibe el DOM terminado y solo tiene que dibujarlo, sin un paso previo de cómputo.

ℹ️
El HTML es el resultado, no la fuente

Ver el HTML final de un .astro puede desconcertar al principio: no hay ni rastro de tus variables, tus map o tus condicionales, solo el marcado plano que produjeron. Eso es exactamente lo esperado. La plantilla es tu código fuente; el HTML es su salida compilada, igual que un binario es la salida de un programa en C. Nadie espera encontrar sus bucles dentro del ejecutable.

La plantilla es una proyección, no una interfaz viva

Quien llega desde React o Vue trae una intuición muy arraigada: la plantilla es la interfaz, un espejo que se mantiene sincronizado con el estado y se repinta cuando ese estado cambia. En Astro esa intuición debe desmontarse, porque la plantilla de un .astro no es un espejo, es una fotografía. Las expresiones no describen una relación continua entre datos y marcado que el framework vigilará en el navegador; describen un cálculo que se hace una vez y se olvida. Cuando escribes {contador} en React, estás declarando una dependencia viva: si contador cambia, ese fragmento del DOM se actualizará. Cuando escribes {contador} en un .astro, estás pidiendo un valor en un instante concreto —el del renderizado— que quedará impreso como texto y nunca volverá a mirarse. La diferencia parece sutil y es abismal, porque explica de un plumazo por qué Astro no envía JavaScript y por qué sus componentes no reaccionan: no hay reactividad que enviar cuando la plantilla nunca prometió reaccionar. Esta reconceptualización tiene una recompensa profunda. Al pensar la plantilla como una proyección de la valla —una función pura de datos a HTML que se ejecuta una sola vez— dejas de arrastrar el peso mental y computacional de mantener una interfaz sincronizada donde no hace falta. La inmensa mayoría de una página web no cambia después de cargarse: un artículo, una cabecera, un pie, una lista de productos. Para todo eso, una fotografía nítida es no solo suficiente sino superior a un espejo, porque la fotografía no exige que un motor de reactividad viaje al cliente para vigilar cambios que nunca ocurrirán. Astro te invita a preguntarte, elemento por elemento: ¿esto necesita seguir vivo en el navegador, o basta con una foto perfecta tomada en el build? Casi siempre, basta con la foto.

⚔️ Proyecta datos en marcado
  1. Declara en la valla un array de objetos y renderízalo como lista con .map; inspecciona el HTML generado y confirma que no queda ninguna llave.
  2. Usa un ternario para alternar entre dos elementos según un booleano, y un && para mostrar un aviso solo cuando una lista esté vacía.
  3. Construye la clase de un elemento con class:list a partir de un objeto con condiciones y verifica qué clases aparecen en la salida.
  4. Asigna a set:html una cadena con etiquetas y luego interpola esa misma cadena entre llaves normales; compara ambos resultados y explica por qué uno se escapa y el otro no.