Directivas de template: set:html, is:inline, define:vars
Las directivas que gobiernan los casos especiales: set:html y set:text para inyectar contenido crudo o escapado, is:inline para dejar scripts y estilos sin procesar, y define:vars para cruzar datos del servidor al codigo que corre en el cliente.
Casi todo en Astro tiene un comportamiento por defecto sensato: el texto se escapa, los scripts se empaquetan, los estilos se aíslan. Las directivas de plantilla son las palancas explícitas para desviarse de esos defaults cuando de verdad hace falta. Cada una es una decisión consciente —y rastreable— de salir del camino trazado, y por eso se escriben con nombre y no se activan solas.
- Inyectar HTML crudo con
set:htmly texto escapado conset:text, con criterio de seguridad. - Dejar un
<script>o un<style>intactos conis:inline, sin el procesado por defecto. - Cruzar datos del servidor al cliente con
define:varsy entender que implicais:inline. - Ver estas directivas como salidas explícitas de un default seguro y procesado.
set:html y set:text: crudo o escapado
Por defecto, {valor} escapa el HTML: una cadena con <b> se pinta como texto literal, no como negrita. Es la red de seguridad contra la inyección. set:html es la renuncia deliberada a esa red: inyecta la cadena tal cual, interpretando sus etiquetas. Su pareja, set:text, hace lo contrario y fuerza el escapado, de modo que aunque la cadena traiga etiquetas se muestran como texto.
---
const desdeCMS = '<strong>Oferta</strong> por tiempo limitado';
---
<div set:html={desdeCMS} />
<Fragment set:html={desdeCMS} />
set:html es una puerta abierta a la inyección si la cadena no es de confianza: nunca la uses con contenido que venga de un usuario sin sanearlo antes. Su terreno legítimo es el contenido que tú controlas —Markdown ya renderizado, HTML de un CMS previamente saneado—.
Escapar y sanear son defensas distintas. El escapado —lo que hace la llave— neutraliza todo el HTML convirtiéndolo en texto. Sanear —lo que necesitas antes de un set:html— elimina solo lo peligroso y conserva el marcado legítimo, como los <strong> o los enlaces de un artículo. Cuando de verdad quieras renderizar HTML ajeno, no basta con confiar: pásalo por una librería de saneado que borre scripts y manejadores de eventos antes de entregárselo a set:html.
Conviene ver set:text no como una directiva exótica sino como la forma explícita del comportamiento por defecto de la llave: {cadena} y <span set:text={cadena} /> producen exactamente el mismo texto escapado. Su valor está en la intención declarada —dejar constancia en el código de que ahí quieres texto, pase lo que pase con el contenido— y en la posibilidad de colgarlo de un <Fragment> para escapar sin envoltorio. Es el par simétrico de set:html: donde uno confía y abre, el otro desconfía y sella.
Cada set:html de tu código es un punto donde el escapado se apaga. La gran ventaja de que sea una directiva con nombre es que puedes buscarla: un grep de set:html te lista toda tu superficie de inyección para auditarla de un vistazo. Trata cada aparición como una promesa firmada de que esa cadena es de confianza o está saneada; si no puedes garantizarlo, no uses la directiva.
is:inline: scripts y estilos sin procesar
Por defecto Astro procesa los <script> y los <style>: empaqueta el JavaScript, resuelve sus imports, lo mueve y lo deduplica; y aísla el CSS al componente para que no se filtre. is:inline le dice a Astro que deje la etiqueta exactamente donde está, verbatim: sin empaquetar, sin resolver imports, sin aislar, en línea y en su sitio.
<script is:inline>
// se emite tal cual, sin empaquetar ni resolver imports
console.log('Corro inline, en mi sitio exacto');
</script>
Lo necesitas para snippets de terceros que deben ir literales, para un bloque de JSON incrustado, o para un script que debe correr antes que cualquier hidratación. El precio es que renuncias a todo lo que el procesado te daba: si el componente se repite, el script se duplica; los imports no se resuelven; no hay TypeScript. Es una salida de emergencia útil, pero con coste, y por eso conviene usarla solo cuando el procesado por defecto de verdad estorba.
Hay un matiz que sorprende a los recién llegados: un <script> normal en Astro no es el script en línea clásico, sino un módulo que Astro procesa, empaqueta y suele mover fuera del punto donde lo escribiste. Si tu intención era el viejo comportamiento —este código, aquí, tal cual, ahora— is:inline es precisamente lo que lo restaura. No es un modo raro: es el que muchos esperan por defecto, sin saber que Astro hace algo más sofisticado a menos que le pidas lo contrario.
El ejemplo canónico es el snippet de un tercero —una analítica, un chat de soporte— que viene con instrucciones de pegarlo literalmente y que se rompería si Astro intentara empaquetarlo:
<script is:inline src="https://analitica.example/tag.js"></script>
<script is:inline>
window.dataLayer = window.dataLayer || []
dataLayer.push({ evento: 'carga' })
</script>
El mismo is:inline vale para los estilos: un <style is:inline> se emite sin el aislamiento por componente que Astro aplica por defecto, tal cual y con alcance global. Se usa poco —el aislamiento suele ser justo lo que quieres—, pero existe para los casos en que de verdad necesitas un bloque de CSS crudo, global y en su sitio exacto, sin que el compilador lo toque.
define:vars: del servidor al cliente
El frontmatter corre en el servidor; un <script> de cliente corre en el navegador. Son dos mundos que no comparten memoria: la variable que calculaste arriba no existe abajo. define:vars es el puente sancionado: serializa variables del servidor y las inyecta como declaraciones al principio del bloque, ya sea un script o un estilo.
---
const usuario = 'Ada';
const intentos = 3;
---
<script define:vars={{ usuario, intentos }}>
console.log(`${usuario} lleva ${intentos} intentos`);
</script>
<style define:vars={{ acento: '#89b4fa' }}>
a { color: var(--acento); }
</style>
En el script, Astro antepone las declaraciones serializadas —const usuario = "Ada"; const intentos = 3;— para que el código de cliente las use. En el estilo, las convierte en variables CSS —--acento— que referencias con var(). El caso del <style> es especialmente elegante para temas: pasas un color calculado en el servidor como propiedad personalizada y lo consumes con var(--acento) en reglas normales, aisladas al componente, sin sembrar atributos style en línea por todo el marcado. El dato viaja una sola vez, como variable CSS, y todo el estilo del componente puede leerlo.
No hace falta que escribas is:inline junto a define:vars: la primera implica la segunda. Y tiene toda la lógica: un script que inyecta valores distintos en cada render no puede compartir un bundle con otros, porque su contenido ya no es fijo. Recordar esta implicación te evita sorpresas: cualquier <script> con define:vars renuncia de forma automática al empaquetado y a la resolución de imports, y vuelve a ser un bloque literal en su sitio.
<script define:vars={{ tema: 'oscuro' }}>
document.documentElement.dataset.tema = tema
</script>
<!-- Astro antepone const tema = "oscuro"; antes de tu codigo -->
Lo que cruza la frontera es una copia serializada, no una referencia viva: solo sobreviven valores serializables a JSON —nada de funciones ni objetos con métodos—, y el valor es una fotografía del instante del render. Y como se escribe en el HTML de la página, jamás metas secretos por ahí.
El límite de la serialización conviene tenerlo presente. Una fecha viaja como su representación en texto, no como un objeto Date con métodos; un Map, un Set o una función no sobreviven al cruce. Si el cliente necesita reconstruir algo complejo, pasa los datos primitivos con define:vars y rehaz el objeto allí, ya en el navegador, con el material que sí atravesó la frontera. Este es, además, el único cruce real de la frontera servidor-cliente en todo el nivel de templating: lo que sigue —las islas y las directivas client:*— vive al otro lado, y define:vars es la primera grieta por la que un dato del servidor llega al código que corre en el navegador.
flowchart LR FM[variables del frontmatter] --> DV[serializa una copia] DV --> INL[el bloque pasa a inline] INL --> CLI[script o estilo en el navegador] style FM fill:#89b4fa,color:#11111b style CLI fill:#a6e3a1,color:#11111b
Las cuatro directivas de esta lección parecen un cajón de sastre —inyectar HTML, dejar un script quieto, pasar variables— pero comparten una misma naturaleza: son el vocabulario con el que Astro nombra las fronteras que por defecto mantiene ocultas. Astro es un framework de defaults fuertes: el texto se escapa, los scripts se empaquetan, los estilos se aíslan, y casi nunca tienes que pensar en ello. Esos defaults no son casualidad, son una postura —seguro y procesado salvo que digas lo contrario—. Cada directiva set:, is: o define:vars es una renuncia explícita a uno de esos defaults, y lo decisivo es que sea explícita: para apagar el escapado tienes que escribir set:html; para saltarte el empaquetado, is:inline; para cruzar datos al cliente, define:vars. Un buen diseño de framework no se mide por la ausencia de salidas de emergencia, sino por su honestidad: una salida que tienes que teclear es una decisión que tienes que asumir, y una que se puede encontrar con un grep es una decisión que tu equipo puede auditar. Por eso set:html es a la vez tu única vía para inyectar HTML y tu mapa completo de superficie de XSS. Y define:vars es la más profunda de todas, porque no desactiva un default menor: nombra la frontera más dura de todo el modelo, la que separa un servidor que corre una vez de un cliente que corre después. A través de ella no pasa una referencia sino una copia congelada, serializada a JSON, muerta en el instante en que cruza. Poner un nombre a esa frontera —en vez de fingir que los dos mundos son uno— es Astro negándose a mentirte sobre dónde corre tu código. Las directivas son, en el fondo, el lugar donde el framework deja de ser magia y te enseña sus costuras una por una, para que sepas exactamente qué default estás apagando y qué responsabilidad aceptas a cambio.
- Inyecta una cadena con etiquetas usando
set:htmlen un<div>y en un<Fragment>; compara la salida con la de interpolar la misma cadena con{}. - Pasa esa misma cadena por
set:texty confirma que las etiquetas se muestran como texto literal, escapadas. - Escribe un
<script>normal y otro conis:inline; inspecciona el HTML y anota cuál empaqueta Astro y cuál deja intacto en su sitio. - Usa
define:varspara pasar dos variables del frontmatter a un<script>de cliente y comprueba que aparecen serializadas como declaraciones al inicio del bloque.