Framework Web Astro
Descripción General
Astro es un framework web diseñado para sitios ricos en contenido: blogs, documentación, portafolios, sitios de marketing y comercio electrónico. Su innovación principal es la Arquitectura de Islas: por defecto, Astro envía cero JavaScript al navegador. Los componentes interactivos se hidratan selectivamente como "islas" aisladas. Astro admite React, Vue, Svelte, Solid y otros frameworks de UI simultáneamente en el mismo proyecto, permitiéndote elegir la herramienta adecuada para cada componente.
Cuándo Usar Este Skill
- Úsalo al construir un blog, sitio de documentación, página de marketing o portafolio
- Úsalo cuando el rendimiento y los Core Web Vitals sean la máxima prioridad
- Úsalo cuando el proyecto tenga mucho contenido con archivos Markdown o MDX
- Úsalo cuando quieras salida SSG (estática) con SSR opcional para rutas dinámicas
- Úsalo cuando el usuario pregunte sobre archivos
.astro,Astro.props, colecciones de contenido o directivasclient:
Cómo Funciona
Paso 1: Configuración del Proyecto
npm create astro@latest mi-sitio
cd mi-sitio
npm install
npm run dev
Añade integraciones según sea necesario:
npx astro add tailwind # Tailwind CSS
npx astro add react # Soporte para componentes React
npx astro add mdx # Soporte MDX
npx astro add sitemap # sitemap.xml automático
npx astro add vercel # Adaptador SSR para Vercel
Estructura del proyecto:
src/
pages/ ← Enrutamiento basado en archivos (.astro, .md, .mdx)
layouts/ ← Plantillas de página reutilizables
components/ ← Componentes UI (.astro, .tsx, .vue, etc.)
content/ ← Colecciones de contenido con tipos seguros (Markdown/MDX)
styles/ ← CSS global
public/ ← Activos estáticos (se copian tal cual)
astro.config.mjs ← Configuración del framework
Paso 2: Sintaxis de Componente Astro
Los archivos .astro tienen una sección de frontmatter al inicio (solo servidor) y una plantilla debajo:
---
// src/components/Card.astro
// Este bloque se ejecuta SOLO en el servidor — nunca en el navegador
interface Props {
title: string;
href: string;
description: string;
}
const { title, href, description } = Astro.props;
---
<article class="card">
<h2><a href={href}>{title}</a></h2>
<p>{description}</p>
</article>
<style>
/* Se aplica a este componente automáticamente */
.card { border: 1px solid #eee; padding: 1rem; }
</style>
Paso 3: Páginas y Enrutamiento Basado en Archivos
src/pages/index.astro → /
src/pages/about.astro → /about
src/pages/blog/[slug].astro → /blog/:slug (dinámico)
src/pages/blog/[...path].astro → /blog/* (comodín)
Ruta dinámica con getStaticPaths:
---
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<h1>{post.data.title}</h1>
<Content />
Paso 4: Colecciones de Contenido
Las colecciones de contenido te dan acceso con tipos seguros a archivos Markdown y MDX:
// src/content/config.ts
import { z, defineCollection } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog'))
.filter(p => !p.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
<time>{post.data.date.toLocaleDateString()}</time>
</li>
))}
</ul>
Paso 5: Islas — Hidratación Selectiva
Por defecto, los componentes de frameworks UI se renderizan como HTML estático sin JS. Usa directivas client: para hidratar:
---
import Counter from '../components/Counter.tsx'; // Componente React
import VideoPlayer from '../components/VideoPlayer.svelte';
---
<!-- HTML estático — no se envía JavaScript al navegador -->
<Counter initialCount={0} />
<!-- Hidrata inmediatamente al cargar la página -->
<Counter initialCount={0} client:load />
<!-- Hidrata cuando el componente se desplaza a la vista -->
<VideoPlayer src="/demo.mp4" client:visible />
<!-- Hidrata solo cuando el navegador está inactivo -->
<Analytics client:idle />
<!-- Hidrata solo en una consulta de medios específica -->
<MobileMenu client:media="(max-width: 768px)" />
Paso 6: Layouts
---
// src/layouts/BaseLayout.astro
interface Props {
title: string;
description?: string;
}
const { title, description = 'Mi Sitio Astro' } = Astro.props;
---
<html lang="es">
<head>
<meta charset="utf-8" />
<title>{title}</title>
<meta name="description" content={description} />
</head>
<body>
<nav>...</nav>
<main>
<slot /> <!-- el contenido de la página se renderiza aquí -->
</main>
<footer>...</footer>
</body>
</html>
---
// src/pages/about.astro
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Sobre Nosotros">
<h1>Sobre Nosotros</h1>
<p>Bienvenido a nuestra empresa...</p>
</BaseLayout>
Paso 7: Modo SSR (Renderizado Bajo Demanda)
Activa SSR para páginas dinámicas configurando un adaptador:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'hybrid', // 'static' | 'server' | 'hybrid'
adapter: vercel(),
});
Opta páginas individuales en SSR con export const prerender = false.
Ejemplos
Ejemplo 1: Blog con Feed RSS
// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: 'Mi Blog',
description: 'Últimas publicaciones',
site: context.site,
items: posts.map(post => ({
title: post.data.title,
pubDate: post.data.date,
link: `/blog/${post.slug}/`,
})),
});
}
Ejemplo 2: Endpoint API (SSR)
// src/pages/api/subscribe.ts
import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ request }) => {
const { email } = await request.json();
if (!email) {
return new Response(JSON.stringify({ error: 'Email requerido' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
await addToNewsletter(email);
return new Response(JSON.stringify({ success: true }), { status: 200 });
};
Ejemplo 3: Componente React como Isla
// src/components/SearchBox.tsx
import { useState } from 'react';
export default function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
async function search(e: React.FormEvent) {
e.preventDefault();
const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
setResults(data);
}
return (
<form onSubmit={search}>
<input value={query} onChange={e => setQuery(e.target.value)} />
<button type="submit">Buscar</button>
<ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
</form>
);
}
---
import SearchBox from '../components/SearchBox.tsx';
---
<!-- Hidratada inmediatamente — esta isla es interactiva -->
<SearchBox client:load />
Mejores Prácticas
- ✅ Mantén la mayoría de los componentes como archivos
.astroestáticos — solo hidrata lo que debe ser interactivo - ✅ Usa colecciones de contenido para todo el contenido Markdown/MDX — obtienes tipos seguros y validación automática
- ✅ Prefiere
client:visiblesobreclient:loadpara componentes que están debajo del pliegue para reducir el JS inicial - ✅ Usa
import.meta.envpara variables de entorno — prefija las variables públicas conPUBLIC_ - ✅ Añade
<ViewTransitions />deastro:transitionspara una navegación fluida entre páginas sin un SPA completo - ❌ No uses
client:loaden todos los componentes — esto anula la ventaja de rendimiento de Astro - ❌ No pongas secretos en el frontmatter de
.astroque se use en plantillas visibles al cliente - ❌ No omitas
getStaticPathspara rutas dinámicas en modo estático — las compilaciones fallarán
Notas de Seguridad
- El código del frontmatter en archivos
.astrose ejecuta solo del lado del servidor y nunca se expone al navegador. - Usa
import.meta.env.PUBLIC_*solo para valores no sensibles. Las variables de entorno privadas (sin prefijoPUBLIC_) nunca se envían al cliente. - En modo SSR, valida todas las entradas de
Astro.requestantes de consultas a la base de datos o llamadas API. - Desinfecta cualquier contenido proporcionado por el usuario antes de renderizar con
set:html— esto evita el escape automático.
Errores Comunes
-
Problema: El JavaScript de un componente React/Vue no se ejecuta en el navegador Solución: Añade una directiva
client:(client:load,client:visible, etc.) — sin ella, los componentes se renderizan solo como HTML estático. -
Problema: Los datos de
getStaticPathsestán desactualizados después de actualizaciones de contenido durante el desarrollo Solución: El servidor de desarrollo de Astro observa los archivos de contenido — reinicia si los cambios encontent/config.tsno se reflejan. -
Problema: El tipo de
Astro.propsesany— sin autocompletado Solución: Define una interfaz o tipoPropsen el frontmatter y Astro lo inferirá automáticamente. -
Problema: El CSS de un componente
.astrose filtra a otros componentes Solución: Los estilos en las etiquetas<style>de.astrotienen ámbito automáticamente. Usa:global()solo cuando quieras apuntar intencionadamente a elementos hijos.
Habilidades Relacionadas
@sveltekit— Cuando necesitas un framework full-stack con UI reactiva (frente al enfoque de contenido de Astro)@nextjs-app-router-patterns— Cuando necesitas un framework full-stack centrado en React@tailwind-patterns— Dar estilo a sitios Astro con Tailwind CSS@progressive-web-app— Añadir capacidades PWA a un sitio Astro
Limitaciones
- Usa este skill solo cuando la tarea coincida claramente con el alcance descrito anteriormente.
- No trates la salida como un sustituto de la validación específica del entorno, pruebas o revisión de expertos.
- Detente y pide aclaraciones si faltan entradas requeridas, permisos, límites de seguridad o criterios de éxito.