Publicación de Sitios Web con Astro
Construye sitios web estáticos rápidos y centrados en contenido con el enfoque SSG sin runtime de Astro, hidratación parcial y amplio soporte para Markdown.
Contenido
- Inicio Rápido
- Cuándo No Usar
- Estructura del Proyecto
- SSG vs SSR vs Híbrido
- Content Collections — Content Layer API, Cargadores Personalizados
- Resaltado de Sintaxis — Shiki, Transformadores, Expressive Code
- Integración de Diagramas — Mermaid, PlantUML, Temas de Modo Oscuro
- Búsqueda en el Lado del Cliente — Pagefind (controles, ponderación), Fuse.js
- Documentación Versionada — Múltiples versiones
- Internacionalización — Enrutamiento, Fallbacks
- Patrones Comunes — Paginación, Etiquetas, RSS, Formularios
- Mejores Prácticas de Rendimiento — Precarga, CSS Crítico
- Despliegue — Netlify
- Lista de Verificación Pre-Despliegue
- Testing y Calidad — Vitest, Playwright, Verificación de Enlaces
- Solución de Problemas
Inicio Rápido
# Crear nuevo proyecto (usa la plantilla Blog para sitios Markdown)
npm create astro@latest
# Desarrollo
npm run dev # Servidor local en http://localhost:4321
npm run build # Generar archivos estáticos en dist/
npm run preview # Vista previa de la compilación de producción
Cuándo No Usar
Esta skill se centra en generación de sitios estáticos (SSG). Considera otros enfoques para:
- Aplicaciones con datos en tiempo real - Usa el modo SSR con conexiones a bases de datos
- Flujos de autenticación de usuarios - Requiere manejo de sesiones del lado del servidor
- Comercio electrónico con inventario dinámico - Usa modo híbrido o SSR completo
- Aplicaciones de una sola página (SPA) - Considera frameworks React/Vue directamente
Para patrones híbridos SSG+SSR, consulta la documentación de adaptadores de Astro.
Estructura del Proyecto
src/
components/ # Componentes Astro, React, Vue, Svelte
content/ # Content Collections (Markdown/MDX)
config.ts # Esquemas de colecciones
docs/ # Ejemplo de colección
layouts/ # Wrappers de página con slots
pages/ # Enrutamiento basado en archivos
public/ # Activos estáticos (imágenes, fuentes, favicons)
astro.config.mjs # Configuración del framework
SSG vs SSR vs Híbrido
| Modo | Cuándo se Renderizan las Páginas | Caso de Uso |
|---|---|---|
| SSG (por defecto) | En tiempo de compilación | Blogs, documentación, sitios de marketing |
| SSR | En cada solicitud | Datos dinámicos, personalización |
| Híbrido | Mezcla de ambos | Páginas estáticas + endpoints dinámicos |
Para sitios puramente estáticos, usa el valor predeterminado output: 'static' - no se necesita adaptador.
Content Collections
Patrón Legacy (Astro 4.x)
Define esquemas en src/content/config.ts:
import { defineCollection, z } from "astro:content";
export const collections = {
docs: defineCollection({
schema: z.object({
title: z.string(),
description: z.string().optional(),
tags: z.array(z.string()).optional(),
order: z.number().optional(),
draft: z.boolean().default(false)
})
})
};
Content Layer API (Astro 5.0+)
Nuevo patrón con el cargador glob() - hasta un 75% más rápido en compilaciones para sitios grandes:
// src/content.config.ts (nota: nombre de archivo diferente)
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
schema: ({ image }) => z.object({
title: z.string(),
pubDate: z.coerce.date(),
draft: z.boolean().default(false),
cover: image(), // Valida que la imagen existe
author: reference('authors'), // Referencia entre colecciones
})
});
export const collections = { blog };
Patrones de Esquema Avanzados
schema: ({ image }) => z.object({
cover: image(), // Valida imagen en src/
category: z.enum(['tech', 'news']),
author: reference('authors'), // Ref. entre colecciones
relatedPosts: z.array(reference('blog')).optional(),
})
Cargadores Personalizados (Contenido Remoto)
Obtén contenido de APIs externas (lanzamientos de GitHub, CMS, etc.):
// src/loaders/github-releases.ts
import type { Loader } from 'astro/loaders';
export function githubReleasesLoader(repo: string): Loader {
return {
name: 'github-releases',
load: async ({ store, logger }) => {
logger.info(`Obteniendo lanzamientos para ${repo}`);
const response = await fetch(`https://api.github.com/repos/${repo}/releases`);
const releases = await response.json();
for (const release of releases) {
store.set({
id: release.tag_name,
data: {
version: release.tag_name,
published_at: release.published_at,
body: release.body // Notas de lanzamiento en Markdown
}
});
}
}
};
}
Registra en content.config.ts:
import { githubReleasesLoader } from './loaders/github-releases';
const releases = defineCollection({
loader: githubReleasesLoader('owner/repo'),
schema: z.object({
version: z.string(),
published_at: z.string(),
body: z.string(),
})
});
Consulta y renderiza colecciones:
---
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const docs = await getCollection("docs");
return docs.map(doc => ({
params: { slug: doc.slug },
props: { doc }
}));
}
const { doc } = Astro.props;
const { Content } = await doc.render();
---
<article>
<h1>{doc.data.title}</h1>
<Content />
</article>
Resaltado de Sintaxis
Configuración Básica de Shiki
import { defineConfig } from "astro/config";
export default defineConfig({
markdown: {
shikiConfig: {
theme: "github-dark",
wrap: true
}
}
});
Tema Dual Claro/Oscuro
shikiConfig: {
themes: {
light: 'github-light',
dark: 'github-dark',
},
}
Añade CSS para cambiar temas:
@media (prefers-color-scheme: dark) {
.astro-code, .astro-code span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
}
Resaltado de Líneas y Transformadores
```typescript {2,4}
const a = 1;
const b = 2; // resaltado
const c = 3;
console.log(a + b + c); // resaltado
```
Transformadores Shiki (Astro 4.14+):
import { transformerNotationFocus, transformerNotationDiff } from '@shikijs/transformers';
shikiConfig: {
transformers: [transformerNotationFocus(), transformerNotationDiff()],
}
Usa comentarios de notación en el código:
// [!code focus]- Enfoca esta línea// [!code ++]- Marcar como adición (verde)// [!code --]- Marcar como eliminación (rojo)
Expressive Code (Recomendado para Documentación)
Bloques de código enriquecidos con botones de copia, nombres de archivo, resaltado de diferencias:
npm install astro-expressive-code
import expressiveCode from 'astro-expressive-code';
export default defineConfig({
integrations: [expressiveCode()],
});
Características: Botón de copia, pestañas de archivos, marcadores de línea, marcos de terminal, marcadores de texto.
Integración de Diagramas
Mermaid (Recomendado)
Instala la integración de Astro:
npm install astro-mermaid mermaid
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mermaid from 'astro-mermaid';
export default defineConfig({
integrations: [mermaid({ theme: 'default' })]
});
Usa en Markdown:
```mermaid
graph TD;
A-->B;
B-->C;
```
Características: Renderizado del lado del cliente, cambio automático de tema, funcionalidad sin conexión, no requiere Playwright.
Alternativa (SVG estático en tiempo de compilación): Usa rehype-mermaid con Playwright para diagramas pre-renderizados (requiere npx playwright install --with-deps).
Estrategias de Temas para Modo Oscuro:
- Variables CSS - Deja que el navegador resuelva los colores en tiempo de ejecución:
// configuración de mermaid
mermaid.initialize({
theme: 'base',
themeVariables: {
primaryColor: 'var(--diagram-primary)',
lineColor: 'var(--diagram-line)'
}
});
- Elemento Picture - Genera ambos temas, intercambia con media query:
<picture>
<source srcset="/diagrams/flow-dark.svg" media="(prefers-color-scheme: dark)">
<img src="/diagrams/flow-light.svg" alt="Diagrama de flujo">
</picture>
- SVG en Línea - Apunta a clases SVG con CSS (riesgo: colisiones de estilo) - añade criterios de accesibilidad con posibilidad de tener role='image' o role="presentation":
.dark .mermaid-svg .node rect {
fill: var(--bg-dark);
}
PlantUML
npx astro add plantuml
Usa en Markdown:
```plantuml
@startuml
Alice -> Bob: Hola
Bob --> Alice: ¡Hola!
@enduml
```
Búsqueda en el Lado del Cliente
Pagefind (Recomendado para Sitios Grandes)
Búsqueda estática sin configuración que indexa en tiempo de compilación:
npm install pagefind
Añade a package.json:
{
"scripts": {
"build": "astro build && npx pagefind --site dist",
"postbuild": "pagefind --site dist"
}
}
Usa en componentes:
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-ui.js" type="text/javascript"></script>
<div id="search"></div>
<script>
window.addEventListener('DOMContentLoaded', () => {
new PagefindUI({ element: '#search', showSubResults: true });
});
</script>
Características: Sin servicio externo, funciona sin conexión, indexación automática, paquete pequeño (~10KB).
Control de Indexación Granular:
<!-- Solo indexa el contenido principal, no los encabezados/barras laterales -->
<main data-pagefind-body>
<h1 data-pagefind-meta="title">Título de la Página</h1>
<p data-pagefind-weight="10">Texto introductorio importante</p>
<!-- Excluir de los fragmentos de búsqueda -->
<nav data-pagefind-ignore>
<a href="/related">Artículos Relacionados</a>
</nav>
</main>
| Atributo | Propósito |
|---|---|
data-pagefind-body | Limitar la indexación solo a este elemento |
data-pagefind-ignore | Excluir elemento del índice |
data-pagefind-meta="key" | Definir campo de metadatos |
data-pagefind-weight="10" | Aumentar relevancia (predeterminado: 1) |
Pagefind vs Fuse.js
| Característica | Pagefind | Fuse.js |
|---|---|---|
| Arquitectura | Fragmentos binarios precompilados | En memoria en tiempo de ejecución |
| Ancho de banda | Bajo (carga solo fragmentos necesarios) | Alto (descarga el índice completo) |
| Escalabilidad | Más de 10,000 páginas | Menos de 500 páginas |
| Multilingüe | Derivación nativa | Configuración manual |
| Caso de uso | Búsqueda global del sitio | Filtrado de listas pequeñas |
Fuse.js (Alternativa Ligera)
Para sitios más pequeños con necesidades de UI personalizadas:
---
import { getCollection } from "astro:content";
const posts = await getCollection('blog');
const searchIndex = JSON.stringify(posts.map(post => ({
title: post.data.title,
slug: post.slug,
body: post.body.slice(0, 500)
})));
---
<input type="search" id="search" placeholder="Buscar..." />
<ul id="results"></ul>
<script define:vars={{ searchIndex }}>
import Fuse from 'fuse.js';
const fuse = new Fuse(JSON.parse(searchIndex), {
keys: ['title', 'body'],
threshold: 0.3
});
document.getElementById('search').addEventListener('input', (e) => {
const results = fuse.search(e.target.value);
const resultsEl = document.getElementById('results');
// Limpiar resultados anteriores de forma segura
resultsEl.replaceChildren();
// Construir resultados usando métodos DOM seguros
results.forEach(r => {
const li = document.createElement('li');
const link = document.createElement('a');
link.href = `/blog/${r.item.slug}`;
link.textContent = r.item.title;
li.appendChild(link);
resultsEl.appendChild(li);
});
});
</script>
Para necesidades empresariales, considera Algolia (API de búsqueda alojada).
Documentación Versionada
Patrón de Documentación Multi-Versión
Usa una estructura basada en carpetas:
src/content/docs/
v1/
getting-started.md
api-reference.md
v2/
getting-started.md
api-reference.md
new-feature.md
Internacionalización (i18n)
Configura en astro.config.mjs:
export default defineConfig({
i18n: {
defaultLocale: "en",
locales: ["en", "fr", "es"],
routing: {
prefixDefaultLocale: false
}
}
});
Estructura el contenido por locale:
src/content/docs/
en/
getting-started.md
fr/
getting-started.md
Detecta el locale en componentes:
---
const locale = Astro.currentLocale || 'en';
---
Fallback para Traducciones Faltantes
Muestra contenido en inglés con un banner cuando no existan traducciones:
---
// src/pages/[lang]/[...slug].astro
import { getCollection, getEntry } from "astro:content";
const languages = ['en', 'es', 'fr'];
const defaultLang = 'en';
export async function getStaticPaths() {
const englishDocs = await getCollection('docs', ({ id }) => id.startsWith('en/'));
const paths = [];
for (const doc of englishDocs) {
const slug = doc.id.replace(/^en\//, '');
for (const lang of languages) {
const localizedId = `${lang}/${slug}`;
const localizedDoc = await getEntry('docs', localizedId);
paths.push({
params: { lang, slug },
props: {
entry: localizedDoc || doc, // Fallback a inglés
isFallback: !localizedDoc
}
});
}
}
return paths;
}
const { entry, isFallback } = Astro.props;
const { Content } = await entry.render();
---
{isFallback && (
<div class="translation-notice">
Esta página aún no está disponible en tu idioma.
</div>
)}
<Content />
Patrones Comunes
Listados Paginados
Para sitios con más de 50 publicaciones, divide en páginas:
---
// src/pages/blog/page/[page].astro
import { getCollection } from "astro:content";
const POSTS_PER_PAGE = 10;
export async function getStaticPaths() {
const allPosts = await getCollection("blog");
const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
return Array.from({ length: totalPages }, (_, i) => ({
params: { page: String(i + 1) },
props: { page: i + 1, totalPages }
}));
}
const { page, totalPages } = Astro.props;
const allPosts = await getCollection("blog");
const start = (page - 1) * POSTS_PER_PAGE;
const posts = allPosts.slice(start, start + POSTS_PER_PAGE);
---
<main>
{posts.map(post => (
<article>
<h2><a href={`/blog/${post.slug}`}>{post.data.title}</a></h2>
<p>{post.data.description}</p>
</article>
))}
<nav>
{page > 1 && <a href={`/blog/page/${page - 1}`}>Anterior</a>}
{page < totalPages && <a href={`/blog/page/${page + 1}`}>Siguiente</a>}
</nav>
</main>
Etiquetas y Categorías
Genera páginas para cada categoría:
---
import { getCollection } from "astro:content";
const allPosts = await getCollection("blog");
const categories = [...new Set(allPosts.flatMap(p => p.data.tags))];
export async function getStaticPaths() {
return categories.map(category => ({
params: { category },
props: {
posts: allPosts.filter(p => p.data.tags?.includes(category))
}
}));
}
const { posts } = Astro.props;
---
RSS Feed
Genera el feed durante la compilación:
npm install @astrojs/rss
// src/pages/rss.xml.js
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: 'Un blog sobre tecnología',
site: context.site,
items: posts.map((post) => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/blog/${post.slug}/`,
})),
});
}
Manejo de Formularios
Envíos de formularios estáticos:
<form netlify name="contact" method="POST" data-netlify="true">
<input type="hidden" name="form-name" value="contact" />
<input type="text" name="name" placeholder="Tu nombre" required />
<input type="email" name="email" placeholder="Tu correo" required />
<textarea name="message" placeholder="Mensaje"></textarea>
<button type="submit">Enviar</button>
</form>
Alternativa: usa Web3Forms, Formspree o un endpoint de funciones serverless.
Mejores Prácticas de Rendimiento
Precarga con Speculation Rules
Precarga las páginas en segundo plano:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
prefetch: {
prefetchAll: true,
defaultStrategy: 'hover'
}
});
Usa en enlaces:
<a href="/about" data-astro-prefetch="tap">Acerca de</a>
CSS Crítico
Extrae CSS en línea para la carga inicial:
npm install astro-critical-css
Optimización de Imágenes
Usa el componente Image de Astro:
---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.png';
---
<Image src={heroImage} alt="Hero" widths={[400, 800, 1200]} sizes="(max-width: 800px) 100vw, 800px" />
Optimización de Fuentes
Carga fuentes de manera eficiente:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet" />
Despliegue
El sitio estático se construye en dist/. Elige un adaptador según la plataforma:
Netlify (Sin Adaptador)
Netlify detecta automáticamente Astro. Configura:
# netlify.toml
[build]
command = "npm run build"
publish = "dist"
Adaptador de Vercel
npm install @astrojs/vercel
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/static';
export default defineConfig({
output: 'static',
adapter: vercel(),
});
Adaptador de GitHub Pages
npm install @astrojs/netlify # No, usa actions/gh-pages
Configura el flujo de trabajo:
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: withastro/action@v2
Adaptador de Firebase (GCP)
npm install @astrojs/firebase
import { defineConfig } from 'astro/config';
import firebase from '@astrojs/firebase';
export default defineConfig({
output: 'static',
adapter: firebase(),
});
Lista de Verificación Pre-Despliegue
- Compilación de producción (
npm run build) se completa sin errores - Vista previa de compilación (
npm run preview) funciona localmente - Las rutas personalizadas 404/500 existen
- Metadatos SEO (título, descripción, Open Graph) son correctos en todas las páginas
- El sitemap se genera (
npm install @astrojs/sitemap) - Los enlaces externos se abren con
target="_blank" rel="noopener noreferrer" - Las imágenes tienen
alttexto descriptivo - El contraste de colores cumple con WCAG AA
- No hay enlaces rotos (usa
npm run check-links) - La navegación por teclado funciona en todos los elementos interactivos
- Los lectores de pantalla pueden acceder al contenido (prueba con NVDA/VoiceOver)
- Los análisis/scripts de terceros son diferidos o asíncronos
- Se configura la compresión de activos estáticos (Brotli/Gzip)
- Las cabeceras de caché se configuran según el tipo de archivo (inmutable para hashes, corto para HTML)
- Las redirecciones se prueban (por ejemplo, dominio antiguo a nuevo, trailing slashes)
- SSL/TLS está habilitado (redirección automática HTTP a HTTPS)
- Se prueba la funcionalidad sin conexión (offline) si se usa Service Worker
- Los formularios se envían correctamente (Netlify/Formspree/Serverless)
- El feed RSS se valida
- El mapa del sitio se presenta a Google Search Console
- Se verifica el rendimiento con Lighthouse (mínimo 90 en todas las categorías)
Testing y Calidad
Testing Unitario con Vitest
npm install -D vitest @astrojs/astro-test
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
include: ['src/**/*.test.ts'],
},
});
Testing de Integración con Playwright
npm install -D @playwright/test
npx playwright install
// tests/navigation.spec.js
import { test, expect } from '@playwright/test';
test('navegación funciona correctamente', async ({ page }) => {
await page.goto('http://localhost:4321');
await page.click('text=Blog');
await expect(page).toHaveURL(/\/blog/);
await expect(page.locator('h1')).toContainText('Blog');
});
Verificación de Enlaces
npm install -D hyperlink
// package.json
{
"scripts": {
"check-links": "hyperlink dist/ --internal --suggest"
}
}
Solución de Problemas
Problemas Comunes
| Problema | Causa | Solución |
|---|---|---|
| La compilación falla con errores de TypeScript | Esquemas de colecciones incorrectos | Verifica las definiciones de Zod importadas desde astro:content |
| El contenido no aparece en producción | Archivos en src/content/ no son seguidos por git | Asegúrate de que git rastrea la carpeta content/ |
| Las imágenes no se muestran en la compilación | Las rutas de recursos son incorrectas | Usa import para imágenes en lugar de cadenas de texto literales |
| El resaltado de sintaxis no funciona | Configuración de Shiki no cargada | Verifica que shikiConfig esté dentro del bloque markdown en astro.config.mjs |
| Error de carga de Content Layer API | Nombre de archivo diferente (content.config.ts en lugar de config.ts) | Usa src/content.config.ts (sin carpeta content/ intermedia) |
| El CSS no se minifica en producción | Opciones de compilación de Vite | Verifica vite.css.minify en astro.config.mjs |
| El rendimiento de Lighthouse es bajo | Falta CSS crítico, carga de fuentes | Implementa extracción de CSS en línea y carga diferida de fuentes |
Depuración
- Compilaciones lentas: Ejecuta
astro build --verbosepara ver tiempos de cada paso - Colecciones vacías: Verifica que los archivos coincidan con el patrón del cargador
glob() - Problemas de SSR: Revisa
outputconfig y el adaptador de despliegue - Errores de Pagefind: Asegúrate de que la compilación se completa antes de la indexación