O que é Astro
Astro é um framework web com foco em sites com conteúdo — blogs, portfólios, documentações, marketings sites. A filosofia central é zero JS por padrão: a página entrega HTML puro ao navegador, sem bundle de JavaScript, a menos que você explicitamente peça.
O que torna o Astro único é a Islands Architecture: a maior parte da página é HTML estático e rápido; ilhas isoladas de interatividade (um carrossel, um menu dropdown, um contador) são hidratadas com JS somente quando necessário. Isso inverte o modelo de SPAs como Next.js, onde tudo é JS e você opta por sair do JS — no Astro você parte do zero JS e opta por entrar nele.
Astro suporta React, Vue, Svelte, Solid, Lit e outros UI frameworks ao mesmo tempo, em qualquer combinação. Você pode ter um componente React e um Vue na mesma página.
Estrutura do Projeto
/
├── public/ # Arquivos estáticos servidos diretamente (favicon, robots.txt)
├── src/
│ ├── assets/ # Imagens, fontes — processadas pelo pipeline Vite
│ ├── components/ # Componentes reutilizáveis (.astro, .jsx, .vue…)
│ ├── content/ # Content collections (Markdown, MDX, JSON, YAML)
│ │ └── content.config.ts # Schema das collections
│ ├── layouts/ # Layouts reutilizáveis de página
│ ├── pages/ # Rotas — cada arquivo vira uma URL
│ │ ├── index.astro
│ │ ├── sobre.astro
│ │ └── posts/
│ │ └── [slug].astro # Rota dinâmica
│ └── middleware.ts # Middleware global (auth, headers, redirect)
├── astro.config.mjs # Configuração central do Astro
└── tsconfig.jsonRegra principal: src/pages/ é mapeado diretamente para URLs. src/pages/sobre.astro → /sobre. src/pages/posts/[slug].astro → /posts/qualquer-slug.
Componentes .astro
Um componente Astro tem duas partes: o frontmatter (script TypeScript entre ---) e o template (HTML com expressões JSX-like abaixo). O frontmatter roda no servidor em build time (ou request time no SSR) — nunca no navegador.
---
// src/components/Card.astro
// Frontmatter — roda no servidor, nunca no browser
import OutroComponente from './OutroComponente.astro';
import type { Post } from '../types';
interface Props {
title: string;
description?: string;
post: Post;
}
const { title, description = 'Sem descrição', post } = Astro.props;
const dataFormatada = new Date(post.date).toLocaleDateString('pt-BR');
---
<!-- Template — HTML + expressões JavaScript entre {} -->
<article class="card">
<h2>{title}</h2>
<p>{description}</p>
<time datetime={post.date}>{dataFormatada}</time>
<!-- Renderização condicional -->
{post.featured && <span class="badge">Destaque</span>}
<!-- Loops -->
<ul>
{post.tags.map(tag => <li class="tag">{tag}</li>)}
</ul>
<!-- Componentes filhos -->
<OutroComponente />
</article>
<style>
/* Estilos com escopo automático — não vazam para fora */
.card { border: 1px solid #eee; padding: 1rem; }
.badge { background: gold; }
</style>Props com TypeScript
A forma idiomática é definir interface Props no frontmatter — o Astro infere automaticamente os tipos de Astro.props.
---
interface Props {
variant: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}
const { variant, size = 'md', disabled = false } = Astro.props;
---
<button
class:list={['btn', `btn-${variant}`, `btn-${size}`, { disabled }]}
{disabled}
>
<slot />
</button>Slots
Slots permitem que o componente pai injete conteúdo HTML dentro do filho. O slot padrão é <slot />. Slots nomeados recebem conteúdo específico.
---
// src/layouts/BaseLayout.astro
const { title } = Astro.props;
---
<html lang="pt-BR">
<head>
<title>{title}</title>
<!-- Slot nomeado para meta tags extras -->
<slot name="head" />
</head>
<body>
<header>...</header>
<main>
<!-- Slot padrão: recebe tudo que não tem slot="" -->
<slot />
</main>
<footer>...</footer>
</body>
</html>---
// src/pages/index.astro — usando o layout
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Home">
<!-- Vai para <slot name="head" /> -->
<meta slot="head" name="description" content="Minha página" />
<!-- Vai para o <slot /> padrão -->
<h1>Olá mundo</h1>
<p>Conteúdo da página</p>
</BaseLayout>class:list e outras diretivas de template
---
const isActive = true;
const classes = ['base', { active: isActive, disabled: false }];
---
<!-- class:list — combina strings, arrays e objetos -->
<div class:list={classes}>...</div>
<!-- Resultado: class="base active" -->
<!-- set:html — injeta HTML raw (cuidado com XSS) -->
<div set:html={conteudoMarkdownRenderizado} />
<!-- set:text — injeta texto escapado -->
<p set:text={textoDoUsuario} />Client Directives — Islands Architecture
Por padrão, componentes de UI frameworks (React, Vue, Svelte) renderizam apenas no servidor como HTML estático — sem JavaScript no browser. Para hidratar (tornar interativo), use uma client:* directive.
---
import BotaoContador from '../components/BotaoContador.jsx'; // React
import MenuDropdown from '../components/MenuDropdown.vue'; // Vue
import Modal from '../components/Modal.svelte'; // Svelte
---
<!-- Hidrata imediatamente quando a página carrega -->
<BotaoContador client:load />
<!-- Hidrata quando o componente fica visível (Intersection Observer) -->
<!-- Ideal para conteúdo abaixo do fold — melhor LCP/TBT -->
<MenuDropdown client:visible />
<!-- Hidrata quando o browser está ocioso (requestIdleCallback) -->
<!-- Para componentes não críticos -->
<Modal client:idle />
<!-- Hidrata somente em um breakpoint de mídia -->
<Sidebar client:media="(max-width: 768px)" />
<!-- Só renderiza no cliente — sem SSR, sem HTML inicial -->
<!-- Use para componentes que dependem de APIs do browser (window, localStorage) -->
<Mapa client:only="react" />| Diretiva | Quando hidrata | Uso ideal |
|---|---|---|
client:load | Imediatamente | Interatividade crítica above-the-fold |
client:idle | Browser ocioso | Componentes não essenciais |
client:visible | Ao entrar na viewport | Conteúdo abaixo do fold |
client:media | Condição de mídia | Componentes só para mobile/desktop |
client:only | Só no cliente (sem SSR) | Depende de window/document |
Por que isso importa: Um site com 5 ilhas interativas e
client:visibleenvia JS apenas para as ilhas que o usuário realmente viu. Uma SPA tradicional envia tudo de uma vez. A diferença em bundle size e TTI pode ser brutal.
Páginas e Roteamento
Rotas estáticas
Qualquer arquivo em src/pages/ vira uma rota. Extensões suportadas: .astro, .md, .mdx, .html, .ts, .js.
src/pages/index.astro → /
src/pages/sobre.astro → /sobre
src/pages/blog/index.astro → /blog
src/pages/blog/primeiro.md → /blog/primeiroRotas dinâmicas (SSG)
Use colchetes no nome do arquivo para criar parâmetros dinâmicos. Em modo estático, você precisa exportar getStaticPaths() para que o Astro saiba quais URLs gerar em build time.
---
// src/pages/posts/[slug].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.id },
props: { post }, // passa o post como prop para a página
}));
}
const { slug } = Astro.params;
const { post } = Astro.props;
const { Content } = await post.render();
---
<h1>{post.data.title}</h1>
<Content />Rest params e rotas catch-all
src/pages/docs/[...path].astro → /docs/qualquer/coisa/aqui---
const { path } = Astro.params; // "qualquer/coisa/aqui"
---Prioridade de rotas
- Rotas estáticas (
/sobre.astro) têm prioridade sobre dinâmicas (/[slug].astro) - Rotas com mais segmentos têm prioridade:
/blog/2024/post>/blog/[slug] - Rest params (
[...path]) têm menor prioridade
Layouts
Layouts são componentes comuns — apenas por convenção ficam em src/layouts/. O padrão para páginas Markdown é passar o layout no frontmatter.
---
// src/layouts/PostLayout.astro
import BaseLayout from './BaseLayout.astro';
const { frontmatter } = Astro.props; // para páginas .md, o frontmatter vem aqui
---
<BaseLayout title={frontmatter.title}>
<article>
<h1>{frontmatter.title}</h1>
<time>{frontmatter.date}</time>
<slot /> <!-- conteúdo do .md renderizado aqui -->
</article>
</BaseLayout>---
layout: ../../layouts/PostLayout.astro
title: Meu primeiro post
date: 2024-01-15
---
# Conteúdo do post em Markdown
O layout acima envolve este conteúdo automaticamente.Content Collections
Content Collections são a forma estruturada de gerenciar conteúdo no Astro — arquivos Markdown, MDX, JSON ou YAML com schema validado pelo Zod. Ao contrário de import.meta.glob, elas oferecem type safety completo.
Definindo a collection (src/content.config.ts)
import { defineCollection } from 'astro:content';
import { glob, file } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(), // string ISO → Date automático
updatedDate: z.coerce.date().optional(),
cover: z.string().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
const autores = defineCollection({
loader: file('src/data/autores.json'), // JSON único com array
schema: z.object({
id: z.string(),
nome: z.string(),
bio: z.string(),
avatar: z.string().url(),
}),
});
export const collections = { blog, autores };Consultando collections
---
import { getCollection, getEntry, render } from 'astro:content';
// Todos os posts (filtrando rascunhos)
const posts = await getCollection('blog', ({ data }) => !data.draft);
// Posts ordenados por data
const postsByDate = posts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
// Post específico pelo ID (nome do arquivo sem extensão)
const post = await getEntry('blog', 'meu-primeiro-post');
if (!post) return Astro.redirect('/404');
// Renderizar o Markdown/MDX como componente
const { Content, headings } = await render(post);
---
<ul>
{postsByDate.map(post => (
<li>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
<time>{post.data.pubDate.toLocaleDateString('pt-BR')}</time>
</li>
))}
</ul>
<!-- Em uma página de detalhe: -->
<article>
<h1>{post.data.title}</h1>
<Content /> <!-- Componente com o conteúdo renderizado -->
</article>Imagens em collections
// schema com imagem — Astro otimiza automaticamente
import { image } from 'astro:content';
schema: z.object({
cover: image(), // referência a src/assets/
// ou
coverUrl: z.string().url(), // URL externa
})---
import { Image } from 'astro:assets';
const posts = await getCollection('blog');
---
{posts.map(post => (
<Image src={post.data.cover} alt={post.data.title} width={800} />
))}Modos de Output: SSG, SSR e Hybrid
Astro tem três modos de output configurados em astro.config.mjs:
| Mode | output | Comportamento |
|---|---|---|
| Static (SSG) | 'static' (default) | Tudo pré-renderizado em build time |
| Server (SSR) | 'server' | Tudo renderizado por request (requer adapter) |
| Hybrid | 'hybrid' | SSR por padrão, opt-in estático com export const prerender = true |
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server', // ou 'static' ou 'hybrid'
adapter: node({ mode: 'standalone' }),
});prerender por página
---
// No modo 'server' — esta página vira estática
export const prerender = true;
------
// No modo 'hybrid' — esta página passa a ser SSR
export const prerender = false;
// Agora você tem acesso ao request
const { searchParams } = Astro.url;
const query = searchParams.get('q');
// Cookies, headers, redirect
const token = Astro.cookies.get('auth');
if (!token) return Astro.redirect('/login');
---Adapters disponíveis
# Node.js (servidor próprio ou Docker)
npx astro add node
# Vercel
npx astro add vercel
# Netlify
npx astro add netlify
# Cloudflare Workers
npx astro add cloudflareAPI Routes (Endpoints)
Arquivos .ts ou .js em src/pages/ exportando funções HTTP viram endpoints de API.
// src/pages/api/posts.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
export const GET: APIRoute = async ({ request, url }) => {
const posts = await getCollection('blog');
const tag = url.searchParams.get('tag');
const filtered = tag
? posts.filter(p => p.data.tags.includes(tag))
: posts;
return new Response(JSON.stringify(filtered.map(p => p.data)), {
headers: { 'Content-Type': 'application/json' },
});
};
export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
// processar...
return new Response(JSON.stringify({ ok: true }), { status: 201 });
};Middleware
Middleware intercepta requests e responses — útil para autenticação, logging, headers de segurança, redirecionamentos globais.
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const { request, cookies, redirect, url } = context;
// Rotas protegidas
if (url.pathname.startsWith('/dashboard')) {
const token = cookies.get('auth-token');
if (!token) return redirect('/login');
}
// Adiciona dados ao contexto (acessível via Astro.locals)
context.locals.user = await getUserFromToken(cookies.get('auth-token')?.value);
// Continua para a página
const response = await next();
// Manipula a response (adiciona headers)
response.headers.set('X-Frame-Options', 'DENY');
return response;
});---
// Em qualquer página — acessando locals definidos no middleware
const { user } = Astro.locals;
---
<p>Bem-vindo, {user?.name}</p>Tipando locals
// src/env.d.ts
declare namespace App {
interface Locals {
user: { id: string; name: string; email: string } | null;
}
}Actions
Actions são funções type-safe para mutações de dados — chamadas de formulários ou JavaScript do cliente. Substituem API Routes para casos de uso de mutation.
// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
export const server = {
enviarContato: defineAction({
input: z.object({
nome: z.string().min(2),
email: z.string().email(),
mensagem: z.string().min(10),
}),
handler: async ({ nome, email, mensagem }) => {
await enviarEmail({ nome, email, mensagem });
return { sucesso: true };
},
}),
};---
// src/pages/contato.astro
import { actions } from 'astro:actions';
// Processar submit de formulário no servidor
const result = Astro.getActionResult(actions.enviarContato);
---
<!-- Formulário com action progressiva — funciona sem JS -->
<form method="POST" action={actions.enviarContato}>
<input name="nome" required />
<input name="email" type="email" required />
<textarea name="mensagem" required></textarea>
<button type="submit">Enviar</button>
{result?.error && <p class="error">{result.error.message}</p>}
{result?.data?.sucesso && <p class="success">Mensagem enviada!</p>}
</form>// Ou chamando via JS no cliente (React, Vue, Svelte…)
import { actions } from 'astro:actions';
const { data, error } = await actions.enviarContato({
nome: 'Rafael',
email: 'rafael@example.com',
mensagem: 'Olá!',
});Imagens
Astro processa e otimiza imagens automaticamente via astro:assets. Converte para WebP/AVIF, aplica loading="lazy" e decoding="async", previne CLS com dimensões inferidas.
---
import { Image, Picture } from 'astro:assets';
import heroBg from '../assets/hero.jpg';
---
<!-- Image — formato único otimizado -->
<Image
src={heroBg}
alt="Hero background"
width={1200}
height={600}
format="webp"
quality={85}
/>
<!-- Picture — múltiplos formatos com fallback -->
<Picture
src={heroBg}
formats={['avif', 'webp']}
alt="Hero"
width={1200}
/>
<!-- Imagem externa (URL) -->
<Image
src="https://exemplo.com/foto.jpg"
alt="Foto externa"
width={400}
height={300}
inferSize
/>Para imagens em CSS (background), use getImage():
---
import { getImage } from 'astro:assets';
import bg from '../assets/bg.jpg';
const bgOtimizado = await getImage({ src: bg, format: 'webp', width: 1920 });
---
<div style={`background-image: url(${bgOtimizado.src})`}>...</div>View Transitions
View Transitions permitem animações suaves entre páginas — sem SPA completo. O Astro usa a View Transitions API do browser com fallback.
---
// src/layouts/BaseLayout.astro
import { ViewTransitions } from 'astro:transitions';
---
<head>
<ViewTransitions /> <!-- Habilita view transitions em todo o site -->
</head><!-- Animação customizada por elemento -->
<h1 transition:name="titulo-post">Título do Post</h1>
<img transition:name="capa-post" src={post.cover} alt="" />
<!-- Animação builtin (fade, slide, morph, none) -->
<div transition:animate="slide">Conteúdo</div>
<div transition:animate="fade">Conteúdo</div>
<!-- Manter elemento persistente entre páginas (não re-renderiza) -->
<MusicPlayer transition:persist />Como funciona: Quando o usuário navega, o Astro faz fetch da próxima página via JS, compara os elementos com
transition:nameiguais e anima a transição entre eles. Para o usuário parece uma SPA; na verdade são páginas independentes.
Integrações
Adicione suporte a frameworks, ferramentas e serviços com um comando:
# UI Frameworks
npx astro add react # React + ReactDOM
npx astro add vue # Vue 3
npx astro add svelte # Svelte 5
npx astro add solid # SolidJS
npx astro add preact # Preact
# Styling
npx astro add tailwind # Tailwind CSS v4
npx astro add sass # SCSS/Sass
# Funcionalidades
npx astro add mdx # Suporte a MDX (MD + JSX)
npx astro add sitemap # Gera sitemap.xml automaticamente
npx astro add partytown # Move scripts de terceiros para web worker
npx astro add db # Astro DB (SQLite gerenciado)// astro.config.mjs — exemplo com múltiplas integrações
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import vue from '@astrojs/vue';
import tailwind from '@astrojs/tailwind';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://meusite.com',
integrations: [
react(),
vue(),
tailwind({ applyBaseStyles: false }),
sitemap(),
],
});astro.config.mjs — Referência Completa
import { defineConfig } from 'astro/config';
export default defineConfig({
// URL do site em produção — necessário para sitemap, canonical URLs
site: 'https://meusite.com',
// Base para deploy em subpasta (ex: GitHub Pages)
base: '/meu-repo',
// Modo de output
output: 'static', // 'static' | 'server' | 'hybrid'
// Servidor de dev
server: {
port: 4321,
host: true, // expõe na rede local
},
// Build
build: {
assets: '_assets', // pasta de assets no output
},
// Vite passthrough — qualquer config Vite funciona aqui
vite: {
plugins: [],
define: {
'import.meta.env.CUSTOM': JSON.stringify('valor'),
},
ssr: {
noExternal: ['algum-pacote'],
},
},
// Markdown
markdown: {
shikiConfig: {
theme: 'github-dark',
langs: ['typescript', 'bash', 'astro'],
},
remarkPlugins: [],
rehypePlugins: [],
},
// Aliases de import
// Configurar no tsconfig.json para suporte TypeScript:
// "paths": { "~/*": ["./src/*"] }
});Variáveis de Ambiente
# .env
PUBLIC_API_URL=https://api.exemplo.com # Exposta no cliente e servidor
SECRET_API_KEY=sk-xxx # Apenas no servidor (sem PUBLIC_)---
// No servidor (frontmatter) — acessa todas
const apiKey = import.meta.env.SECRET_API_KEY;
const apiUrl = import.meta.env.PUBLIC_API_URL;
---// No cliente (script no browser) — só PUBLIC_*
const url = import.meta.env.PUBLIC_API_URL; // ok
const key = import.meta.env.SECRET_API_KEY; // undefined (não exposta)Para validação das env vars com tipo seguro:
// src/env.d.ts
interface ImportMetaEnv {
readonly PUBLIC_API_URL: string;
readonly SECRET_API_KEY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}Patterns Frequentes
Fetch de dados no frontmatter
---
// Roda no servidor — pode chamar banco, API, arquivo, qualquer coisa
const res = await fetch('https://api.github.com/users/rafaelmarques');
const user = await res.json();
// Em SSG: chamado 1x em build time
// Em SSR: chamado a cada request
---
<h1>{user.name}</h1>
<p>Repositórios: {user.public_repos}</p>Páginas 404 customizadas
---
// src/pages/404.astro
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Página não encontrada">
<h1>404 — Não encontrado</h1>
<a href="/">Voltar ao início</a>
</BaseLayout>Redirect
---
// Redirect no servidor (SSR ou getStaticPaths)
if (!Astro.locals.user) {
return Astro.redirect('/login', 302);
}
---Head dinâmico com SEO
---
// src/components/SEO.astro
interface Props {
title: string;
description: string;
image?: string;
canonical?: string;
}
const { title, description, image, canonical } = Astro.props;
const url = canonical ?? Astro.url.href;
---
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
{image && <meta property="og:image" content={image} />}
<meta name="twitter:card" content="summary_large_image" />