07. Contenido Dinámico
7.1 Uso de Markdown y MDX
Section titled “7.1 Uso de Markdown y MDX”Markdown y MDX en Astro
Section titled “Markdown y MDX en Astro”Markdown es un lenguaje de marcado ligero para crear contenido formateado, mientras que MDX extiende Markdown permitiendo usar componentes JSX dentro del contenido.
📝 Markdown
Section titled “📝 Markdown”Propósito
- Escribir contenido de forma simple y legible
- Generar HTML automáticamente
- Ideal para blogs, documentación y páginas de contenido
- Separar contenido de presentación
Características
- Sintaxis simple y minimalista
- Conversión automática a HTML
- Soporte nativo en Astro
- Frontmatter para metadatos
⚡ MDX (Markdown + JSX)
Section titled “⚡ MDX (Markdown + JSX)”Propósito
- Combinar contenido Markdown con componentes interactivos
- Reutilizar componentes en documentación
- Crear contenido dinámico y rico
- Mantener la simplicidad de Markdown con poder de React
Características
- Importar y usar componentes React/Vue/Svelte
- Usar JavaScript dentro del contenido
- Mantener sintaxis Markdown
- Validación de tipos con TypeScript
🔧 Cómo Funciona en Astro
Section titled “🔧 Cómo Funciona en Astro”- Archivos
.md: Markdown puro, se convierten a HTML - Archivos
.mdx: Markdown con componentes, se procesan con MDX - Frontmatter: Metadatos en YAML al inicio del archivo
- Layouts: Plantillas para envolver el contenido
- Componentes: Se pueden importar y usar en MDX
📋 Frontmatter
Section titled “📋 Frontmatter”Bloque de metadatos en formato YAML al inicio del archivo:
---title: Mi Artículodate: 2024-01-15author: Juan Péreztags: [astro, web]---🎯 Ventajas
Section titled “🎯 Ventajas”| Aspecto | Markdown | MDX |
|---|---|---|
| Simplicidad | ✅ Muy simple | ⚠️ Requiere conocer componentes |
| Interactividad | ❌ Solo estático | ✅ Componentes dinámicos |
| Rendimiento | ✅ Muy rápido | ✅ Rápido (SSG) |
| Mantenibilidad | ✅ Fácil de editar | ✅ Reutilizable |
| Uso recomendado | Contenido simple | Contenido rico |
📝 Archivo Markdown Básico
Section titled “📝 Archivo Markdown Básico”src/pages/blog/mi-post.md
---title: Mi Primer Postdescription: Introducción a AstropubDate: 2024-01-15author: Juan Pérezimage: /images/post1.jpgtags: [astro, web, tutorial]---
# Mi Primer Post
Este es un **post de ejemplo** usando Markdown en Astro.
## Características
- Fácil de escribir- Conversión automática a HTML- Soporte para imágenes

## Código
```javascriptconst mensaje = "Hola Astro";console.log(mensaje);Conclusión
Section titled “Conclusión”Markdown es perfecto para contenido simple y legible.
### ⚡ Archivo MDX con Componentes
**`src/pages/blog/post-interactivo.mdx`**```mdx---title: Post Interactivodescription: Usando componentes en MDXpubDate: 2024-01-20---
import Counter from '../../components/Counter.jsx';import Alert from '../../components/Alert.astro';import { Code } from '@astrojs/starlight/components';
# Post Interactivo con MDX
Este post combina **Markdown** con componentes interactivos.
## Componente React
Aquí hay un contador interactivo:
<Counter client:load />
## Componente Astro
<Alert type="info"> 💡 **Tip**: MDX te permite usar componentes directamente en tu contenido.</Alert>
## Código con Sintaxis
<Code code={`function saludar(nombre) { return \`Hola, \${nombre}!\`;}`} lang="js" />
## Contenido Normal
Puedes seguir escribiendo Markdown normal después de usar componentes.🎨 Layout para Markdown/MDX
Section titled “🎨 Layout para Markdown/MDX”src/layouts/BlogLayout.astro
---const { frontmatter } = Astro.props;---
<html> <head> <title>{frontmatter.title}</title> <meta name="description" content={frontmatter.description} /> </head> <body> <article> <header> <h1>{frontmatter.title}</h1> <p class="meta"> Por {frontmatter.author} • {frontmatter.pubDate} </p> {frontmatter.tags && ( <div class="tags"> {frontmatter.tags.map(tag => ( <span class="tag">{tag}</span> ))} </div> )} </header>
<div class="content"> <slot /> </div> </article> </body></html>🔧 Configuración de Layout
Section titled “🔧 Configuración de Layout”En archivo Markdown/MDX
---layout: ../../layouts/BlogLayout.astrotitle: Mi Post---
Contenido del post...O en astro.config.mjs
import { defineConfig } from 'astro/config';import mdx from '@astrojs/mdx';
export default defineConfig({ integrations: [mdx()], markdown: { // Layout por defecto para archivos .md layout: './src/layouts/BlogLayout.astro' }});🎯 Componentes Personalizados en MDX
Section titled “🎯 Componentes Personalizados en MDX”src/components/Callout.astro
---const { type = 'info' } = Astro.props;---
<div class={`callout callout-${type}`}> <slot /></div>
<style> .callout { padding: 1rem; border-radius: 8px; margin: 1rem 0; } .callout-info { background: #e3f2fd; } .callout-warning { background: #fff3e0; } .callout-error { background: #ffebee; }</style>Uso en MDX
---title: Tutorial---
import Callout from '../components/Callout.astro';
# Tutorial de Astro
<Callout type="info"> 📘 Este es un tutorial para principiantes.</Callout>
## Instalación
<Callout type="warning"> ⚠️ Asegúrate de tener Node.js instalado.</Callout>7.2 Colecciones de contenido (content collections)
Section titled “7.2 Colecciones de contenido (content collections)”Content Collections en Astro
Section titled “Content Collections en Astro”Las Content Collections son una forma estructurada y con validación de tipos para gestionar contenido en Astro (blogs, documentación, productos, etc.).
🎯 Propósito
Section titled “🎯 Propósito”- Organizar contenido de forma estructurada
- Validar esquemas con Zod
- Obtener autocompletado y type-safety
- Consultar y filtrar contenido fácilmente
- Generar páginas dinámicas automáticamente
⚙️ Cómo Funciona
Section titled “⚙️ Cómo Funciona”- Definición: Se crean en
src/content/[colección]/ - Esquema: Se define en
src/content/config.ts - Validación: Zod valida el frontmatter
- Consulta: API de
getCollection()ygetEntry() - Renderizado: Función
render()para obtener HTML
📁 Estructura
Section titled “📁 Estructura”src/└── content/ ├── config.ts # Definición de esquemas ├── blog/ # Colección de blog │ ├── post-1.md │ └── post-2.mdx └── docs/ # Colección de docs ├── intro.md └── guide.md🔍 API de Collections
Section titled “🔍 API de Collections”Funciones principales
getCollection(name): Obtiene todas las entradas de una coleccióngetEntry(collection, slug): Obtiene una entrada específicaentry.render(): Renderiza el contenido a HTML
📊 Validación con Zod
Section titled “📊 Validación con Zod”Zod es una librería de validación de esquemas TypeScript:
- Define tipos de datos esperados
- Valida frontmatter automáticamente
- Proporciona errores claros
- Genera tipos TypeScript automáticamente
🌟 Ventajas
Section titled “🌟 Ventajas”- Type Safety: Errores en tiempo de desarrollo
- Autocompletado: IntelliSense en el editor
- Validación: Frontmatter correcto garantizado
- Organización: Estructura clara del contenido
- Rendimiento: Optimización automática
- Consultas: API simple y poderosa
📋 Tipos de Colecciones
Section titled “📋 Tipos de Colecciones”| Tipo | Descripción | Uso |
|---|---|---|
| content | Archivos Markdown/MDX | Blogs, docs |
| data | Archivos JSON/YAML | Configuración, datos |
📋 Definición de Esquema
Section titled “📋 Definición de Esquema”src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({ type: 'content', schema: z.object({ title: z.string(), description: z.string(), pubDate: z.date(), author: z.string(), image: z.string().optional(), tags: z.array(z.string()), draft: z.boolean().default(false), }),});
const docsCollection = defineCollection({ type: 'content', schema: z.object({ title: z.string(), description: z.string(), order: z.number(), category: z.enum(['tutorial', 'guide', 'reference']), }),});
export const collections = { blog: blogCollection, docs: docsCollection,};📝 Entrada de Colección
Section titled “📝 Entrada de Colección”src/content/blog/primer-post.md
---title: Mi Primer Postdescription: Introducción a Astro Content CollectionspubDate: 2024-01-15author: Juan Péreztags: [astro, tutorial, web]draft: false---
# Mi Primer Post
Este es el contenido del post usando Content Collections.📄 Listar Todas las Entradas
Section titled “📄 Listar Todas las Entradas”src/pages/blog/index.astro
---import { getCollection } from 'astro:content';
// Obtener todos los postsconst allPosts = await getCollection('blog');
// Filtrar posts publicados (no drafts)const publishedPosts = allPosts.filter(post => !post.data.draft);
// Ordenar por fechaconst sortedPosts = publishedPosts.sort( (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());---
<html> <body> <h1>Blog</h1> <ul> {sortedPosts.map(post => ( <li> <a href={`/blog/${post.slug}`}> <h2>{post.data.title}</h2> <p>{post.data.description}</p> <time>{post.data.pubDate.toLocaleDateString()}</time> </a> </li> ))} </ul> </body></html>📖 Página Individual con getStaticPaths
Section titled “📖 Página Individual con getStaticPaths”src/pages/blog/[slug].astro
---import { getCollection } from 'astro:content';import BlogLayout from '../../layouts/BlogLayout.astro';
// Generar rutas para todos los postsexport async function getStaticPaths() { const posts = await getCollection('blog');
return posts.map(post => ({ params: { slug: post.slug }, props: { post }, }));}
const { post } = Astro.props;const { Content } = await post.render();---
<BlogLayout frontmatter={post.data}> <Content /></BlogLayout>🔍 Filtrado y Consultas Avanzadas
Section titled “🔍 Filtrado y Consultas Avanzadas”---import { getCollection } from 'astro:content';
// Filtrar por tagconst astroPost = await getCollection('blog', ({ data }) => { return data.tags.includes('astro');});
// Filtrar por autorconst myPosts = await getCollection('blog', ({ data }) => { return data.author === 'Juan Pérez';});
// Filtrar por fechaconst recentPosts = await getCollection('blog', ({ data }) => { const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); return data.pubDate > thirtyDaysAgo;});---🏷️ Página de Tags
Section titled “🏷️ Página de Tags”src/pages/tags/[tag].astro
---import { getCollection } from 'astro:content';
export async function getStaticPaths() { const allPosts = await getCollection('blog');
// Obtener todos los tags únicos const uniqueTags = [...new Set( allPosts.flatMap(post => post.data.tags) )];
// Crear una ruta por cada tag return uniqueTags.map(tag => ({ params: { tag }, props: { posts: allPosts.filter(post => post.data.tags.includes(tag) ) } }));}
const { tag } = Astro.params;const { posts } = Astro.props;---
<html> <body> <h1>Posts con tag: {tag}</h1> <ul> {posts.map(post => ( <li> <a href={`/blog/${post.slug}`}> {post.data.title} </a> </li> ))} </ul> </body></html>📊 Colección de Datos (JSON)
Section titled “📊 Colección de Datos (JSON)”src/content/config.ts
const teamCollection = defineCollection({ type: 'data', schema: z.object({ name: z.string(), role: z.string(), bio: z.string(), avatar: z.string(), social: z.object({ twitter: z.string().optional(), github: z.string().optional(), }), }),});
export const collections = { team: teamCollection,};src/content/team/juan.json
{ "name": "Juan Pérez", "role": "Developer", "bio": "Full-stack developer", "avatar": "/avatars/juan.jpg", "social": { "twitter": "@juanperez", "github": "juanperez" }}Uso
---import { getCollection } from 'astro:content';
const team = await getCollection('team');---
<div class="team"> {team.map(member => ( <div class="member"> <img src={member.data.avatar} alt={member.data.name} /> <h3>{member.data.name}</h3> <p>{member.data.role}</p> </div> ))}</div>7.3 Consumo de APIs y archivos JSON
Section titled “7.3 Consumo de APIs y archivos JSON”Consumo de APIs y Datos Externos en Astro
Section titled “Consumo de APIs y Datos Externos en Astro”Astro permite consumir datos de APIs REST, archivos JSON locales y otras fuentes de datos durante el proceso de build para generar páginas estáticas.
🎯 Propósito
Section titled “🎯 Propósito”- Obtener datos de APIs externas (REST, GraphQL)
- Cargar datos desde archivos JSON/YAML locales
- Generar páginas dinámicas con datos externos
- Mantener contenido actualizado desde fuentes externas
- Crear sitios estáticos con datos dinámicos
⚙️ Cómo Funciona
Section titled “⚙️ Cómo Funciona”En Build Time (SSG)
- Astro ejecuta código durante el build
- Hace peticiones a APIs o lee archivos
- Procesa y transforma los datos
- Genera HTML estático con los datos
- El resultado es un sitio completamente estático
En Runtime (SSR)
- Las peticiones se hacen cuando el usuario visita la página
- Los datos se obtienen en tiempo real
- Se genera HTML dinámicamente
- Requiere un servidor Node.js
📊 Métodos de Consumo
Section titled “📊 Métodos de Consumo”1. Fetch API
- API nativa de JavaScript
- Disponible en el frontmatter de Astro
- Ideal para APIs REST
- Soporte para async/await
2. Archivos JSON Locales
- Importación directa con
import - Datos estáticos en el proyecto
- Sin necesidad de red
- Ideal para configuración y datos fijos
3. Librerías HTTP
- Axios, node-fetch, etc.
- Más funcionalidades que fetch
- Interceptores, transformaciones
- Manejo avanzado de errores
🔄 Cuándo Usar Cada Método
Section titled “🔄 Cuándo Usar Cada Método”| Método | Cuándo Usar | Ventajas |
|---|---|---|
| Fetch en Build | Datos que cambian poco | Sitio estático rápido |
| JSON Local | Datos fijos del proyecto | Sin dependencias externas |
| SSR | Datos en tiempo real | Siempre actualizado |
| ISR | Balance actualización/rendimiento | Lo mejor de ambos |
🌟 Mejores Prácticas
Section titled “🌟 Mejores Prácticas”- Caché: Guarda respuestas para evitar peticiones repetidas
- Manejo de Errores: Siempre valida respuestas de APIs
- Tipos: Usa TypeScript para validar datos
- Paginación: Maneja grandes conjuntos de datos
- Rate Limiting: Respeta límites de APIs
- Variables de Entorno: Usa
.envpara API keys
🔒 Seguridad
Section titled “🔒 Seguridad”- API Keys: Nunca expongas en el cliente
- Variables de Entorno: Usa
import.meta.env - Validación: Valida datos de APIs externas
- CORS: Considera restricciones de origen cruzado
🌐 Consumo de API REST con Fetch
Section titled “🌐 Consumo de API REST con Fetch”src/pages/posts.astro
---// Fetch durante el buildconst response = await fetch('https://jsonplaceholder.typicode.com/posts');const posts = await response.json();---
<html> <body> <h1>Posts desde API</h1> <ul> {posts.map(post => ( <li> <h2>{post.title}</h2> <p>{post.body}</p> </li> ))} </ul> </body></html>📄 Páginas Dinámicas desde API
Section titled “📄 Páginas Dinámicas desde API”src/pages/posts/[id].astro
---export async function getStaticPaths() { // Obtener todos los posts const response = await fetch('https://jsonplaceholder.typicode.com/posts'); const posts = await response.json();
// Generar una ruta por cada post return posts.map(post => ({ params: { id: post.id.toString() }, props: { post } }));}
const { post } = Astro.props;---
<html> <body> <article> <h1>{post.title}</h1> <p>{post.body}</p> </article> </body></html>📦 Importar JSON Local
Section titled “📦 Importar JSON Local”src/data/products.json
[ { "id": 1, "name": "Laptop", "price": 999, "category": "electronics" }, { "id": 2, "name": "Mouse", "price": 29, "category": "electronics" }]src/pages/products.astro
---import productsData from '../data/products.json';---
<html> <body> <h1>Productos</h1> <div class="products"> {productsData.map(product => ( <div class="product"> <h2>{product.name}</h2> <p>${product.price}</p> <span>{product.category}</span> </div> ))} </div> </body></html>🔧 Manejo de Errores y Validación
Section titled “🔧 Manejo de Errores y Validación”---interface Post { id: number; title: string; body: string; userId: number;}
let posts: Post[] = [];let error: string | null = null;
try { const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
posts = await response.json();
// Validar datos if (!Array.isArray(posts)) { throw new Error('La respuesta no es un array'); }
} catch (e) { error = e instanceof Error ? e.message : 'Error desconocido'; console.error('Error al obtener posts:', error);}---
<html> <body> {error ? ( <div class="error"> <p>Error: {error}</p> </div> ) : ( <ul> {posts.map(post => ( <li>{post.title}</li> ))} </ul> )} </body></html>🔑 Uso de Variables de Entorno
Section titled “🔑 Uso de Variables de Entorno”.env
API_KEY=tu_api_key_secretaAPI_URL=https://api.ejemplo.comsrc/pages/data.astro
---const API_KEY = import.meta.env.API_KEY;const API_URL = import.meta.env.API_URL;
const response = await fetch(`${API_URL}/data`, { headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' }});
const data = await response.json();---
<html> <body> <pre>{JSON.stringify(data, null, 2)}</pre> </body></html>🚀 Función Auxiliar para Fetch
Section titled “🚀 Función Auxiliar para Fetch”src/utils/api.ts
export async function fetchAPI<T>( endpoint: string, options?: RequestInit): Promise<T> { const API_URL = import.meta.env.API_URL || 'https://api.ejemplo.com';
const response = await fetch(`${API_URL}${endpoint}`, { ...options, headers: { 'Content-Type': 'application/json', ...options?.headers, }, });
if (!response.ok) { throw new Error(`API error: ${response.status} ${response.statusText}`); }
return response.json();}
export async function fetchWithCache<T>( key: string, fetcher: () => Promise<T>, ttl: number = 3600000 // 1 hora): Promise<T> { const cached = globalThis.__cache?.get(key);
if (cached && Date.now() - cached.timestamp < ttl) { return cached.data; }
const data = await fetcher();
if (!globalThis.__cache) { globalThis.__cache = new Map(); }
globalThis.__cache.set(key, { data, timestamp: Date.now() });
return data;}Uso de la función auxiliar
---import { fetchAPI, fetchWithCache } from '../utils/api';
interface User { id: number; name: string; email: string;}
// Fetch simpleconst users = await fetchAPI<User[]>('/users');
// Fetch con cachéconst cachedUsers = await fetchWithCache( 'users', () => fetchAPI<User[]>('/users'));---📊 GraphQL API
Section titled “📊 GraphQL API”---const query = ` query { posts { id title content author { name } } }`;
const response = await fetch('https://api.ejemplo.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query })});
const { data } = await response.json();const posts = data.posts;---
<html> <body> {posts.map(post => ( <article> <h2>{post.title}</h2> <p>Por {post.author.name}</p> <div>{post.content}</div> </article> ))} </body></html>🔄 Combinación de Múltiples Fuentes
Section titled “🔄 Combinación de Múltiples Fuentes”---import localProducts from '../data/products.json';
// Datos de API externaconst apiResponse = await fetch('https://api.ejemplo.com/featured');const featuredProducts = await apiResponse.json();
// Combinar datos locales y externosconst allProducts = [ ...localProducts, ...featuredProducts];
// Filtrar y ordenarconst sortedProducts = allProducts .filter(p => p.price > 0) .sort((a, b) => b.price - a.price);---
<html> <body> <h1>Todos los Productos</h1> {sortedProducts.map(product => ( <div class="product"> <h3>{product.name}</h3> <p>${product.price}</p> </div> ))} </body></html>⏱️ Paginación de API
Section titled “⏱️ Paginación de API”src/pages/users/[page].astro
---const ITEMS_PER_PAGE = 10;
export async function getStaticPaths() { const response = await fetch('https://jsonplaceholder.typicode.com/users'); const users = await response.json();
const totalPages = Math.ceil(users.length / ITEMS_PER_PAGE);
return Array.from({ length: totalPages }, (_, i) => ({ params: { page: (i + 1).toString() }, props: { users: users.slice(i * ITEMS_PER_PAGE, (i + 1) * ITEMS_PER_PAGE), currentPage: i + 1, totalPages } }));}
const { users, currentPage, totalPages } = Astro.props;---
<html> <body> <h1>Usuarios - Página {currentPage} de {totalPages}</h1> <ul> {users.map(user => ( <li>{user.name} - {user.email}</li> ))} </ul>
<nav> {currentPage > 1 && ( <a href={`/users/${currentPage - 1}`}>← Anterior</a> )} {currentPage < totalPages && ( <a href={`/users/${currentPage + 1}`}>Siguiente →</a> )} </nav> </body></html>