Pular para o conteúdo

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

DiretivaDescriçãoCaso de Uso
client:loadHidrata imediatamente durante o carregamento da páginaComponentes críticos que precisam ser interativos imediatamente
client:idleHidrata assim que o navegador fica ociosoComponentes importantes, mas não críticos
client:visibleHidrata quando o componente entra na viewportComponentes abaixo da dobra ou em tabs
client:media="(query)"Hidrata apenas se a media query corresponderComponentes específicos para certos dispositivos
client:only="framework"Renderiza apenas no cliente, sem SSRComponentes que dependem de APIs do navegador

Implementação de Islands

1. Componente Interativo em React/Preact

src/components/interactive/Counter.jsx
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

src/pages/interactive-demo.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:

src/components/interactive/VanillaCounter.astro
---
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

src/content/config.ts
import { defineCollection, z } from 'astro:content';
// Esquema para posts do blog
const 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 produtos
const 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 collections
export 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.ts

3. Exemplo de Conteúdo Markdown

---
title: Meu Primeiro Post
description: Uma introdução ao nosso blog
publishDate: 2025-01-15
author: João Silva
image: /images/posts/primeiro-post.jpg
categories: [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 blog
const 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ção
const 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 slug
const post = await getEntry('blog', 'primeiro-post');
// Renderizar o conteúdo Markdown
const { 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

src/pages/blog/[...slug].astro
---
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';
// Gera rotas para todos os posts do blog
export 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 prop
const { 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

src/components/ProductCard.astro
---
// Define as props com tipos e valores padrão
interface 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 moeda
const 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

src/pages/produtos.astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
import ProductCard from '../components/ProductCard.astro';
import { getCollection } from 'astro:content';
// Busca produtos da collection
const 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

src/components/layout/Section.astro
---
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 Section
import 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:

astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'server', // Habilita SSR para todo o site
});
src/pages/dynamic.astro
---
// Esta página será renderizada a cada requisição
// Dados que mudam a cada requisição
const 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:

astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'hybrid', // Permite escolher por página
});
src/pages/hybrid-example.astro
---
// Define se esta página específica será renderizada no servidor
export const prerender = false;
// Dados dinâmicos
const 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:

src/pages/api/data.json.js
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',
},
}
);
}
src/pages/api/contact.js
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:

src/layouts/BaseLayout.astro
---
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

src/pages/produtos/[id].astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { Image } from 'astro:assets';
import { getCollection, getEntry } from 'astro:content';
// Gera rotas para todos os produtos
export 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:


Última atualização: 21 de março de 2025