6. Integração com Astro
Este documento detalha como aproveitar os recursos específicos do Astro 5 para criar soluções web eficientes, focando em suas características principais como Islands, Content Collections, gerenciamento de props e opções de renderização.
Astro Islands e Hidratação Parcial
O Astro introduz o conceito de “Islands” (Ilhas) de interatividade em um mar de HTML estático, permitindo uma abordagem mais eficiente para adicionar interatividade.
Conceito de Islands
As Islands são componentes interativos isolados que são hidratados individualmente, enquanto o resto da página permanece como HTML estático. Isso resulta em:
- Melhor performance: Apenas o JavaScript necessário é enviado ao navegador
- Carregamento mais rápido: O HTML estático é renderizado imediatamente
- Menor uso de recursos: Menos JavaScript significa menos processamento no cliente
Diretivas de Cliente
O Astro oferece várias diretivas de cliente para controlar quando e como os componentes são hidratados:
---import InteractiveCounter from '../components/InteractiveCounter.jsx';import HeavyComponent from '../components/HeavyComponent.jsx';import CommentSection from '../components/CommentSection.jsx';import LazyWidget from '../components/LazyWidget.jsx';---
<!-- Hidrata o componente no carregamento da página --><InteractiveCounter client:load />
<!-- Hidrata o componente após o carregamento da página (prioridade menor) --><HeavyComponent client:idle />
<!-- Hidrata o componente apenas quando se torna visível na viewport --><CommentSection client:visible />
<!-- Hidrata o componente apenas em dispositivos desktop --><LazyWidget client:media="(min-width: 1024px)" />
<!-- Não hidrata o componente (renderiza apenas HTML estático) --><InteractiveCounter />Diretivas Disponíveis
| Diretiva | Descrição | Caso de Uso |
|---|---|---|
client:load | Hidrata imediatamente durante o carregamento da página | Componentes críticos que precisam ser interativos imediatamente |
client:idle | Hidrata assim que o navegador fica ocioso | Componentes importantes, mas não críticos |
client:visible | Hidrata quando o componente entra na viewport | Componentes abaixo da dobra ou em tabs |
client:media="(query)" | Hidrata apenas se a media query corresponder | Componentes específicos para certos dispositivos |
client:only="framework" | Renderiza apenas no cliente, sem SSR | Componentes que dependem de APIs do navegador |
Implementação de Islands
1. Componente Interativo em React/Preact
import { useState } from 'react';
export default function Counter({ initialCount = 0 }) { const [count, setCount] = useState(initialCount);
return ( <div className="p-4 bg-white rounded-lg shadow"> <p className="text-lg font-medium mb-2">Contador: {count}</p> <div className="flex space-x-2"> <button onClick={() => setCount(count - 1)} className="px-3 py-1 bg-red-100 text-red-700 rounded hover:bg-red-200" > Diminuir </button> <button onClick={() => setCount(count + 1)} className="px-3 py-1 bg-green-100 text-green-700 rounded hover:bg-green-200" > Aumentar </button> </div> </div> );}2. Uso em uma Página Astro
---import BaseLayout from '../layouts/BaseLayout.astro';import Counter from '../components/interactive/Counter.jsx';import { Container } from '../components/layout/Container.astro';---
<BaseLayout title="Demonstração de Islands"> <Container> <h1 class="text-3xl font-bold mb-8">Demonstração de Astro Islands</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8"> <div> <h2 class="text-xl font-semibold mb-4">Contador com Hidratação Imediata</h2> <Counter client:load initialCount={5} /> <p class="mt-2 text-sm text-gray-600"> Este contador é hidratado imediatamente durante o carregamento da página. </p> </div>
<div> <h2 class="text-xl font-semibold mb-4">Contador com Hidratação Lazy</h2> <Counter client:visible initialCount={10} /> <p class="mt-2 text-sm text-gray-600"> Este contador é hidratado apenas quando se torna visível na viewport. </p> </div> </div> </Container></BaseLayout>Vanilla JavaScript com Islands
Você também pode usar JavaScript vanilla em componentes Astro com hidratação parcial:
---const { initialCount = 0 } = Astro.props;---
<div class="counter p-4 bg-white rounded-lg shadow" data-initial-count={initialCount}> <p class="text-lg font-medium mb-2">Contador: <span class="count">{initialCount}</span></p> <div class="flex space-x-2"> <button class="decrease px-3 py-1 bg-red-100 text-red-700 rounded hover:bg-red-200" > Diminuir </button> <button class="increase px-3 py-1 bg-green-100 text-green-700 rounded hover:bg-green-200" > Aumentar </button> </div></div>
<script> // Este script será executado apenas quando o componente for hidratado function setupCounter(counterEl) { const initialCount = parseInt(counterEl.dataset.initialCount || '0'); const countEl = counterEl.querySelector('.count'); const decreaseBtn = counterEl.querySelector('.decrease'); const increaseBtn = counterEl.querySelector('.increase');
let count = initialCount;
function updateCount() { countEl.textContent = count; }
decreaseBtn.addEventListener('click', () => { count--; updateCount(); });
increaseBtn.addEventListener('click', () => { count++; updateCount(); }); }
// Inicializa todos os contadores na página document.querySelectorAll('.counter').forEach(setupCounter);</script>Content Collections
As Content Collections do Astro 5 fornecem uma maneira poderosa de gerenciar conteúdo estruturado com validação de esquema.
Configuração de Collections
1. Definir o Esquema
import { defineCollection, z } from 'astro:content';
// Esquema para posts do blogconst blogCollection = defineCollection({ type: 'content', // Conteúdo baseado em Markdown/MDX schema: z.object({ title: z.string(), description: z.string(), publishDate: z.date(), author: z.string(), image: z.string().optional(), categories: z.array(z.string()).default([]), featured: z.boolean().default(false), draft: z.boolean().default(false), }),});
// Esquema para produtosconst productsCollection = defineCollection({ type: 'data', // Conteúdo baseado em JSON/YAML schema: z.object({ name: z.string(), price: z.number().positive(), description: z.string(), images: z.array(z.string()), categories: z.array(z.string()), features: z.array(z.string()), inStock: z.boolean().default(true), rating: z.number().min(0).max(5).optional(), }),});
// Exporta as collectionsexport const collections = { 'blog': blogCollection, 'products': productsCollection,};2. Estrutura de Arquivos
src/content/├── blog/│ ├── primeiro-post.md│ ├── segundo-post.md│ └── ...├── products/│ ├── produto-1.json│ ├── produto-2.json│ └── ...└── config.ts3. Exemplo de Conteúdo Markdown
---title: Meu Primeiro Postdescription: Uma introdução ao nosso blogpublishDate: 2025-01-15author: João Silvaimage: /images/posts/primeiro-post.jpgcategories: [tecnologia, tutorial]featured: true---
# Meu Primeiro Post
Este é o conteúdo do post escrito em Markdown.
## Subtítulo
Mais conteúdo aqui...Consultando Collections
Obter Todos os Itens de uma Collection
---import { getCollection } from 'astro:content';
// Obter todos os posts do blogconst allPosts = await getCollection('blog');
// Obter apenas posts publicados (não rascunhos)const publishedPosts = await getCollection('blog', ({ data }) => { return !data.draft && data.publishDate <= new Date();});
// Ordenar por data de publicaçãoconst sortedPosts = publishedPosts.sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime());---Obter um Item Específico
---import { getEntry } from 'astro:content';
// Obter um post específico pelo slugconst post = await getEntry('blog', 'primeiro-post');
// Renderizar o conteúdo Markdownconst { Content } = await post.render();---
<article> <h1>{post.data.title}</h1> <time datetime={post.data.publishDate.toISOString()}> {post.data.publishDate.toLocaleDateString()} </time> <Content /></article>Rotas Dinâmicas com Collections
---import { getCollection } from 'astro:content';import BlogLayout from '../../layouts/BlogLayout.astro';
// Gera rotas para todos os posts do blogexport async function getStaticPaths() { const posts = await getCollection('blog', ({ data }) => { return import.meta.env.PROD ? !data.draft : true; });
return posts.map(post => ({ params: { slug: post.slug }, props: { post }, }));}
// Recebe o post como propconst { post } = Astro.props;const { Content } = await post.render();---
<BlogLayout title={post.data.title} description={post.data.description} image={post.data.image}> <article> <h1 class="text-4xl font-bold mb-4">{post.data.title}</h1> <div class="flex items-center mb-8"> <span class="text-gray-600"> {post.data.publishDate.toLocaleDateString()} </span> <span class="mx-2">•</span> <span class="text-gray-600"> {post.data.author} </span> </div>
{post.data.image && ( <img src={post.data.image} alt={post.data.title} class="w-full h-64 object-cover rounded-lg mb-8" /> )}
<div class="prose prose-lg max-w-none"> <Content /> </div> </article></BlogLayout>Astro.props e Gerenciamento de Dados
O Astro oferece várias maneiras de passar e gerenciar dados entre componentes.
Props em Componentes Astro
---// Define as props com tipos e valores padrãointerface Props { name: string; price: number; description: string; image: string; inStock?: boolean; rating?: number;}
const { name, price, description, image, inStock = true, rating} = Astro.props;
// Formata o preço como moedaconst formattedPrice = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL'}).format(price);---
<div class="bg-white rounded-lg shadow-md overflow-hidden"> <img src={image} alt={name} class="w-full h-48 object-cover" />
<div class="p-4"> <h3 class="text-xl font-semibold mb-1">{name}</h3>
{rating && ( <div class="flex items-center mb-2"> {Array.from({ length: 5 }).map((_, i) => ( <svg class={`w-4 h-4 ${i < rating ? 'text-yellow-400' : 'text-gray-300'}`} fill="currentColor" viewBox="0 0 20 20" > <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118l-2.8-2.034c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" /> </svg> ))} </div> )}
<p class="text-gray-600 mb-2">{description}</p>
<div class="flex items-center justify-between"> <span class="text-xl font-bold text-gray-900">{formattedPrice}</span>
{inStock ? ( <span class="text-sm text-green-600 font-medium">Em estoque</span> ) : ( <span class="text-sm text-red-600 font-medium">Indisponível</span> )} </div> </div></div>Passando Props para Componentes
---import BaseLayout from '../layouts/BaseLayout.astro';import ProductCard from '../components/ProductCard.astro';import { getCollection } from 'astro:content';
// Busca produtos da collectionconst products = await getCollection('products');---
<BaseLayout title="Produtos"> <div class="container mx-auto py-8"> <h1 class="text-3xl font-bold mb-8">Nossos Produtos</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {products.map(product => ( <ProductCard name={product.data.name} price={product.data.price} description={product.data.description} image={product.data.images[0]} inStock={product.data.inStock} rating={product.data.rating} /> ))} </div> </div></BaseLayout>Slots para Composição de Componentes
---const { title, subtitle, background = 'white', spacing = 'default', class: className, ...props} = Astro.props;
const backgroundClasses = { white: 'bg-white', light: 'bg-gray-50', dark: 'bg-gray-900 text-white', primary: 'bg-primary-50',};
const spacingClasses = { small: 'py-4 md:py-6', default: 'py-8 md:py-12', large: 'py-12 md:py-16',};
const classes = `${backgroundClasses[background]} ${spacingClasses[spacing]} ${className || ''}`;---
<section class={classes} {...props}> <div class="container mx-auto px-4"> {title && ( <div class="text-center mb-8 md:mb-12"> <h2 class="text-3xl font-bold">{title}</h2> {subtitle && <p class="mt-2 text-xl text-gray-600">{subtitle}</p>} </div> )}
<slot name="header" />
<div> <slot /> </div>
<slot name="footer" /> </div></section>---// Uso do componente Sectionimport Section from '../components/layout/Section.astro';import { Button } from '../components/ui/Button.astro';---
<Section title="Nossos Serviços" subtitle="Soluções completas para seu negócio" background="light" spacing="large"> <div class="grid grid-cols-1 md:grid-cols-3 gap-8"> <!-- Conteúdo da seção --> </div>
<div slot="footer" class="mt-8 text-center"> <Button variant="primary" size="lg">Ver todos os serviços</Button> </div></Section>Server-side Rendering e Renderização Estática
O Astro 5 suporta diferentes modos de renderização para otimizar a performance e a experiência do usuário.
Modos de Renderização
1. Renderização Estática (SSG)
Por padrão, o Astro gera sites estáticos durante o build, resultando em HTML puro que pode ser servido de CDNs.
---// Esta página será renderizada estaticamente durante o build---
<html> <head> <title>Página Estática</title> </head> <body> <h1>Conteúdo Estático</h1> <p>Esta página foi gerada durante o build.</p> <p>Timestamp do build: {new Date().toLocaleString()}</p> </body></html>2. Renderização no Servidor (SSR)
Para conteúdo dinâmico que precisa ser renderizado a cada requisição:
import { defineConfig } from 'astro/config';
export default defineConfig({ output: 'server', // Habilita SSR para todo o site});---// Esta página será renderizada a cada requisição
// Dados que mudam a cada requisiçãoconst currentTime = new Date().toLocaleString();const randomNumber = Math.floor(Math.random() * 100);
// Dados do usuário (exemplo)const userAgent = Astro.request.headers.get('user-agent');---
<html> <head> <title>Página Dinâmica</title> </head> <body> <h1>Conteúdo Dinâmico</h1> <p>Esta página é renderizada a cada requisição.</p> <p>Horário atual: {currentTime}</p> <p>Número aleatório: {randomNumber}</p> <p>Seu navegador: {userAgent}</p> </body></html>3. Modo Híbrido
O Astro 5 permite escolher o modo de renderização por página:
import { defineConfig } from 'astro/config';
export default defineConfig({ output: 'hybrid', // Permite escolher por página});---// Define se esta página específica será renderizada no servidorexport const prerender = false;
// Dados dinâmicosconst currentTime = new Date().toLocaleString();---
<html> <head> <title>Exemplo Híbrido</title> </head> <body> <h1>Renderização Híbrida</h1> <p>Esta página é renderizada no servidor.</p> <p>Horário atual: {currentTime}</p> </body></html>Endpoints de API
Com SSR habilitado, você pode criar endpoints de API:
export async function GET() { // Buscar dados de uma fonte externa const data = await fetchSomeData();
// Retornar como JSON return new Response( JSON.stringify({ success: true, data, timestamp: new Date().toISOString(), }), { status: 200, headers: { 'Content-Type': 'application/json', }, } );}export async function POST({ request }) { try { // Obter dados do formulário const formData = await request.formData(); const name = formData.get('name'); const email = formData.get('email'); const message = formData.get('message');
// Validação básica if (!name || !email || !message) { return new Response( JSON.stringify({ success: false, message: 'Todos os campos são obrigatórios', }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); }
// Processar os dados (enviar email, salvar no banco, etc.) await processContactForm({ name, email, message });
// Resposta de sucesso return new Response( JSON.stringify({ success: true, message: 'Mensagem enviada com sucesso!', }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); } catch (error) { console.error('Erro ao processar formulário:', error);
// Resposta de erro return new Response( JSON.stringify({ success: false, message: 'Erro ao processar sua solicitação', }), { status: 500, headers: { 'Content-Type': 'application/json' } } ); }}View Transitions
O Astro 5 introduz suporte nativo para View Transitions API, permitindo transições suaves entre páginas:
---import { ViewTransitions } from 'astro:transitions';---
<html lang="pt-BR"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>{Astro.props.title || 'Site Default'}</title> <ViewTransitions /> </head> <body> <slot /> </body></html>Transições Personalizadas
---import BaseLayout from '../../layouts/BaseLayout.astro';import { Image } from 'astro:assets';import { getCollection, getEntry } from 'astro:content';
// Gera rotas para todos os produtosexport async function getStaticPaths() { const products = await getCollection('products'); return products.map(product => ({ params: { id: product.id }, props: { product }, }));}
const { product } = Astro.props;---
<BaseLayout title={product.data.name}> <div class="container mx-auto py-8"> <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> <div> <Image src={product.data.images[0]} alt={product.data.name} width={600} height={400} class="rounded-lg" transition:name={`product-image-${product.id}`} /> </div>
<div> <h1 class="text-3xl font-bold mb-4" transition:name={`product-title-${product.id}`} > {product.data.name} </h1>
<p class="text-2xl font-bold text-primary-600 mb-4" transition:name={`product-price-${product.id}`} > {new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(product.data.price)} </p>
<div class="prose max-w-none"> <p>{product.data.description}</p> </div> </div> </div> </div></BaseLayout>Conclusão
A integração eficiente com os recursos do Astro 5 permite criar soluções web modernas, performáticas e fáceis de manter. Ao aproveitar Islands, Content Collections, gerenciamento de props e opções de renderização, você pode construir sites que oferecem excelente experiência tanto para desenvolvedores quanto para usuários.
Os próximos documentos detalharão outros aspectos do desenvolvimento:
- Otimizações de Performance: Estratégias para melhorar a performance
- Integração com Backends: Configuração de backends e APIs
- Ferramentas de Desenvolvimento: Setup do ambiente de desenvolvimento
Última atualização: 21 de março de 2025