wandres.dev
TEMPLATING · expresiones y directivas

Expresiones en el template

La llave como interpolador de expresiones JavaScript, la frontera entre el frontmatter y el HTML, y la diferencia esencial con JSX: en Astro las expresiones se evaluan una sola vez en el servidor y su resultado se congela en el marcado.

⏱ 13 min

El componente .astro tiene dos mitades: un guion que corre en el servidor y una plantilla que produce HTML. La llave {} es la costura entre ambas, el punto donde un valor calculado arriba se cuela en el marcado de abajo. Parece la misma llave de JSX, pero esconde una diferencia de fondo: aquí la expresión se evalúa una vez, en el servidor, y su resultado se congela en el HTML para siempre.

🎯 Al terminar esta lección sabrás
  • Interpolar variables, llamadas y operaciones con la llave {} en la plantilla.
  • Distinguir una expresión, que produce un valor, de una sentencia, que no cabe en la llave.
  • Entender que el frontmatter corre en el servidor y su resultado se serializa a HTML.
  • Situar la diferencia con JSX: sin re-render, sin reactividad, sin hooks en el cliente.

La llave: del frontmatter a la plantilla

Un .astro es literalmente dos zonas. Entre las dos líneas de --- vive el frontmatter: JavaScript o TypeScript normal que se ejecuta en el servidor antes de emitir nada. Debajo empieza la plantilla, que es HTML con una única superpotencia: la llave. Todo lo que escribes fuera de una llave es texto literal; todo lo que encierras en una llave es una expresión que Astro evalúa y sustituye por su valor en ese hueco exacto.

---
const nombre = 'Ada';
const anio = 2026;
const saludar = (quien) => `Hola, ${quien}`;
---
<h1>{saludar(nombre)}</h1>
<p>Año actual: {anio}. El próximo: {anio + 1}.</p>

Dentro de la llave no hay una mini-sintaxis que memorizar: es JavaScript de verdad. Interpolas una variable, sí, pero también una llamada a función, una operación aritmética, un acceso a propiedad o el resultado de encadenar métodos. La plantilla no aprende un lenguaje nuevo; hereda el que ya conoces. Esa es la primera decisión de diseño que conviene interiorizar: Astro no reinventa el control de flujo, lo toma prestado del lenguaje anfitrión.

Fuera de las llaves, todo es texto literal, incluida la separación entre elementos: los espacios, los saltos de línea y la puntuación que escribes se emiten tal cual. Dentro de una llave puede ir una expresión tan escueta como un nombre o tan rica como una cadena de métodos con encadenamiento opcional; Astro no pone límite a su complejidad, solo a su categoría gramatical.

---
const autor = { nombre: 'Ada', obras: ['Notas', 'Análisis'] };
---
<p>{autor.nombre} escribió {autor.obras.length} obras.</p>
<p>La primera, en mayúsculas: {autor.obras.at(0)?.toUpperCase()}</p>

Expresión, no sentencia

La restricción central es sutil y define todo lo demás: la llave admite una expresión, no una sentencia. Una expresión evalúa a un valor —2 + 2, usuario.nombre, saludar(nombre)—; una sentencia realiza una acción y no produce valor alguno —if, for, while, const, return—. Como Astro necesita algo que insertar en el hueco, solo lo que tiene valor cabe ahí.

Expresiones (caben en la llave)

Variables, llamadas a función, aritmética, acceso a propiedades, plantillas de texto, ternarios y el operador &&. Todo lo que devuelve un valor insertable.

🧱

Sentencias (viven en el frontmatter)

if, for, while, const, let, return. Son acciones sin valor; su sitio es la zona superior, no la plantilla.

El compilador te lo recuerda sin piedad: colar una sentencia en la llave es un error de sintaxis, no un aviso suave que puedas ignorar.

