Guía de Astro: De 0 a 100
¿Qué es Astro?
Astro es un framework web moderno para construir sitios web rápidos, lanzado como versión 1.0 en 2022 y creado por Fred Schott (ex-Google, creador de Snowpack). Su filosofía es content-driven: prioriza el contenido sobre la interactividad, enviando cero JavaScript al navegador por defecto.
Arquitectura:
- Zero JS by default: Astro renderiza HTML en el servidor (build time o SSR) y elimina todo el JavaScript que no sea necesario
- Islands Architecture: solo los componentes interactivos (islas) envían JavaScript al cliente
- MPA (Multi-Page Application): cada página es un HTML completo. Con ViewTransitions puedes conseguir navegación SPA-like
- Framework-agnóstico: puedes usar componentes de React, Svelte, Vue, Solid, Preact, Lit en el mismo proyecto
¿Dónde se usa?
- Blogs y documentación: el caso de uso original. Ej: docs.astro.build
- E-commerce: tiendas con SSR + BD, carritos interactivos con islas
- Dashboards: paneles de administración con server islands
- Portfolios y landing pages: sitios estáticos ultrarrápidos
- Sitios multilingüe: i18n routing nativo desde Astro 4
¿Quién lo usa? Trivius, Wayfair, Kontent.ai, Google (proyectos internos).
🔗 Astro docs — Concepts, Astro blog — 1.0 announcement
Prerrequisitos
Antes de empezar, necesitas:
- Conocimientos básicos de terminal: navegar directorios, ejecutar comandos, usar un editor de texto
- El lenguaje/herramienta instalado en tu sistema (sigue la sección de Instalación si aún no lo tienes)
Si no cumples algún requisito, no te preocupes: cada sección te guiará paso a paso.
¿Cómo empezar? Instalación en todos los entornos
Requisitos
- Node.js 18+ (recomendado Node.js 22 LTS o 20 LTS)
- Editor: VSCode con extensión oficial Astro
Crear proyecto
npm create astro@latest
pnpm create astro@latest
bun create astro@latest
Docker
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
FROM nginx:alpine AS runner
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Alias útiles
alias adev='npm run dev'
alias abuild='npm run build'
alias apreview='npm run preview'
alias aadd='npx astro add'
alias acheck='npx astro check'
🔗 Astro docs — Installation, Docker Hub — Node
Escala de aprendizaje 0–100
Nivel 0–15: Fundamentos absolutos
Qué aprender:
- ¿Qué es Astro? Zero JS vs SPA. Diferencia con Next.js, Nuxt
npm create astro@latesty estructura del proyecto- Archivos
.astro: frontmatter, template HTML, expresiones{} - Páginas en
src/pages/, rutas basadas en archivos - Layouts simples con
<slot /> npm run dev,npm run build,npm run preview- Conceptos clave: Astro es declarativo, MPA, islands architecture
Proyecto: Página personal estática — 3 páginas (inicio, sobre mí, contacto) con layouts compartidos. Sin JS en cliente.
Nivel 15–30: Colecciones de contenido y MDX
Qué aprender:
src/content/ydefineCollectioncon ZodgetCollection(),getEntry(), filtros, ordenación- Archivos MDX con componentes
remark/rehypeplugins
Proyecto: Blog personal — Colección MDX con schema Zod. Página de listado ordenado. Tags. RSS feed.
Nivel 30–45: Islas, componentes y Tailwind
Qué aprender:
- Directivas
client:load,client:idle,client:visible - Integrar Svelte/React/Vue
- Tailwind CSS, estilos scoped
- Slots y named slots
Proyecto: Portfolio con galería — Componente interactivo Svelte. Modo oscuro. Layout responsivo.
Nivel 45–60: SSR, endpoints API, autenticación
Qué aprender:
- SSR vs SSG vs Hybrid
- Adaptadores: Vercel, Netlify, Node
- Endpoints API, middleware, cookies
- Paginación con
paginate(), búsqueda con Pagefind
Proyecto: Gestor de tareas con login — SSR, cookies, CRUD con endpoints API.
Nivel 60–75: Server Islands, ViewTransitions, i18n
Qué aprender:
- Server Islands
server:defer - ViewTransitions con morphing
- i18n routing multilingüe
- Astro DB, optimización de imágenes
Proyecto: E-commerce básico — Catálogo, server island para precio, carrito Svelte, ViewTransitions, i18n.
Nivel 75–90: Actions, BD, CMS, Stripe
Qué aprender:
- Astro Actions con validación Zod
- Turso/libSQL remoto, integración CMS
- Stripe Checkout, webhooks
- Testing con Playwright
Proyecto: Tienda con pagos — SSR, Stripe, webhooks, Astro Actions, tests E2E.
Nivel 90–100: Edge, arquitectura, monorepo
Qué aprender:
- Edge Functions, streaming SSR
- Monorepo con Turborepo
- Caché CDN, ISR, stale-while-revalidate
- Performance, Core Web Vitals, observabilidad
Proyecto: Sitio multilingüe completo — 5 idiomas, edge functions, Stripe, búsqueda, monorepo, ISR.
Primeros pasos y configuración del entorno
Estructura del proyecto (Astro 6)
mi-proyecto/
├── src/
│ ├── pages/ # Rutas basadas en archivos
│ ├── components/ # Componentes .astro, .svelte, .jsx
│ ├── layouts/ # Layouts compartidos
│ ├── content/ # Colecciones de contenido (MDX, JSON, etc.)
│ │ ├── posts/ # Contenido plano (sin config.ts aquí)
│ │ └── config.ts # ⚠️ Astro 5: defineCollection con type+loader
│ ├── content.config.ts # ✅ Astro 6: config en raíz de src/ (loader API nativa)
│ ├── styles/ # CSS global
│ └── assets/ # Imágenes que pasan por Vite (hash, optimización)
├── public/ # Archivos estáticos (se sirven tal cual, sin procesar)
├── astro.config.mjs # Configuración principal
└── package.json
Nota sobre versiones: En Astro 5 la config de colecciones iba en
src/content/config.ts. Desde Astro 6 se migró asrc/content.config.tsusando la API deloaders(comoglob()) directamente, sintype: 'content'. El resto de la estructura (pages, components, layouts) se mantiene igual entre versiones.
Servidor de desarrollo
npm run dev # http://localhost:4321
npm run build # Genera HTML estático en dist/
npm run preview # Previsualiza la build localmente
Tu primera página
---
const titulo = "Hola Mundo";
const items = ["Astro", "Svelte", "Tailwind"];
---
<html>
<head><title>{titulo}</title></head>
<body>
<h1>{titulo}</h1>
<ul>
{items.map(item => <li>{item}</li>)}
</ul>
</body>
</html>
Layout básico
---
export interface Props { title: string; }
const { title } = Astro.props;
---
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
Colecciones de contenido básico
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const posts = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
tags: z.array(z.string()),
draft: z.boolean().default(false),
}),
});
export const collections = { posts };
---
import { getCollection } from 'astro:content';
const posts = await getCollection('posts');
---
{posts.map(post => <a href={`/posts/${post.id}`}>{post.data.title}</a>)}
🧪 Mini-script: verificar instalación
#!/bin/bash
echo "=== Verificar Astro ==="
node --version && echo "✅ Node OK" || echo "❌ Node"
npm --version && echo "✅ npm OK" || echo "❌ npm"
npm create astro@latest -- --template basics test-astro 2>&1 | tail -3
cd test-astro && npm run build 2>&1 | tail -3 && echo "✅ Astro build OK" || echo "❌ Build falló"
cd .. && rm -rf test-astro
🔗 Astro docs — Project Structure, Astro docs — Pages
Paradigmas
Declarativo
Los componentes .astro siguen un estilo declarativo: describes qué renderizar, no cómo.
---
const items = await getCollection('posts');
const filtrados = items.filter(p => !p.data.draft);
---
<ul>
{filtrados.map(item => <li>{item.data.title}</li>)}
</ul>
MPA (Multi-Page Application)
Astro genera páginas HTML completas por cada ruta. No hay router del lado del cliente.
| Característica | Astro (MPA) | Next.js/Nuxt (SPA/SSR) |
|---|---|---|
| HTML por ruta | Completo | Shell + JS |
| JS por defecto | Ninguno | Bundle de cliente |
| Router | Servidor (URL → archivo) | Cliente (history API) |
Islands Architecture
Solo los componentes interactivos envían JavaScript al cliente. El resto es HTML/CSS estático.
┌──────────────────────────────┐
│ HTML estático │
│ ┌──────────────────────┐ │
│ │ Isla interactiva │ │
│ └──────────────────────┘ │
│ HTML estático │
└──────────────────────────────┘
Compilación estática + SSR opcional
Por defecto, Astro genera HTML estático. Puedes activar SSR con output: 'server' o output: 'hybrid'.
Component-based (no POO)
Astro usa componentes (.astro, .svelte, .jsx, .vue) que reciben props y renderizan HTML.
Multi-framework
npx astro add react svelte vue solid lit
---
import Mapa from '../components/Mapa.jsx';
import Contador from '../components/Contador.svelte';
---
<Mapa client:only="react" />
<Contador client:load />
🔗 Astro docs — Components, Astro docs — Islands
Sistema de archivos: contenido y activos
¿Qué es?
Astro es un framework web — el sistema de archivos se usa durante el build o SSR, no en el cliente. Los archivos se organizan en colecciones, activos estáticos y rutas.
Content Collections
Configuración con loaders (Astro 3+):
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const posts = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/posts' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
tags: z.array(z.string()),
draft: z.boolean().default(false),
}),
});
export const collections = { posts };
Consultas:
---
import { getCollection, getEntry } from 'astro:content';
const allPosts = await getCollection('posts');
const publishedPosts = await getCollection('posts', ({ data }) => !data.draft);
const sortedPosts = (await getCollection('posts'))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
const post = await getEntry('posts', 'mi-articulo');
const { Content, headings } = await post.render();
---
<article><Content /></article>
Astro.glob() — lectura de archivos en build
---
const posts = await Astro.glob('../content/posts/*.mdx');
const publicados = posts.filter(p => !p.frontmatter.draft);
---
<ul>
{publicados.map(post => (
<li><a href={post.url}>{post.frontmatter.title}</a></li>
))}
</ul>
import.meta.glob (Vite)
---
const modulos = import.meta.glob('../data/*.json', { eager: true });
const lazyModulos = import.meta.glob('../data/*.json');
---
Archivos estáticos: public/ y src/assets
public/: se sirven tal cual (favicon, robots.txt). No pasan por Vite.src/assets/: pasan por Vite (hash, optimización, importación como módulo).
---
import logo from '../assets/logo.svg';
import foto from '../assets/foto.jpg';
---
<img src={logo} alt="Logo" />
<img src="/favicon.ico" alt="" />
<img src={foto} alt="Foto" />
Endpoints API
// src/pages/api/productos.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ params, request, locals }) => {
const productos = await locals.db.findAll();
return new Response(JSON.stringify(productos), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
🧪 Mini-script: verificar estructura de archivos
---
import { getCollection } from 'astro:content';
import { glob } from 'astro/loaders';
const posts = await getCollection('posts');
const archivosMD = await Astro.glob('../content/posts/*.{md,mdx}');
---
<p>Total colecciones: {posts.length}</p>
<p>Total archivos glob: {archivosMD.length}</p>
<ul>
{posts.map(p => <li>{p.id} — {p.data.title}</li>)}
</ul>
🔗 Astro docs — Astro.glob, Astro docs — Content Collections, Astro docs — Endpoints
Conceptos clave explicados a fondo
Islas (Islands Architecture)
¿Qué es? Componentes interactivos en medio de HTML estático. Astro renderiza el HTML en el servidor y solo hidrata los componentes que lo necesitan.
Directivas de hidratación:
<Contador client:load /> <!-- Inmediato -->
<MenuDesplegable client:idle /> <!-- Cuando el navegador está inactivo -->
<GaleriaImagenes client:visible /> <!-- Cuando es visible en viewport -->
<SidebarComplejo client:media="(min-width: 768px)" />
<BotonEstatico /> <!-- Sin hidratación. Solo HTML y CSS -->
<MapaInteractivo client:only="react" /> <!-- Solo cliente, sin SSR -->
| Directiva | Cuándo usarla | Ejemplo |
|---|---|---|
client:load | Debe estar disponible de inmediato | Navbar, header |
client:idle | No es urgente, puede esperar | Chat widget, notificaciones |
client:visible | Está abajo en la página | Galería, comentarios |
client:media | Solo en ciertos tamaños | Sidebar desktop, menú mobile |
client:only | Usa APIs del navegador | Mapas, canvas, WebGL |
Server Islands (Astro 4.5+)
---
import PrecioProducto from '../components/PrecioProducto.astro';
---
<main>
<h1>Producto</h1>
<PrecioProducto server:defer>
<p slot="fallback">Cargando precio...</p>
</PrecioProducto>
</main>
Archivos .astro
Estructura:
---
// Frontmatter: código servidor
import Base from '../layouts/Base.astro';
const titulo = "Página de ejemplo";
---
<!-- Template HTML -->
<Base title={titulo}>
<main>
<h1>{titulo}</h1>
<slot />
</main>
</Base>
<style>
/* Estilos scoped por defecto */
h1 { font-size: 2rem; }
</style>
<script>
// Código del lado del cliente
document.addEventListener('DOMContentLoaded', () => {
console.log('Página cargada');
});
</script>
Integraciones
Frameworks:
npx astro add react svelte vue solid preact lit
CSS: npx astro add tailwind
Adaptadores: npx astro add vercel netlify cloudflare node deno
Utilidades: npx astro add mdx sitemap partytown prefetch
Ejemplo config:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import svelte from '@astrojs/svelte';
import tailwind from '@astrojs/tailwind';
import vercel from '@astrojs/vercel/serverless';
import mdx from '@astrojs/mdx';
export default defineConfig({
integrations: [svelte(), tailwind(), mdx()],
output: 'server',
adapter: vercel(),
});
Output: static, server, hybrid
Astro ofrece 3 modos de renderizado que determinan cuándo y dónde se genera el HTML. La elección depende del tipo de contenido, la necesidad de datos dinámicos y el adaptador de despliegue.
output: 'static' — HTML en build time
Cómo funciona: Todo el HTML se genera durante astro build. No hay servidor. Los archivos son estáticos y se sirven desde CDN.
Cuándo usarlo:
- Blogs, documentación, portfolios, landing pages
- Sitios sin contenido dinámico por usuario
- Máximo rendimiento (cero latency en el servidor)
Ejemplo:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({ output: 'static' });
---
// src/pages/index.astro — esto se evalúa UNA VEZ en build
const posts = await getCollection('posts');
---
<h1>Blog ({posts.length} artículos)</h1>
Ventajas: Velocidad máxima, se puede servir desde CDN (Netlify, Vercel Static, S3, GitHub Pages), cero coste de servidor.
output: 'server' — SSR (Server-Side Rendering)
Cómo funciona: Las páginas se renderizan en cada petición HTTP. Necesita un servidor Node.js (o edge function). El usuario ve contenido actualizado siempre.
Cuándo usarlo:
- Paneles de administración, dashboards
- E-commerce con precios/stock dinámicos
- Aplicaciones con autenticación y sesiones
- APIs y endpoints con datos de BD
Ejemplo:
// astro.config.mjs
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'server',
adapter: vercel(),
});
---
// src/pages/perfil/[id].astro — esto se evalúa en CADA petición
const { id } = Astro.params;
const usuario = await db.getUser(id); // Consulta a BD en cada request
if (!usuario) return Astro.redirect('/404');
const { nombre, email, avatar } = usuario;
---
<h1>{nombre}</h1>
<p>{email}</p>
<img src={avatar} alt={nombre} />
Ventajas: Contenido siempre actualizado, autenticación real, acceso a BD/APIs.
Desventajas: Mayor latencia (cada request ejecuta el servidor), coste de cómputo, necesita adaptador (Vercel, Netlify, Node, Deno, Cloudflare).
Adaptadores disponibles:
npx astro add vercel # Vercel Serverless/Edge Functions
npx astro add netlify # Netlify Functions
npx astro add node # Servidor Node.js propio
npx astro add deno # Deno Deploy
npx astro add cloudflare # Cloudflare Pages/Workers
output: 'hybrid' — SSG + SSR combinados
Cómo funciona: Por defecto, todas las páginas son estáticas (SSG), pero puedes marcar páginas individuales como SSR con export const prerender = false. Es lo mejor de ambos mundos.
Cuándo usarlo:
- Blog con páginas estáticas + carrito SSR
- Sitio informativo + panel de admin dinámico
- La mayoría de proyectos reales: contenido estático + funcionalidades dinámicas
Ejemplo:
// astro.config.mjs
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'hybrid',
adapter: vercel(),
});
---
// src/pages/productos/[id].astro — página SSR (se renderiza en cada petición)
export const prerender = false;
const { id } = Astro.params;
const producto = await db.getProducto(id);
---
<h1>{producto.nombre}</h1>
<p>Precio: {producto.precio}€</p>
---
// src/pages/index.astro — página SSG (se genera en build)
const posts = await getCollection('posts');
---
<h1>Blog</h1>
<!-- Estático, se sirve desde CDN -->
Export prerender — control página por página:
| Valor | Efecto |
|---|---|
export const prerender = true; | Fuerza SSG aunque output sea server |
export const prerender = false; | Fuerza SSR aunque output sea hybrid o static |
| Sin export | Sigue el modo global (static, server, hybrid) |
Comparativa rápida
| Aspecto | static | server | hybrid |
|---|---|---|---|
| HTML generado | Build time | Cada petición | Mixto |
| Velocidad | Máxima | Variable | Según página |
| Contenido dinámico | ❌ No | ✅ Sí | ✅ Según página |
| BD / APIs | ❌ Sin servidor | ✅ Sí | ✅ Según página |
| Autenticación | ❌ No | ✅ Sí | ✅ Según página |
| Coste servidor | Ninguno | Sí | Parcial |
| CDN | ✅ Total | Parcial | ✅ Mayoría |
| Adaptador necesario | ❌ No | ✅ Sí | ✅ Sí |
| Ideal para | Blogs, docs | Dashboards, APIs | Sitios mixtos |
ViewTransitions (navegación SPA-like)
Configuración básica:
---
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<ViewTransitions />
</head>
<body><slot /></body>
</html>
Transiciones entre páginas:
<a href="/blog/post-2" transition:animate="slide">Siguiente artículo</a>
<h1 transition:animate="slide">Título del artículo</h1>
Animaciones: morph, slide, fade, initial, only, personalizadas con transition:name
Imágenes: astro:assets
Image component:
---
import { Image } from 'astro:assets';
import miImagen from '../images/foto.jpg';
---
<Image src={miImagen} alt="Descripción" width={800} height={600} format="webp" quality={80} />
Picture component:
---
import { Picture } from 'astro:assets';
import heroDesktop from '../images/hero-desktop.jpg';
---
<Picture src={heroDesktop} alt="Hero" widths={[400,800,1200]} formats={['avif','webp','jpg']} />
i18n routing (Astro 4+)
export default defineConfig({
i18n: {
defaultLocale: 'es',
locales: ['es', 'en', 'ca'],
routing: { prefixDefaultLocale: false, strategy: 'prefix' },
},
});
---
const { currentLocale } = Astro;
---
<p>Idioma actual: {currentLocale}</p>
Markdown/MDX y remark/rehype plugins
// astro.config.mjs
import mdx from '@astrojs/mdx';
import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
export default defineConfig({
integrations: [mdx({
remarkPlugins: [remarkToc],
rehypePlugins: [rehypeSlug],
gfm: true,
})],
});
Componentes, composición y slots
Layouts anidados:
<!-- src/layouts/Base.astro -->
<!doctype html>
<html>
<head><slot name="head" /></head>
<body>
<nav>...</nav>
<slot />
<footer>...</footer>
</body>
</html>
<!-- src/layouts/Blog.astro -->
import Base from './Base.astro';
<Base>
<article class="prose"><slot /></article>
</Base>
Slots y named slots:
---
// Card.astro
export interface Props { titulo: string; }
const { titulo } = Astro.props;
---
<div class="card">
<slot name="icon" />
<h2>{titulo}</h2>
<slot />
<slot name="footer"><p>Por defecto</p></slot>
</div>
Patrones de diseño (reinterpretación de POO/patrones):
- Layout Pattern: herencia de layouts anidados
- Island Pattern: componente interactivo mínimo
- Composition Pattern: slots y named slots para composición
- Data Fetching Pattern:
Astro.glob()para archivos locales,fetchpara APIs - Route Patterns: parámetros dinámicos
[slug], grupos(app), catch-all[...slug]
Polimorfismo por props:
---
// Boton.astro
export interface Props {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
href?: string;
}
const { variant = 'primary', size = 'md', href } = Astro.props;
---
{href ? <a href={href} class={`btn btn-${variant} btn-${size}`}><slot /></a>
: <button class={`btn btn-${variant} btn-${size}`}><slot /></button>}
Middleware y cookies
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
context.locals.user = await getUserFromCookie(context.cookies);
const response = await next();
return response;
});
Astro Actions (Astro 5+)
// src/actions/index.ts
import { defineAction, z } from 'astro:actions';
export const server = {
likePost: defineAction({
accept: 'form',
input: z.object({ postId: z.string(), action: z.enum(['like', 'unlike']) }),
handler: async ({ postId, action }, context) => {
return await db.updateLikes(postId, action);
},
}),
};
🧪 Mini-script: verificar conceptos clave
#!/bin/bash
echo "=== Verificar Astro ==="
npx astro --version 2>/dev/null && echo "✅ Astro CLI OK" || echo "❌ Astro CLI"
echo "=== Componentes .astro ==="
find src -name "*.astro" 2>/dev/null | head -5
echo "=== Colecciones ==="
ls src/content/ 2>/dev/null
echo "=== Config ==="
cat astro.config.mjs 2>/dev/null | head -10
🔗 Astro docs — Islands, Astro docs — View Transitions, Astro docs — SSR, Astro docs — i18n, Astro docs — Actions
Testing y calidad
Frameworks y herramientas
| Framework | Propósito | Async | CLI |
|---|---|---|---|
| Playwright | E2E testing | Sí | npx playwright test |
| Vitest | Unitario/Integración | Sí | npx vitest |
| Astro check | Type checking | No | npx astro check |
| Pagefind | Búsqueda offline | Sí | npx pagefind --site dist |
Cómo testear cada concepto
Islas y componentes:
// tests/island.test.ts
import { test, expect } from 'vitest';
import { mount } from '@astro/test-utils';
test('componente se renderiza correctamente', async () => {
const html = await mount('./src/components/Card.astro', {
props: { titulo: 'Test' },
slots: { default: '<p>Contenido</p>' },
});
expect(html).toContain('Test');
expect(html).toContain('Contenido');
});
Content Collections:
// tests/collections.test.ts
import { test, expect } from 'vitest';
test('posts tienen frontmatter válido', async () => {
const posts = await getCollection('posts');
for (const post of posts) {
expect(post.data.title).toBeTruthy();
expect(post.data.pubDate).toBeInstanceOf(Date);
expect(Array.isArray(post.data.tags)).toBe(true);
}
});
E2E con Playwright:
// tests/e2e/navigation.spec.ts
import { test, expect } from '@playwright/test';
test('navegación entre páginas', async ({ page }) => {
await page.goto('http://localhost:4321');
await page.click('a[href="/blog"]');
await expect(page.locator('h1')).toContainText('Blog');
await page.click('a:first-of-type');
await expect(page.locator('article')).toBeVisible();
});
test('vista previa de post en listado', async ({ page }) => {
await page.goto('http://localhost:4321/blog');
const articles = page.locator('article');
const count = await articles.count();
expect(count).toBeGreaterThan(0);
for (let i = 0; i < count; i++) {
await expect(articles.nth(i).locator('h2')).toBeVisible();
}
});
Integración continua:
# .github/workflows/test.yml
name: Astro Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install
- run: pnpm run build
- run: npx astro check
- run: npx playwright test
🧪 Mini-scripts de verificación
Mini-script 1: Verificar build
#!/bin/bash
echo "=== Verificar build ==="
npm run build 2>&1 | tail -5
if [ -d "dist" ]; then
echo "✅ Build generado en dist/"
du -sh dist/
else
echo "❌ Build falló"
exit 1
fi
Mini-script 2: Verificar colecciones
---
import { getCollection } from 'astro:content';
const posts = await getCollection('posts');
const errors: string[] = [];
for (const post of posts) {
if (!post.data.title) errors.push(`${post.id}: sin título`);
if (!post.data.pubDate) errors.push(`${post.id}: sin fecha`);
}
---
{errors.length === 0 ? <p>✅ Todas las colecciones válidas</p>
: <ul>{errors.map(e => <li>{e}</li>)}</ul>}
<p>Total: {posts.length} posts</p>
🔗 Playwright docs, Vitest docs, Astro check CLI
Conceptos avanzados
View Transitions avanzadas
---
import { ViewTransitions } from 'astro:transitions';
---
<ViewTransitions />
<!-- Morphing de elementos entre páginas -->
<img src="foto.jpg" transition:name="hero" />
<!-- En otra página -->
<img src="foto-grande.jpg" transition:name="hero" />
Server Islands anidadas
---
import PanelUsuario from '../components/PanelUsuario.astro';
import ActividadReciente from '../components/ActividadReciente.astro';
---
<PanelUsuario server:defer>
<p slot="fallback">Cargando panel...</p>
<ActividadReciente server:defer>
<p slot="fallback">Cargando actividad...</p>
</ActividadReciente>
</PanelUsuario>
Content Collections avanzado
Loaders personalizados:
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const posts = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/posts' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
tags: z.array(z.string()),
draft: z.boolean().default(false),
image: "img/guia_0_100_astro/guia_0_100_astro_cover-1200.webp"
author: z.string().default('Jorge Beneyto Castelló'),
}),
});
Consultas avanzadas:
---
import { getCollection, getEntry } from 'astro:content';
const postsPorTag = (await getCollection('posts'))
.filter(p => p.data.tags?.includes('astro'));
const postsPorAnio = (await getCollection('posts')).reduce((acc, post) => {
const anio = post.data.pubDate.getFullYear();
if (!acc[anio]) acc[anio] = [];
acc[anio].push(post);
return acc;
}, {} as Record<number, typeof posts>);
---
i18n avanzado
// astro.config.mjs
export default defineConfig({
i18n: {
defaultLocale: 'es',
locales: ['es', 'en', 'ca', 'de', 'fr'],
routing: { prefixDefaultLocale: false, strategy: 'prefix' },
},
});
Selector de idioma:
---
const { currentLocale } = Astro;
const locales = [
{ code: 'es', label: 'Español' },
{ code: 'en', label: 'English' },
{ code: 'ca', label: 'Català' },
];
---
<nav>
{locales.map(locale => (
<a href={Astro.url.pathname.replace(`/${currentLocale}`, `/${locale.code}`)}
class={locale.code === currentLocale ? 'active' : ''}>
{locale.label}
</a>
))}
</nav>
Edge Functions, ISR y rendimiento
// astro.config.mjs
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'hybrid',
adapter: vercel({
isr: { expiration: 60, exclude: ['/admin/**'] },
edgeMiddleware: true,
}),
build: { inlineStylesheets: 'auto' },
});
RSS, sitemap, redirects
---
// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export const GET = async () => {
const posts = (await getCollection('posts'))
.filter(p => !p.data.draft)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
return rss({
title: 'Mi Blog',
description: 'Descripción del feed',
site: 'https://mi-sitio.com',
items: posts.map(post => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/posts/${post.id}/`,
})),
});
};
🧪 Mini-script: probar ViewTransitions
---
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head><ViewTransitions /></head>
<body>
<h1 transition:animate="slide">Inicio</h1>
<nav>
<a href="/about" transition:animate="morph">Sobre mí</a>
<a href="/blog" transition:animate="slide">Blog</a>
</nav>
<slot />
</body>
</html>
🔗 Astro docs — View Transitions, Astro docs — Server Islands, Astro docs — i18n, Astro docs — RSS
Proyecto final integrador
Blog personal con Astro
Proyecto que combina: file system (content collections), conceptos (islas, SSR, views), testing, y red (RSS, deploy).
Estructura del proyecto:
astro-blog/
├── src/
│ ├── content/
│ │ ├── config.ts
│ │ └── posts/
│ │ ├── post-1.mdx
│ │ └── post-2.mdx
│ ├── components/
│ │ ├── Card.astro
│ │ ├── YouTube.astro
│ │ └── Contador.svelte
│ ├── layouts/
│ │ ├── Base.astro
│ │ └── BlogPost.astro
│ └── pages/
│ ├── index.astro
│ ├── blog/
│ │ ├── [...page].astro
│ │ └── [slug].astro
│ └── rss.xml.ts
├── astro.config.mjs
└── package.json
Content Collections:
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const posts = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/posts' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
tags: z.array(z.string()),
image: z.string().optional(),
author: z.string().default('Jorge Beneyto'),
}),
});
export const collections = { posts };
Listado con paginación:
---
import { getCollection } from 'astro:content';
import Base from '../../layouts/Base.astro';
export async function getStaticPaths({ paginate }) {
const posts = (await getCollection('posts'))
.filter(p => !p.data.draft)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
return paginate(posts, { pageSize: 10 });
}
const { page } = Astro.props;
---
<Base title="Blog">
{page.data.map(post => (
<article>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString('es')}
</time>
<h2><a href={`/blog/${post.id}/`}>{post.data.title}</a></h2>
<p>{post.data.description}</p>
<div class="tags">
{post.data.tags.map(tag => <span class="tag">{tag}</span>)}
</div>
</article>
))}
<nav>
{page.url.prev && <a href={page.url.prev}>← Anterior</a>}
{page.url.next && <a href={page.url.next}>Siguiente →</a>}
</nav>
</Base>
Página de detalle:
---
import { getCollection } from 'astro:content';
import Base from '../../layouts/Base.astro';
import BlogPost from '../../layouts/BlogPost.astro';
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts.map(post => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<Base title={post.data.title}>
<BlogPost title={post.data.title} author={post.data.author} date={post.data.pubDate}>
<Content />
</BlogPost>
</Base>
Tests:
#!/bin/bash
# test_proyecto.sh
echo "=== Test 1: Build ==="
npm run build || exit 1
echo "=== Test 2: RSS existe ==="
test -f dist/rss.xml && echo "✅ RSS generado" || echo "⚠️ Sin RSS"
echo "=== Test 3: Páginas generadas ==="
find dist -name "*.html" | wc -l
echo "páginas HTML generadas"
echo "=== Test 4: Type check ==="
npx astro check 2>&1 | grep -q "No errors" && echo "✅ Types OK" || echo "⚠️ Revisar types"
echo "=== Todos los tests pasaron ==="
CI/CD:
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
paths: ['src/content/**', 'src/**']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install
- run: pnpm run build
- run: npx astro check
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
Canales y recursos en español
YouTube
- Midulive — Astro, JavaScript, desarrollo web
- MoureDev — Astro, proyectos web, automatización
- HolaMundo — Tutoriales rápidos de tecnologías web
- Carlos Azaustre — Desarrollo web con Astro, React
- Código 369 — Astro, Jamstack, frontend moderno
Comunidades
- r/astrojs (Reddit) — Comunidad oficial
- Astro Discord (astro.build/chat)
- Stack Overflow en español — Etiqueta
astro - Dev.to — Artículos en español con
astro
Repositorios
- withastro/astro — Código fuente
- one-aalam/awesome-astro — Recursos curados
- withastro/astro.new — Playground
- astro/themes — Temas oficiales
Blogs y newsletters
- Astro Blog — Anuncios oficiales
- Netlify Blog — Tutoriales de deploy
- Vercel Blog — Guías de rendimiento
- Astro Weekly — Newsletter semanal
Hacks y tips de productividad
1. Content collections con imágenes locales
---
import { getCollection } from 'astro:content';
import { Image } from 'astro:assets';
const posts = await getCollection('posts');
---
{posts.map(async post => {
const { image } = await post.data.image ? import(`../images/${post.data.image}`) : null;
return image ? <Image src={image} alt={post.data.title} /> : null;
})}
2. --host para pruebas en móviles
npm run dev -- --host 0.0.0.0
3. Hybrid rendering
export default defineConfig({ output: 'hybrid' });
---
export const prerender = false;
---
4. Asset pipeline con Vite
---
import Logo from '../images/logo.svg?raw';
---
<Fragment set:html={Logo} />
5. Slug personalizado desde frontmatter
---
import { getCollection } from 'astro:content';
const post = await getEntry('posts', ({ data }) => data.slug === 'mi-slug');
---
6. Deploy previews automáticos
on: [pull_request]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pnpm install && pnpm run build
- uses: amondnet/vercel-action@v25
7. astro check para TypeScript
npx astro check
8. Prefetch de enlaces
---
import { ViewTransitions } from 'astro:transitions';
---
<ViewTransitions />
9. Inline CSS crítico
export default defineConfig({
build: { inlineStylesheets: 'auto' },
});
10. Depurar frontmatter
---
const posts = await getCollection('posts');
---
<pre>{JSON.stringify(posts.map(p => p.data), null, 2)}</pre>
⚠️ Errores comunes
| Error | Causa | Solución |
|---|---|---|
Hydration completed but contains mismatches | El HTML renderizado en servidor (SSR) no coincide con el resultado del hydration en cliente | Revisa condicionales que dependan de import.meta.env o window; usa client:only si el componente solo corre en cliente |
[astro] X is not used as a JSX element | Componente importado pero no se usa con sintaxis JSX (<X />) en la plantilla | Asegúrate de usar <Componente /> en vez de llamarlo como función Componente() |
Unable to find the server | astro dev no puede arrancar el servidor de desarrollo | Verifica que el puerto no esté ocupado (lsof -i :4321), o cambia el puerto en astro.config.mjs con server: { port: 4322 } |
Cannot find module ... for 'astro:content' | La colección de contenido no está definida o el content/config.ts falta | Crea src/content/config.ts con defineCollection y ejecuta astro sync para generar tipos |
Error: Invalid option: strategies | Configuración de output: 'hybrid' o output: 'server' incompatiblemente mezclada | Revisa que output en astro.config.mjs sea 'static', 'server' o 'hybrid'; no combines strategies antiguas |
GET /_astro/... 404 en producción | Assets estáticos no se copiaron al build o la ruta de base es incorrecta | Comprueba base en astro.config.mjs si despliegues en subcarpeta; ejecuta astro build y verifica dist/_astro/ |
ReferenceError: Document is not defined | Código que usa document o window ejecutándose en el servidor durante SSR | Envuelve ese código en un onMount() de Astro o usa client:only="vue" para forzar ejecución en cliente |
🔗 Guías relacionadas
Si quieres ampliar tu stack con otras tecnologías, estas guías complementan Astro perfectamente:
- Guía de Markdown y MDX: de 0 a 100 — domina el formato que alimenta las colecciones de contenido de Astro
- Guía de TypeScript: de 0 a 100 — tipado estático para componentes y props en Astro
- Guía de JavaScript: de 0 a 100 — la base que soporta Islands, server endpoints y cualquier framework moderno
Referencias y documentación oficial
- Docs oficiales: docs.astro.build
- Astro Blog: astro.build/blog
- GitHub: github.com/withastro/astro
- Discord: astro.build/chat
- Playground: astro.new
- Themes: astro.build/themes
- Integrations: astro.build/integrations
- Awesome Astro: github.com/one-aalam/awesome-astro
- Netlify Blog: netlify.com/blog
- Vercel Blog: vercel.com/blog
- Tailwind CSS v4 Docs: tailwindcss.com/docs/v4-beta
- Stripe Docs: stripe.com/docs
- Playwright Docs: playwright.dev
- Vitest Docs: vitest.dev
- Pagefind Docs: pagefind.app
- Contentful Docs: contentful.com/developers/docs
- Decap CMS Docs: decapcms.org/docs
- Supabase Docs: supabase.com/docs
- r/astrojs: reddit.com/r/astrojs
- Astro Weekly: astroweekly.com
- MDX Docs: mdxjs.com
🏆 Retos Relacionados
Pon a prueba lo aprendido con estos desafíos:
