Astro Cheatsheet
■h1 títuloAstro - Marco Web para sitios web enviados por contenido "Clase de inscripción" Astro es un generador de sitio estático moderno que ofrece un rendimiento rápido y luminoso con una experiencia de desarrollador moderna. Construir sitios de contenido como blogs, marketing y comercio electrónico con sus componentes UI favoritos e implementar en cualquier lugar. ▪/p] ■/div titulada
########################################################################################################################################################################################################################################################## Copiar todos los comandos
########################################################################################################################################################################################################################################################## Generar PDF seleccionado/button
■/div titulada ■/div titulada
Cuadro de contenidos
- Instalación
- Creación del proyecto
- Estructura del proyecto
- Components
- Pagos y Routing
- Layouts
- Styling
- Content Collections
- Integraciones
- Islands Architecture
- API Routes
- Optimización de imagen
- Deployment
- Performance
- Testing
- Solucionando
- Las mejores prácticas
Instalación
Prerrequisitos
# Node.js version requirements
node --version # Should be 16.12.0 or later
# Check npm version
npm --version
# Update npm if needed
npm install -g npm@latest
Crear un proyecto de Astro
# Create new Astro project
npm create astro@latest
# Create with specific template
npm create astro@latest my-project -- --template blog
npm create astro@latest my-project -- --template portfolio
npm create astro@latest my-project -- --template docs
# Skip prompts with defaults
npm create astro@latest my-project -- --template minimal --yes
# Install dependencies and start
cd my-project
npm install
npm run dev
Instalación manual
# Create project directory
mkdir my-astro-project
cd my-astro-project
# Initialize package.json
npm init -y
# Install Astro
npm install astro
# Create basic structure
mkdir src src/pages src/layouts src/components
touch src/pages/index.astro
touch astro.config.mjs
Paquete.json Scripts
{
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
}
}
Creación de proyectos
Plantillas disponibles
# Official templates
npm create astro@latest -- --template minimal
npm create astro@latest -- --template blog
npm create astro@latest -- --template portfolio
npm create astro@latest -- --template docs
npm create astro@latest -- --template with-tailwindcss
npm create astro@latest -- --template with-react
npm create astro@latest -- --template with-vue
npm create astro@latest -- --template with-svelte
npm create astro@latest -- --template with-preact
npm create astro@latest -- --template with-solid-js
# Community templates
npm create astro@latest -- --template cassidoo/shopify-react-astro
npm create astro@latest -- --template withastro/astro-theme-creek
npm create astro@latest -- --template satnaing/astro-paper
Comandos de Desarrollo
# Start development server
npm run dev
# Start with specific port
npm run dev -- --port 3000
# Start with specific host
npm run dev -- --host 0.0.0.0
# Start with open browser
npm run dev -- --open
# Start with verbose logging
npm run dev -- --verbose
Construir comandos
# Build for production
npm run build
# Preview production build
npm run preview
# Check for issues
npx astro check
# Add missing integrations
npx astro add tailwind
npx astro add react
npx astro add vue
Estructura del proyecto
Estructura básica
my-astro-project/
├── public/ # Static assets
│ ├── favicon.svg
│ └── images/
├── src/
│ ├── components/ # Reusable components
│ ├── layouts/ # Layout components
│ ├── pages/ # File-based routing
│ ├── styles/ # CSS files
│ └── content/ # Content collections
├── astro.config.mjs # Astro configuration
├── package.json
└── tsconfig.json
Estructura avanzada
src/
├── components/
│ ├── ui/ # Basic UI components
│ ├── layout/ # Layout-specific components
│ └── content/ # Content-related components
├── layouts/
│ ├── BaseLayout.astro
│ ├── BlogLayout.astro
│ └── DocsLayout.astro
├── pages/
│ ├── index.astro # Homepage
│ ├── about.astro # About page
│ ├── blog/
│ │ ├── index.astro # Blog index
│ │ └── [slug].astro # Blog post
│ └── api/ # API routes
├── content/
│ ├── blog/ # Blog posts
│ └── docs/ # Documentation
├── styles/
│ ├── global.css
│ └── components.css
└── utils/
├── helpers.ts
└── constants.ts
Configuración
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import react from '@astrojs/react';
import mdx from '@astrojs/mdx';
export default defineConfig({
site: 'https://example.com',
base: '/',
integrations: [
tailwind(),
react(),
mdx()
],
markdown: {
shikiConfig: {
theme: 'github-dark',
wrap: true
}
},
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "src/styles/variables.scss";`
}
}
}
},
server: {
port: 3000,
host: true
},
build: {
assets: 'assets'
}
});
Componentes
Componentes de astro
---
// src/components/Card.astro
export interface Props {
title: string;
description: string;
href?: string;
image?: string;
}
const { title, description, href, image } = Astro.props;
---
<div class="card">
{image && (
<img src={image} alt={title} class="card-image" />
)}
<div class="card-content">
<h3 class="card-title">{title}</h3>
<p class="card-description">{description}</p>
{href && (
<a href={href} class="card-link">
Read more →
</a>
)}
</div>
</div>
<style>
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: transform 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
}
.card-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.card-content {
padding: 1.5rem;
}
.card-title {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
font-weight: 600;
}
.card-description {
margin: 0 0 1rem 0;
color: #666;
line-height: 1.6;
}
.card-link {
color: #3b82f6;
text-decoration: none;
font-weight: 500;
}
.card-link:hover {
text-decoration: underline;
}
</style>
Componente con Ranuras
---
// src/components/Modal.astro
export interface Props {
isOpen: boolean;
title: string;
}
const { isOpen, title } = Astro.props;
---
{isOpen && (
<div class="modal-overlay">
<div class="modal">
<header class="modal-header">
<h2>{title}</h2>
<button class="modal-close" onclick="closeModal()">×</button>
</header>
<div class="modal-body">
<slot />
</div>
<footer class="modal-footer">
<slot name="footer" />
</footer>
</div>
</div>
)}
<script>
function closeModal() {
document.querySelector('.modal-overlay').style.display = 'none';
}
</script>
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: white;
border-radius: 8px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
.modal-body {
padding: 1rem;
}
.modal-footer {
padding: 1rem;
border-top: 1px solid #e5e7eb;
}
</style>
Componentes marco
---
// src/components/Counter.astro
---
<div id="counter-container">
<h3>Counter Example</h3>
<!-- React Component -->
<div class="framework-example">
<h4>React Counter</h4>
<ReactCounter client:load />
</div>
<!-- Vue Component -->
<div class="framework-example">
<h4>Vue Counter</h4>
<VueCounter client:load />
</div>
<!-- Svelte Component -->
<div class="framework-example">
<h4>Svelte Counter</h4>
<SvelteCounter client:load />
</div>
</div>
<style>
.framework-example {
margin: 1rem 0;
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
</style>
// src/components/ReactCounter.jsx
import { useState } from 'react';
export default function ReactCounter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
</div>
);
}
Scripts de componentes
---
// src/components/InteractiveCard.astro
export interface Props {
title: string;
content: string;
}
const { title, content } = Astro.props;
---
<div class="interactive-card" data-title={title}>
<h3>{title}</h3>
<p>{content}</p>
<button class="toggle-btn">Toggle Details</button>
<div class="details" style="display: none;">
<p>Additional details about {title}</p>
</div>
</div>
<script>
// Client-side script
document.addEventListener('DOMContentLoaded', () => {
const cards = document.querySelectorAll('.interactive-card');
cards.forEach(card => {
const button = card.querySelector('.toggle-btn');
const details = card.querySelector('.details');
button.addEventListener('click', () => {
const isVisible = details.style.display !== 'none';
details.style.display = isVisible ? 'none' : 'block';
button.textContent = isVisible ? 'Show Details' : 'Hide Details';
});
});
});
</script>
<style>
.interactive-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
.toggle-btn {
background: #3b82f6;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
.details {
margin-top: 1rem;
padding: 1rem;
background: #f9fafb;
border-radius: 4px;
}
</style>
Pages and Routing
Routing basado en archivos
# Route structure
src/pages/
├── index.astro # / (homepage)
├── about.astro # /about
├── contact.astro # /contact
├── blog/
│ ├── index.astro # /blog
│ ├── [slug].astro # /blog/[slug]
│ └── tag/
│ └── [tag].astro # /blog/tag/[tag]
├── products/
│ ├── index.astro # /products
│ ├── [id].astro # /products/[id]
│ └── [...path].astro # /products/[...path] (catch-all)
└── api/
├── posts.json.ts # /api/posts.json
└── contact.ts # /api/contact
Página básica
---
// src/pages/about.astro
import Layout from '../layouts/Layout.astro';
const pageTitle = 'About Us';
const description = 'Learn more about our company and mission.';
---
<Layout title={pageTitle} description={description}>
<main>
<h1>About Us</h1>
<section class="hero">
<h2>Our Mission</h2>
<p>
We're dedicated to creating amazing web experiences
that delight users and drive business results.
</p>
</section>
<section class="team">
<h2>Our Team</h2>
<div class="team-grid">
<div class="team-member">
<img src="/team/john.jpg" alt="John Doe" />
<h3>John Doe</h3>
<p>CEO & Founder</p>
</div>
<div class="team-member">
<img src="/team/jane.jpg" alt="Jane Smith" />
<h3>Jane Smith</h3>
<p>CTO</p>
</div>
</div>
</section>
</main>
</Layout>
<style>
.hero {
text-align: center;
padding: 4rem 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px;
margin: 2rem 0;
}
.team-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin: 2rem 0;
}
.team-member {
text-align: center;
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.team-member img {
width: 150px;
height: 150px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 1rem;
}
</style>
Páginas dinámicas
---
// src/pages/blog/[slug].astro
import Layout from '../../layouts/BlogLayout.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const blogEntries = await getCollection('blog');
return blogEntries.map(entry => ({
params: { slug: entry.slug },
props: { entry }
}));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
const { title, description, publishDate, author, tags } = entry.data;
---
<Layout title={title} description={description}>
<article class="blog-post">
<header class="post-header">
<h1>{title}</h1>
<div class="post-meta">
<time datetime={publishDate.toISOString()}>
{publishDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
<span class="author">by {author}</span>
</div>
{tags && (
<div class="tags">
{tags.map(tag => (
<a href={`/blog/tag/${tag}`} class="tag">
#{tag}
</a>
))}
</div>
)}
</header>
<div class="post-content">
<Content />
</div>
</article>
</Layout>
<style>
.blog-post {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.post-header {
margin-bottom: 3rem;
text-align: center;
}
.post-meta {
color: #666;
margin: 1rem 0;
}
.author {
margin-left: 1rem;
}
.tags {
margin-top: 1rem;
}
.tag {
display: inline-block;
background: #f3f4f6;
color: #374151;
padding: 0.25rem 0.5rem;
border-radius: 4px;
text-decoration: none;
margin: 0 0.25rem;
font-size: 0.875rem;
}
.tag:hover {
background: #e5e7eb;
}
.post-content {
line-height: 1.8;
}
.post-content h2 {
margin: 2rem 0 1rem 0;
color: #1f2937;
}
.post-content p {
margin: 1rem 0;
}
.post-content pre {
background: #f9fafb;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}
</style>
Recorridos
---
// src/pages/docs/[...path].astro
import Layout from '../../layouts/DocsLayout.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const docs = await getCollection('docs');
return docs.map(doc => ({
params: { path: doc.slug },
props: { doc }
}));
}
const { doc } = Astro.props;
const { Content, headings } = await doc.render();
---
<Layout title={doc.data.title}>
<div class="docs-layout">
<aside class="sidebar">
<nav class="toc">
<h3>Table of Contents</h3>
<ul>
{headings.map(heading => (
<li class={`toc-${heading.depth}`}>
<a href={`#${heading.slug}`}>
{heading.text}
</a>
</li>
))}
</ul>
</nav>
</aside>
<main class="content">
<h1>{doc.data.title}</h1>
<Content />
</main>
</div>
</Layout>
<style>
.docs-layout {
display: grid;
grid-template-columns: 250px 1fr;
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.sidebar {
position: sticky;
top: 2rem;
height: fit-content;
}
.toc ul {
list-style: none;
padding: 0;
}
.toc li {
margin: 0.5rem 0;
}
.toc-2 {
padding-left: 1rem;
}
.toc-3 {
padding-left: 2rem;
}
.content {
min-width: 0;
}
</style>
Disposiciones
Base:
---
// src/layouts/Layout.astro
export interface Props {
title: string;
description?: string;
image?: string;
}
const { title, description = 'Default description', image = '/og-image.jpg' } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content={description} />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.url)} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />
<title>{title}</title>
</head>
<body>
<header class="site-header">
<nav class="nav">
<a href="/" class="logo">
<img src="/logo.svg" alt="Logo" />
</a>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
<button class="mobile-menu-toggle" aria-label="Toggle menu">
<span></span>
<span></span>
<span></span>
</button>
</nav>
</header>
<slot />
<footer class="site-footer">
<div class="footer-content">
<p>© 2024 My Website. All rights reserved.</p>
<div class="social-links">
<a href="https://twitter.com" aria-label="Twitter">Twitter</a>
<a href="https://github.com" aria-label="GitHub">GitHub</a>
</div>
</div>
</footer>
</body>
</html>
<style is:global>
html {
font-family: system-ui, sans-serif;
scroll-behavior: smooth;
}
body {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.site-header {
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: sticky;
top: 0;
z-index: 100;
}
.nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
max-width: 1200px;
margin: 0 auto;
}
.logo img {
height: 40px;
}
.nav-links {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 2rem;
}
.nav-links a {
text-decoration: none;
color: #374151;
font-weight: 500;
transition: color 0.2s;
}
.nav-links a:hover {
color: #3b82f6;
}
.mobile-menu-toggle {
display: none;
flex-direction: column;
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
}
.mobile-menu-toggle span {
width: 25px;
height: 3px;
background: #374151;
margin: 3px 0;
transition: 0.3s;
}
.site-footer {
background: #1f2937;
color: white;
padding: 2rem;
margin-top: auto;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.social-links {
display: flex;
gap: 1rem;
}
.social-links a {
color: white;
text-decoration: none;
}
@media (max-width: 768px) {
.nav-links {
display: none;
}
.mobile-menu-toggle {
display: flex;
}
.footer-content {
flex-direction: column;
gap: 1rem;
text-align: center;
}
}
</style>
<script>
// Mobile menu toggle
document.addEventListener('DOMContentLoaded', () => {
const toggle = document.querySelector('.mobile-menu-toggle');
const navLinks = document.querySelector('.nav-links');
toggle?.addEventListener('click', () => {
navLinks?.classList.toggle('active');
});
});
</script>
Blog Diseño
---
// src/layouts/BlogLayout.astro
import Layout from './Layout.astro';
export interface Props {
title: string;
description?: string;
publishDate?: Date;
author?: string;
image?: string;
}
const { title, description, publishDate, author, image } = Astro.props;
---
<Layout title={title} description={description} image={image}>
<main class="blog-main">
<div class="container">
<article class="blog-article">
<slot />
</article>
<aside class="blog-sidebar">
<div class="sidebar-section">
<h3>Recent Posts</h3>
<ul class="recent-posts">
<!-- Recent posts would be populated here -->
</ul>
</div>
<div class="sidebar-section">
<h3>Categories</h3>
<ul class="categories">
<li><a href="/blog/category/web-development">Web Development</a></li>
<li><a href="/blog/category/javascript">JavaScript</a></li>
<li><a href="/blog/category/css">CSS</a></li>
</ul>
</div>
<div class="sidebar-section">
<h3>Newsletter</h3>
<form class="newsletter-form">
<input type="email" placeholder="Your email" required />
<button type="submit">Subscribe</button>
</form>
</div>
</aside>
</div>
</main>
</Layout>
<style>
.blog-main {
padding: 2rem 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: grid;
grid-template-columns: 1fr 300px;
gap: 3rem;
}
.blog-article {
min-width: 0;
}
.blog-sidebar {
position: sticky;
top: 2rem;
height: fit-content;
}
.sidebar-section {
background: #f9fafb;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.sidebar-section h3 {
margin: 0 0 1rem 0;
color: #1f2937;
}
.recent-posts,
.categories {
list-style: none;
padding: 0;
margin: 0;
}
.recent-posts li,
.categories li {
margin: 0.5rem 0;
}
.recent-posts a,
.categories a {
color: #374151;
text-decoration: none;
}
.recent-posts a:hover,
.categories a:hover {
color: #3b82f6;
}
.newsletter-form {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.newsletter-form input {
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 4px;
}
.newsletter-form button {
background: #3b82f6;
color: white;
border: none;
padding: 0.5rem;
border-radius: 4px;
cursor: pointer;
}
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
gap: 2rem;
}
.blog-sidebar {
position: static;
}
}
</style>
Styling
Estilos globales
/* src/styles/global.css */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display;=swap');
:root {
--color-primary: #3b82f6;
--color-secondary: #64748b;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-text: #1f2937;
--color-text-light: #6b7280;
--color-background: #ffffff;
--color-surface: #f9fafb;
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
--border-radius: 0.375rem;
--border-radius-lg: 0.5rem;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
* {
box-sizing: border-box;
}
html {
font-family: 'Inter', system-ui, sans-serif;
scroll-behavior: smooth;
}
body {
margin: 0;
color: var(--color-text);
background-color: var(--color-background);
line-height: 1.6;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
margin: 0 0 var(--spacing-md) 0;
font-weight: 600;
line-height: 1.3;
}
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.5rem; }
h4 { font-size: 1.25rem; }
h5 { font-size: 1.125rem; }
h6 { font-size: 1rem; }
p {
margin: 0 0 var(--spacing-md) 0;
}
/* Links */
a {
color: var(--color-primary);
text-decoration: none;
transition: color 0.2s ease;
}
a:hover {
color: #2563eb;
text-decoration: underline;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: var(--border-radius);
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary {
background-color: var(--color-primary);
color: white;
}
.btn-primary:hover {
background-color: #2563eb;
text-decoration: none;
}
.btn-secondary {
background-color: var(--color-secondary);
color: white;
}
.btn-outline {
background-color: transparent;
color: var(--color-primary);
border: 1px solid var(--color-primary);
}
/* Utilities */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--spacing-md);
}
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: var(--spacing-xs); }
.mt-2 { margin-top: var(--spacing-sm); }
.mt-4 { margin-top: var(--spacing-md); }
.mt-6 { margin-top: var(--spacing-lg); }
.mt-8 { margin-top: var(--spacing-xl); }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: var(--spacing-xs); }
.mb-2 { margin-bottom: var(--spacing-sm); }
.mb-4 { margin-bottom: var(--spacing-md); }
.mb-6 { margin-bottom: var(--spacing-lg); }
.mb-8 { margin-bottom: var(--spacing-xl); }
/* Responsive */
@media (max-width: 768px) {
h1 { font-size: 2rem; }
h2 { font-size: 1.75rem; }
h3 { font-size: 1.25rem; }
.container {
padding: 0 var(--spacing-sm);
}
}
Estilos de componentes
---
// src/components/Hero.astro
export interface Props {
title: string;
subtitle: string;
backgroundImage?: string;
}
const { title, subtitle, backgroundImage } = Astro.props;
---
<section class="hero" style={backgroundImage ? `background-image: url(${backgroundImage})` : ''}>
<div class="hero-content">
<h1 class="hero-title">{title}</h1>
<p class="hero-subtitle">{subtitle}</p>
<div class="hero-actions">
<a href="/get-started" class="btn btn-primary">Get Started</a>
<a href="/learn-more" class="btn btn-outline">Learn More</a>
</div>
</div>
</section>
<style>
.hero {
position: relative;
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-size: cover;
background-position: center;
color: white;
text-align: center;
}
.hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 1;
}
.hero-content {
position: relative;
z-index: 2;
max-width: 800px;
padding: 2rem;
}
.hero-title {
font-size: 3.5rem;
font-weight: 700;
margin-bottom: 1rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.hero-subtitle {
font-size: 1.25rem;
margin-bottom: 2rem;
opacity: 0.9;
}
.hero-actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.hero-title {
font-size: 2.5rem;
}
.hero-subtitle {
font-size: 1.125rem;
}
.hero-actions {
flex-direction: column;
align-items: center;
}
}
</style>
Tailwind CSS Integración
# Install Tailwind CSS
npx astro add tailwind
---
// src/components/Card.astro with Tailwind
export interface Props {
title: string;
description: string;
image?: string;
}
const { title, description, image } = Astro.props;
---
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
{image && (
<img
src={image}
alt={title}
class="w-full h-48 object-cover"
/>
)}
<div class="p-6">
<h3 class="text-xl font-semibold text-gray-900 mb-2">{title}</h3>
<p class="text-gray-600 leading-relaxed">{description}</p>
<div class="mt-4">
<a
href="#"
class="inline-flex items-center text-blue-600 hover:text-blue-800 font-medium"
>
Read more
<svg class="ml-1 w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</a>
</div>
</div>
</div>
Colecciones de contenidos
Configuración
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
publishDate: z.date(),
author: z.string(),
image: z.string().optional(),
tags: z.array(z.string()).optional(),
featured: z.boolean().default(false),
draft: z.boolean().default(false)
})
});
const docs = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
category: z.string(),
order: z.number().optional(),
lastUpdated: z.date().optional()
})
});
const team = 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(),
linkedin: z.string().optional()
}).optional()
})
});
export const collections = {
blog,
docs,
team
};
Archivos de contenido
---
# src/content/blog/getting-started-with-astro.md
title: "Getting Started with Astro"
description: "Learn how to build fast, content-focused websites with Astro"
publishDate: 2024-01-15
author: "John Doe"
image: "/blog/astro-getting-started.jpg"
tags: ["astro", "web-development", "static-sites"]
featured: true
---
# Getting Started with Astro
Astro is a modern static site generator that delivers lightning-fast performance with a modern developer experience. In this guide, we'll explore how to get started with Astro and build your first website.
## What is Astro?
Astro is designed for building content-rich websites like blogs, marketing sites, and documentation. It uses a unique "Islands Architecture" that allows you to use your favorite UI framework while shipping minimal JavaScript to the browser.
## Key Features
- **Zero JavaScript by default**: Astro generates static HTML with no client-side JavaScript unless you explicitly opt-in
- **Framework agnostic**: Use React, Vue, Svelte, or any other framework
- **File-based routing**: Create pages by adding files to the `src/pages` directory
- **Content collections**: Organize and validate your content with TypeScript
## Installation
Getting started with Astro is simple:
```bash
npm crear astro@latest
cd mi proyecto
npm
npm run dev
Your First Component
Here's a simple Astro component:
-...
saludo const = "¡Hola, Astro!";
-...
"grado"
No.
> > > >
■/div titulada
♪ ♪ ♪ ♪
.
text-align: centro;
acolchado: 2rem;
}
> >
Conclusion
Astro provides an excellent developer experience while delivering exceptional performance. Its unique approach to JavaScript hydration makes it perfect for content-focused websites.
### Utilizando Colecciones
```astro
---
// src/pages/blog/index.astro
import Layout from '../../layouts/Layout.astro';
import Card from '../../components/Card.astro';
import { getCollection } from 'astro:content';
const allBlogPosts = await getCollection('blog', ({ data }) => {
return !data.draft;
});
// Sort by publish date
const sortedPosts = allBlogPosts.sort((a, b) =>
b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
);
// Get featured posts
const featuredPosts = sortedPosts.filter(post => post.data.featured);
---
<Layout title="Blog" description="Read our latest blog posts">
<main class="blog-index">
<div class="container">
<header class="page-header">
<h1>Blog</h1>
<p>Insights, tutorials, and updates from our team</p>
</header>
{featuredPosts.length > 0 && (
<section class="featured-posts">
<h2>Featured Posts</h2>
<div class="posts-grid">
{featuredPosts.map(post => (
<Card
title={post.data.title}
description={post.data.description}
href={`/blog/${post.slug}`}
image={post.data.image}
/>
))}
</div>
</section>
)}
<section class="all-posts">
<h2>All Posts</h2>
<div class="posts-grid">
{sortedPosts.map(post => (
<Card
title={post.data.title}
description={post.data.description}
href={`/blog/${post.slug}`}
image={post.data.image}
/>
))}
</div>
</section>
</div>
</main>
</Layout>
<style>
.blog-index {
padding: 2rem 0;
}
.page-header {
text-align: center;
margin-bottom: 3rem;
}
.page-header h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
.featured-posts,
.all-posts {
margin-bottom: 3rem;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
</style>
Integración
React Integration
# Add React integration
npx astro add react
// src/components/ReactCounter.jsx
import { useState } from 'react';
export default function ReactCounter({ initialCount = 0 }) {
const [count, setCount] = useState(initialCount);
return (
<div className="counter">
<h3>React Counter</h3>
<p>Count: {count}</p>
<div className="counter-buttons">
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
</div>
);
}
---
// Using React component in Astro
import Layout from '../layouts/Layout.astro';
import ReactCounter from '../components/ReactCounter.jsx';
---
<Layout title="Interactive Demo">
<main>
<h1>Interactive Components</h1>
<!-- Hydrate on page load -->
<ReactCounter client:load initialCount={5} />
<!-- Hydrate when visible -->
<ReactCounter client:visible initialCount={10} />
<!-- Hydrate on idle -->
<ReactCounter client:idle />
<!-- Hydrate on media query -->
<ReactCounter client:media="(max-width: 768px)" />
</main>
</Layout>
Vue Integration
# Add Vue integration
npx astro add vue
<!-- src/components/VueCounter.vue -->
<template>
<div class="counter">
<h3>Vue Counter</h3>
<p>Count: {{ count }}</p>
<div class="counter-buttons">
<button @click="decrement">-</button>
<button @click="increment">+</button>
<button @click="reset">Reset</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
initialCount: {
type: Number,
default: 0
}
});
const count = ref(props.initialCount);
const increment = () => count.value++;
const decrement = () => count.value--;
const reset = () => count.value = 0;
</script>
<style scoped>
.counter {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
.counter-buttons {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
button {
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
border-radius: 4px;
background: white;
cursor: pointer;
}
button:hover {
background: #f9fafb;
}
</style>
MDX Integración
# Add MDX integration
npx astro add mdx
---
# src/pages/interactive-post.mdx
title: "Interactive Blog Post"
description: "A blog post with interactive components"
---
import ReactCounter from '../components/ReactCounter.jsx';
import VueCounter from '../components/VueCounter.vue';
# Interactive Blog Post
This is a regular markdown paragraph. But we can also include interactive components:
<ReactCounter client:load initialCount={5} />
Here's another component with different hydration:
<VueCounter client:visible initialCount={10} />
## Code Examples
```javascript
función salud(nombre) {
retorno `Hello, ${name}!`;
}
And we can continue with regular markdown content...
## Islas Arquitectura
### Directivas del cliente
```astro
---
// src/pages/islands-demo.astro
import Layout from '../layouts/Layout.astro';
import ReactCounter from '../components/ReactCounter.jsx';
import VueChart from '../components/VueChart.vue';
import SvelteWidget from '../components/SvelteWidget.svelte';
---
<Layout title="Islands Architecture Demo">
<main>
<h1>Islands Architecture</h1>
<!-- Load immediately -->
<ReactCounter client:load />
<!-- Load when component becomes visible -->
<VueChart client:visible />
<!-- Load when browser is idle -->
<SvelteWidget client:idle />
<!-- Load only on mobile -->
<ReactCounter client:media="(max-width: 768px)" />
<!-- Load only on specific event -->
<VueChart client:only="vue" />
<!-- Never hydrate (static only) -->
<ReactCounter />
</main>
</Layout>
Optimización del rendimiento
---
// Optimize component loading
import { Image } from 'astro:assets';
import HeavyComponent from '../components/HeavyComponent.jsx';
---
<div class="optimized-page">
<!-- Optimized images -->
<Image
src="/hero.jpg"
alt="Hero image"
width={800}
height={600}
loading="eager"
/>
<!-- Lazy load heavy components -->
<HeavyComponent client:visible />
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
</div>
API Routes
Rutas básicas de API
// src/pages/api/posts.json.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
export const GET: APIRoute = async ({ params, request }) => {
const posts = await getCollection('blog');
return new Response(JSON.stringify(posts), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
};
export const POST: APIRoute = async ({ request }) => {
const data = await request.json();
// Process the data
console.log('Received:', data);
return new Response(JSON.stringify({ success: true }), {
status: 201,
headers: {
'Content-Type': 'application/json'
}
});
};
Rutas dinámicas de API
// src/pages/api/posts/[id].json.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
export const GET: APIRoute = async ({ params }) => {
const { id } = params;
if (!id) {
return new Response(JSON.stringify({ error: 'ID is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const posts = await getCollection('blog');
const post = posts.find(p => p.slug === id);
if (!post) {
return new Response(JSON.stringify({ error: 'Post not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify(post), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
};
Manejo de formularios
// src/pages/api/contact.ts
import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ request }) => {
try {
const formData = await request.formData();
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const message = formData.get('message') as string;
// Validate data
| if (!name | | !email | | !message) { |
return new Response(JSON.stringify({
error: 'All fields are required'
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Process form (send email, save to database, etc.)
await sendEmail({ name, email, message });
return new Response(JSON.stringify({
success: true,
message: 'Thank you for your message!'
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(JSON.stringify({
error: 'Internal server error'
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};
async function sendEmail(data: { name: string; email: string; message: string }) {
// Email sending logic
console.log('Sending email:', data);
}
```_
## Optimización de imagen
### Astro Assets
```astro
---
// src/components/OptimizedImages.astro
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
import profileImage from '../assets/profile.png';
---
<div class="image-gallery">
<!-- Local images with optimization -->
<Image
src={heroImage}
alt="Hero image"
width={800}
height={600}
loading="eager"
class="hero-image"
/>
<!-- Responsive images -->
<Image
src={profileImage}
alt="Profile"
widths={[240, 540, 720]}
sizes="(max-width: 360px) 240px, (max-width: 720px) 540px, 720px"
class="profile-image"
/>
<!-- Remote images -->
<Image
src="https://example.com/image.jpg"
alt="Remote image"
width={400}
height={300}
loading="lazy"
/>
<!-- Background images -->
<div class="hero-section">
<Image
src={heroImage}
alt=""
width={1200}
height={600}
class="background-image"
/>
<div class="hero-content">
<h1>Hero Title</h1>
<p>Hero description</p>
</div>
</div>
</div>
<style>
.image-gallery {
display: grid;
gap: 2rem;
padding: 2rem;
}
.hero-image {
width: 100%;
height: auto;
border-radius: 8px;
}
.profile-image {
width: 200px;
height: 200px;
border-radius: 50%;
object-fit: cover;
}
.hero-section {
position: relative;
height: 400px;
border-radius: 8px;
overflow: hidden;
}
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.hero-content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
color: white;
text-align: center;
background: rgba(0, 0, 0, 0.4);
}
</style>
Procesamiento de imagen
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
image: {
service: {
entrypoint: 'astro/assets/services/sharp'
}
},
vite: {
optimizeDeps: {
include: ['sharp']
}
}
});
Despliegue
Despliegue estatico
# Build static site
npm run build
# Output will be in dist/ directory
# Deploy to any static hosting service
Despliegue de Vercel
// vercel.json
{
"framework": "astro",
"buildCommand": "npm run build",
"devCommand": "npm run dev",
"installCommand": "npm install"
}
Netlify Deployment
# netlify.toml
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "18"
[[redirects]]
from = "/*"
to = "/404.html"
status = 404
```_
### Docker Deployment
```dockerfile
# Dockerfile
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Server-Side Rendering
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone'
})
});
Ejecución
Bundle Analysis
# Analyze bundle size
npm run build -- --analyze
# Check lighthouse scores
npx lighthouse http://localhost:3000
Técnicas de optimización
---
// Performance optimizations
import { Image } from 'astro:assets';
---
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
<!-- Optimize images -->
<Image
src="/hero.jpg"
alt="Hero"
width={800}
height={600}
loading="eager"
decoding="async"
/>
<!-- Lazy load non-critical components -->
<HeavyComponent client:visible />
<!-- Minimize JavaScript -->
<script>
// Only essential JavaScript
console.log('Page loaded');
</script>
Pruebas
Pruebas de unidad
# Install testing dependencies
npm install --save-dev vitest @astrojs/test-utils
// tests/components/Card.test.ts
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import Card from '../src/components/Card.astro';
test('Card component renders correctly', async () => {
const container = await AstroContainer.create();
const result = await container.renderToString(Card, {
props: {
title: 'Test Card',
description: 'Test description'
}
});
expect(result).toContain('Test Card');
expect(result).toContain('Test description');
});
E2E Pruebas
# Install Playwright
npm install --save-dev @playwright/test
// tests/e2e/homepage.spec.ts
import { test, expect } from '@playwright/test';
test('homepage loads correctly', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/My Astro Site/);
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});
test('navigation works', async ({ page }) => {
await page.goto('/');
await page.getByRole('link', { name: 'About' }).click();
await expect(page).toHaveURL('/about');
});
Solución de problemas
Cuestiones comunes
Errores de construcción
# Clear cache and rebuild
rm -rf node_modules/.astro dist
npm install
npm run build
# Check for TypeScript errors
npx astro check
Cuestiones de hidratación
<!-- Fix hydration mismatches -->
<div class="client-only">
<ClientComponent client:only="react" />
</div>
<!-- Use proper client directives -->
<InteractiveComponent client:load />
<LazyComponent client:visible />
Cuestiones de ejecución
---
// Optimize component loading
---
<!-- Use appropriate client directives -->
<HeavyComponent client:idle />
<!-- Optimize images -->
<Image src="/large-image.jpg" width={800} height={600} loading="lazy" />
<!-- Minimize JavaScript -->
<script is:inline>
// Inline only critical scripts
</script>
Buenas prácticas
Project Organization
# Recommended structure
src/
├── components/
│ ├── ui/ # Reusable UI components
│ ├── layout/ # Layout components
│ └── content/ # Content-specific components
├── layouts/ # Page layouts
├── pages/ # Routes
├── content/ # Content collections
├── assets/ # Images, fonts, etc.
├── styles/ # Global styles
└── utils/ # Utility functions
Prácticas óptimas de rendimiento
---
// Optimize for performance
import { Image } from 'astro:assets';
---
<!-- Use appropriate loading strategies -->
<Image src="/hero.jpg" alt="Hero" loading="eager" />
<Image src="/gallery.jpg" alt="Gallery" loading="lazy" />
<!-- Minimize client-side JavaScript -->
<InteractiveComponent client:visible />
<!-- Preload critical resources -->
<link rel="preload" href="/critical.css" as="style" />
SEO Buenas prácticas
---
// SEO optimization
const { title, description, image } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<head>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<!-- Structured data -->
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "WebPage",
"name": title,
"description": description
})}
</script>
</head>
-...
Resumen
Astro es un potente generador de sitios estáticos que se destaca en la construcción de sitios web basados en contenidos con un rendimiento excepcional. Las características principales incluyen:
- Islands Architecture: Ship minimal JavaScript with selective hidrataation
- Framework Agnostic: Use React, Vue, Svelte o cualquier marco
- ** Colecciones de contenido**: Gestión de contenidos de tipo seguro con validación
- Rotación basada en archivos: Enrutamiento intuitivo basado en la estructura de archivos
- ** Optimización de imagen**: Procesamiento y optimización de imágenes incorporadas
- Performance First: Zero JavaScript por defecto con interactividad opt-in
Para obtener resultados óptimos, apalanque la generación estática para páginas de contenido, utilice las directivas del cliente con sensatez para interactividad, optimice las imágenes con las herramientas incorporadas de Astro y siga los patrones de colección de contenidos para la gestión de contenidos escalable.
" copia de la funciónToClipboard() {} comandos const = document.querySelectorAll('code'); que todos losCommands = '; comandos. paraCada(cmd = confianza allCommands += cmd.textContent + '\n'); navigator.clipboard.writeText(allCommands); alerta ('Todos los comandos copiados a portapapeles!'); }
función generaPDF() { ventana.print(); } ■/script título