{/* válido: expresiones, todas devuelven un valor */}
<p>{esVip ? 'Bienvenido' : 'Hola'}</p>
<ul>{tareas.map((t) => <li>{t}</li>)}</ul>

{/* inválido: sentencias, no caben en la llave */}
{if (esVip) { ... }}
{for (const t of tareas) { ... }}

¿Cómo ramificas o iteras entonces en la plantilla? Traduciendo cada sentencia a su forma de expresión: el ternario sustituye al if, el método .map sustituye al for, el operador && expresa el mostrar-u-ocultar. Y cuando la lógica crece hasta volverse ilegible, no la fuerzas dentro de la llave: la subes al frontmatter, le das un nombre claro y dejas en la plantilla solo la interpolación del resultado. La regla práctica es una línea: calcula arriba, interpola abajo.

💡
El resquicio de la función invocada

Existe un único truco para colar sentencias donde se espera una expresión: envolverlas en una función que se invoca al instante y devuelve un valor —{(() => { return x })()}—. Funciona porque la llamada es una expresión, aunque su cuerpo contenga sentencias. Pero úsalo con moderación: casi siempre es señal de que esa lógica debería vivir en el frontmatter con un nombre claro, no incrustada y anónima en mitad del marcado. El resquicio existe; abusar de él delata que estás peleando con la separación que Astro te ofrece gratis.

Evaluación en el servidor: la frontera con JSX

Aquí está el corazón de la lección, y el punto donde la intuición de quien viene de React falla. En un framework de cliente, la llave de JSX describe un vínculo vivo: la función del componente se reejecuta en cada cambio de estado y la expresión se reevalúa una y otra vez en el navegador. En Astro no hay tal cosa. El componente .astro corre exactamente una vez —en el build para un sitio estático, en la petición para SSR— y nunca más. No hay re-render, ni estado reactivo, ni hooks. La llave se evalúa una sola vez y su resultado se serializa a una cadena de HTML.

---
const hora = new Date().toLocaleTimeString();
---
<p>Página renderizada a las {hora}</p>
<!-- salida: <p>Página renderizada a las 12:00:00</p> -->

En el HTML que llega al navegador no queda new Date() ni la llave: solo el texto ya calculado. El navegador jamás ejecuta esa expresión; recibe su ceniza. Por eso un onclick={...} no cablea un manejador vivo como en React: Astro es, en esencia, un motor de plantillas con JavaScript real, más cercano a un render de servidor que a un runtime de cliente. La interactividad del navegador entra por otra puerta —las islas y las directivas client:*— que verás más adelante, y que asumes conscientemente cuando la necesitas.

Un par de detalles cierran el cuadro. Todo valor interpolado se escapa como HTML por defecto: si una variable contiene <script>, se pinta como texto inerte, no como etiqueta ejecutable; es la defensa de Astro contra la inyección, y para inyectar HTML crudo hay que pedirlo con set:html. Y algunos valores se comportan de forma peculiar al interpolarse: false, null y undefined no pintan nada —utilísimo para condicionales—, pero el número 0 sí pinta su “0”, una trampa que reaparecerá en la próxima lección.

Conviene precisar cuándo es ese “una vez”. En un sitio estático, la expresión se evalúa en el build, y su resultado queda idéntico para todos los visitantes hasta la próxima reconstrucción. En una ruta renderizada bajo demanda (SSR), se evalúa en cada petición, así que puede variar entre un visitante y otro según la cookie, la hora o la base de datos. En ambos casos la mecánica es la misma —se evalúa en el servidor y se congela—; lo único que cambia es cada cuánto vuelve a ejecutarse ese servidor. La llave no sabe ni le importa en cuál de los dos modos corre: siempre entrega una fotografía, solo que a veces se retoma en cada visita.

flowchart LR
FM[frontmatter en el servidor] --> EV[evalua la expresion una vez]
EV --> STR[cadena de HTML congelada]
STR --> NAV[el navegador recibe solo texto]
style FM fill:#89b4fa,color:#11111b
style NAV fill:#a6e3a1,color:#11111b
ℹ️
La llave no es una atadura reactiva

