El frontmatter: código que nunca llega al cliente
El component script de un .astro por dentro: cuándo corre —en el build para rutas estáticas, por petición en las dinámicas—, cómo escribir imports, variables y fetch con await de nivel superior, y por qué ese código jamás se envía al navegador del visitante.
El frontmatter de un .astro es, en esencia, un pequeño servidor: código que se ejecuta lejos del visitante para preparar el HTML que sí verá. Ahí traes datos, importas dependencias y calculas variables, sabiendo que nada de ese trabajo viaja al cliente. Entender cuándo corre y dónde corre —y sobre todo dónde no— es lo que convierte esta zona en tu mayor aliada de rendimiento y seguridad.
- Situar cuándo se ejecuta el frontmatter: en el build para rutas estáticas, por petición en las dinámicas.
- Escribir imports, variables y lógica en la valla como en cualquier módulo de JavaScript.
- Obtener datos con
fetchyawaitde nivel superior sin bloquear ni cargar el cliente. - Interiorizar por qué ese código nunca se envía al navegador y qué implica para secretos y peso.
Cuándo corre la valla
El frontmatter no se ejecuta cuando el visitante abre la página: se ejecuta antes, del lado del constructor. El cuándo exacto depende del output del proyecto que viste en el nivel anterior. Con output: 'static', la valla corre una sola vez durante astro build y su resultado queda congelado en un .html. Con output: 'server' o en una ruta marcada como dinámica, la valla corre de nuevo en cada petición, dentro del servidor, para componer una respuesta fresca.
En ambos casos el rasgo invariante es el mismo: la valla corre en una máquina que no es el navegador del visitante. Tu build en CI o tu servidor de producción ejecutan ese código; el cliente solo recibe el HTML que produjo. Esa distancia es la que hace posible todo lo demás.
flowchart LR
SRC[componente .astro] --> Q{output del proyecto}
Q -->|static| BUILD[la valla corre una vez en el build]
Q -->|server| REQ[la valla corre en cada peticion]
BUILD --> HTML[HTML precomputado]
REQ --> RESP[HTML por respuesta]
HTML --> CLIENT[el navegador recibe solo HTML]
RESP --> CLIENT
style SRC fill:#89b4fa,color:#11111b
style CLIENT fill:#a6e3a1,color:#11111bEsta distinción de tiempos tiene una consecuencia que conviene fijar pronto: en modo estático, si los datos que pides cambian en su origen después del build, tu página no se entera hasta la próxima construcción. En modo servidor, cada visita ve datos frescos, a cambio de ejecutar la valla una y otra vez. Elegir dónde corre la valla es elegir entre frescura y coste por petición.
---
// en un proyecto server, congela ESTA ruta en el build
export const prerender = true;
const paginas = await cargarPaginasEstaticas();
---
No tienes que elegir un solo modo para todo el sitio. En un proyecto server, marca una ruta concreta con export const prerender = true y esa página se congelará en el build mientras el resto se renderiza por petición. La granularidad es por archivo, así que optimizas ruta a ruta según cada página necesite datos frescos o pueda vivir de una foto tomada una vez.
Imports y variables: un módulo de verdad
Dentro de la valla escribes JavaScript o TypeScript sin restricciones de dialecto. Importas componentes para componerlos, utilidades para reutilizar lógica y funciones del propio Astro como getCollection. Declaras variables con const y let, calculas, transformas y ordenas datos. Todo lo que sea válido en un módulo ES lo es aquí.
---
import Tarjeta from '../components/Tarjeta.astro';
import { getCollection } from 'astro:content';
import { formatearFecha } from '../lib/fechas.ts';
const posts = await getCollection('blog');
const recientes = posts
.sort((a, b) => b.data.fecha - a.data.fecha)
.slice(0, 3);
---
Fíjate en el await de la primera línea de lógica: el frontmatter admite await de nivel superior sin envolverlo en una función asíncrona. Esto es posible porque Astro trata la valla entera como un cuerpo asíncrono, y es lo que permite pedir datos de forma lineal, sin promesas anidadas ni callbacks. Lees el código de arriba abajo como una receta: trae, ordena, recorta.
Importar un módulo pesado en la valla —una librería de fechas, un cliente de base de datos— no infla el bundle del cliente. Ese import se resuelve en el build o en el servidor y solo influye en el HTML resultante. Es una diferencia crucial con un componente de framework, donde cada import puede acabar viajando al navegador. En la valla de un .astro, importar es gratis para el cliente: el coste vive del lado del servidor y desaparece antes de la respuesta.
En la valla puedes importar casi cualquier cosa: otros componentes .astro, componentes de framework, funciones utilitarias en .ts, datos en JSON, e incluso módulos de Node para leer el sistema de archivos cuando corres en el servidor. El único límite lo marca el destino de ejecución: un módulo que dependa del navegador no tiene sentido aquí, porque aquí no hay navegador.
---
import datos from '../data/menu.json';
import { calcularTotal } from '../lib/carrito.ts';
const total = calcularTotal(datos);
---
<p>Total del menu: {total}</p>
fetch en el frontmatter
Como la valla corre en el servidor, fetch es de primera clase: pides datos a una API, lees un archivo, consultas una base de datos, y el resultado se materializa en HTML antes de llegar al visitante. No hay estado de carga que gestionar en el cliente, ni parpadeo de contenido: cuando la página aparece, los datos ya están dentro.
---
const clave = import.meta.env.API_KEY;
const respuesta = await fetch('https://api.ejemplo.com/productos', {
headers: { Authorization: `Bearer ${clave}` },
});
const productos = await respuesta.json();
---
<ul>
{productos.map((p) => <li>{p.nombre}</li>)}
</ul>
Observa el uso de import.meta.env.API_KEY. Una variable de entorno sin el prefijo público solo existe en el servidor: la usas para autenticar la petición y nunca aparece en el HTML. El visitante ve la lista de productos, jamás la clave que hizo falta para obtenerla. Ese secreto vivió y murió en la valla.
Un error frecuente es asumir que una variable de la valla está disponible en el navegador. No lo está. Si escribes una etiqueta script de cliente en la plantilla, ese script corre en el navegador y no ve las variables del frontmatter salvo que se las pases explícitamente. La valla y el script de cliente viven en mundos distintos; confundirlos lleva a filtrar secretos por descuido o a buscar en vano una variable que se quedó en el servidor.
Cuando necesitas varias fuentes a la vez, el await de nivel superior se combina con las herramientas normales de concurrencia. Promise.all lanza las peticiones en paralelo y espera a todas, en lugar de encadenar esperas que sumarían sus tiempos:
---
const [usuarios, articulos] = await Promise.all([
fetch('https://api.ejemplo.com/usuarios').then((r) => r.json()),
fetch('https://api.ejemplo.com/articulos').then((r) => r.json()),
]);
---
<p>{usuarios.length} usuarios y {articulos.length} articulos</p>
Que esto ocurra en el servidor aporta una ventaja de red que se subestima: las llamadas parten desde tu infraestructura, no desde el dispositivo del visitante, que suele tener peor latencia y ancho de banda. El visitante recibe el resultado ya montado, sin sufrir la cascada de peticiones que un cliente tendría que hacer.
Si un fetch de la valla lanza una excepción durante astro build, la construcción entera falla y el error aparece en tu terminal, no en la cara del visitante. Es una ventaja: los datos rotos se detectan antes de desplegar. En modo servidor, en cambio, el fallo ocurre por petición, así que ahí conviene envolver las llamadas frágiles en try/catch y decidir un contenido de reserva.
La frontera que el código no cruza
La plantilla consume los valores de la valla, pero el código de la valla no cruza a la salida. Astro evalúa el frontmatter, retiene los valores que la plantilla necesita y descarta todo lo demás: los imports, las llamadas a la red, los bucles intermedios. Lo que el navegador descarga es el resultado, nunca la maquinaria.
Corre en el servidor
El build o el runtime ejecutan la valla. Node, no el navegador, tiene acceso al sistema de archivos y a los secretos.
Guarda los secretos
Claves y tokens sin prefijo público se quedan en la valla. Autentican peticiones sin aparecer en el HTML.
No pesa en el cliente
Los imports y el cómputo de la valla no engordan el bundle. El visitante recibe HTML, no tu lógica.
Comprobar esto es trivial y muy formativo: construye el sitio, abre el HTML generado con un editor de texto y busca en él el nombre de una variable de tu valla o la URL de tu fetch. No los encontrarás. Solo está el resultado —el texto, la lista, los números— sin ninguna huella del código que lo produjo. Esa ausencia es, literalmente, la seguridad y la ligereza de Astro hechas archivo.
La idea más liberadora de la valla es que borra la frontera artificial entre backend y componente. En la arquitectura habitual, obtener datos vive en un mundo —un endpoint, un controlador, una capa de servicios— y pintarlos vive en otro —el componente de UI—, y entre ambos media una API que tú diseñas, versionas y mantienes. Astro colapsa esa distancia: el frontmatter de un .astro es a la vez el lugar donde consultas la base de datos y el lugar donde decides cómo se verá el resultado, sin un contrato HTTP intermedio que inventar. Esto no es azúcar sintáctico; es un cambio de topología. El componente deja de ser un cliente que pide datos a un servidor lejano y pasa a ser el servidor durante el instante de su renderizado. Por eso puedes leer un secreto y usarlo en la misma valla sin exponerlo: no hay red que cruzar entre el sitio donde vive la clave y el sitio donde se pinta el HTML, porque ambos son el mismo bloque de código en la misma máquina. La consecuencia práctica es doble. De un lado, ganas seguridad casi gratis: todo lo que no marques como público es, por defecto, invisible al cliente. Del otro, ganas simplicidad: buena parte de las APIs internas que antes escribías para alimentar tu front desaparecen, porque el componente se alimenta a sí mismo. Cuando de verdad necesites un endpoint —para un cliente externo, para una isla interactiva— lo escribirás a conciencia, no por obligación arquitectónica. La valla te devuelve una pregunta que la industria casi había olvidado: si tu página se arma en el servidor, ¿para qué querías una API entre el servidor y su propia página?
- En un
.astro, usaawait fetchcontra una API pública sin clave y renderiza tres campos del JSON en la plantilla; comprueba que el HTML llega con los datos ya dentro. - Añade un
console.logen la valla, recarga la página y confirma que el mensaje aparece en la terminal del servidor, no en la consola del navegador. - Define una variable en
.envsin prefijo público, léela conimport.meta.enven la valla e inspecciona el HTML resultante para verificar que el valor no aparece. - Importa una utilidad pesada solo en la valla, construye el sitio y revisa que el bundle de cliente no crece por ese import.