Guía de Astro: De 0 a 100

Guía de Astro: De 0 a 100

Desde la primera página hasta producción: instalación, conceptos, islas, colecciones, despliegue y recursos en español.

FECHA:
ACTUALIZADO:
AUTOR:Jorge Beneyto Castelló
LECTURA:27 MINUTOS DE LECTURA

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

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@latest y 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/ y defineCollection con Zod
  • getCollection(), getEntry(), filtros, ordenación
  • Archivos MDX con componentes
  • remark/rehype plugins

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ó a src/content.config.ts usando la API de loaders (como glob()) directamente, sin type: '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ísticaAstro (MPA)Next.js/Nuxt (SPA/SSR)
HTML por rutaCompletoShell + JS
JS por defectoNingunoBundle de cliente
RouterServidor (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 -->
DirectivaCuándo usarlaEjemplo
client:loadDebe estar disponible de inmediatoNavbar, header
client:idleNo es urgente, puede esperarChat widget, notificaciones
client:visibleEstá abajo en la páginaGalería, comentarios
client:mediaSolo en ciertos tamañosSidebar desktop, menú mobile
client:onlyUsa APIs del navegadorMapas, 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:

ValorEfecto
export const prerender = true;Fuerza SSG aunque output sea server
export const prerender = false;Fuerza SSR aunque output sea hybrid o static
Sin exportSigue el modo global (static, server, hybrid)

Comparativa rápida

Aspectostaticserverhybrid
HTML generadoBuild timeCada peticiónMixto
VelocidadMáximaVariableSegú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 servidorNingunoSíParcial
CDN✅ TotalParcial✅ Mayoría
Adaptador necesario❌ No✅ Sí✅ Sí
Ideal paraBlogs, docsDashboards, APIsSitios 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, fetch para 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

FrameworkPropósitoAsyncCLI
PlaywrightE2E testingSínpx playwright test
VitestUnitario/IntegraciónSínpx vitest
Astro checkType checkingNonpx astro check
PagefindBúsqueda offlineSí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

Blogs y newsletters


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

ErrorCausaSolución
Hydration completed but contains mismatchesEl HTML renderizado en servidor (SSR) no coincide con el resultado del hydration en clienteRevisa 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 elementComponente importado pero no se usa con sintaxis JSX (<X />) en la plantillaAsegúrate de usar <Componente /> en vez de llamarlo como función Componente()
Unable to find the serverastro dev no puede arrancar el servidor de desarrolloVerifica 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 faltaCrea src/content/config.ts con defineCollection y ejecuta astro sync para generar tipos
Error: Invalid option: strategiesConfiguración de output: 'hybrid' o output: 'server' incompatiblemente mezcladaRevisa que output en astro.config.mjs sea 'static', 'server' o 'hybrid'; no combines strategies antiguas
GET /_astro/... 404 en producciónAssets estáticos no se copiaron al build o la ruta de base es incorrectaComprueba base en astro.config.mjs si despliegues en subcarpeta; ejecuta astro build y verifica dist/_astro/
ReferenceError: Document is not definedCódigo que usa document o window ejecutándose en el servidor durante SSREnvuelve 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:


Referencias y documentación oficial


🏆 Retos Relacionados

Pon a prueba lo aprendido con estos desafíos:

COMPARTIR:
ETIQUETADO EN:
COMENTARIOS:

📋 Contenido