En un framework de cliente, {cuenta} crea un vínculo vivo: si cuenta cambia, el DOM se actualiza. En Astro no hay tal vínculo. {cuenta} es una fotografía tomada en el servidor: se lee el valor, se escribe en el HTML y se corta el cordón. Cambiar cuenta después no mueve nada, porque no queda nada que mover, solo texto estático. Para actualizaciones en vivo necesitas JavaScript de cliente, que es un mundo aparte.

El escapado automático como red de seguridad

Merece detenerse en el escapado, porque es una de las defensas silenciosas de Astro. Cuando interpolas una cadena, Astro convierte los caracteres especiales de HTML —los que abren etiquetas o entidades— en sus versiones inertes antes de escribirlos. Una variable que contenga marcado con un manejador malicioso no ejecuta nada: se muestra como texto visible, carácter por carácter.

---
const entrada = '<img src=x onerror="robar()">';
---
<p>{entrada}</p>
<!-- salida: <p>&lt;img src=x onerror="robar()"&gt;</p> -->

Esta red está siempre puesta y no cuesta un rendimiento perceptible. Solo se retira cuando tú lo pides explícitamente, con la directiva set:html, que estudiarás al final del nivel. Hasta entonces la regla es tranquilizadora: cualquier dato que interpoles entre llaves llega al navegador como texto, nunca como código ejecutable. La inyección de HTML en Astro no es un descuido posible por defecto, sino una decisión que hay que escribir con todas las letras.

La llave es una puerta de un solo sentido en el tiempo

Interiorizar la llave de Astro exige un cambio de eje: dejar de pensar en el espacio —dónde va el valor— para pensar en el tiempo —cuándo se calcula—. En un framework reactivo, {expresion} es una promesa permanente: mientras el componente viva en el navegador, esa expresión se reevaluará cada vez que algo de lo que depende cambie, y el DOM seguirá ese vaivén. Astro rompe esa promesa a propósito. Su llave se evalúa una única vez, en el servidor, en el instante del build o de la petición, y lo que produce no es un vínculo sino una cadena de texto inmutable que se cose al documento y se envía. La expresión, con toda su lógica, muere en el servidor; al navegador solo viaja su resultado, indistinguible de si lo hubieras escrito a mano. Esta frontera temporal explica casi todo lo demás: por qué no hay hooks ni estado que sobreviva, por qué un manejador de eventos escrito en la llave no se cablea solo, por qué interpolar una fecha te da una hora fija y no un reloj. No es una limitación que Astro no haya superado; es la decisión que lo define. Al evaluar en el servidor y congelar el resultado, Astro paga el coste de la dinámica una sola vez —en tu máquina o en el edge, no en el dispositivo del visitante— y entrega un artefacto que ya no tiene que pensar. Cuando de verdad necesites que algo cambie en vivo, no fuerzas la llave: abres otra puerta, la de las islas, y aceptas el JavaScript que eso cuesta. La llave, mientras tanto, sigue siendo lo que parece: un agujero por el que el servidor vierte un valor en el HTML, y lo sella al pasar.

⚔️ Congela una expresión en el HTML
  1. Crea un componente con una variable y una función en el frontmatter; interpólalos en la plantilla con {} e inspecciona el HTML resultante para confirmar que no queda ninguna llave.
  2. Intenta meter un if dentro de la llave y observa el error; reescribe la misma lógica con un ternario, que sí es una expresión.
  3. Interpola new Date().toLocaleTimeString() y recarga la página varias veces: en un sitio estático la hora no cambia hasta reconstruir, porque la expresión se evaluó una sola vez.
  4. Interpola una variable con valor 0 y otra con false; anota cuál pinta y cuál no, y razona por qué esa diferencia importa para los condicionales.