Frontend

Astro

Referência completa de Astro — islands architecture, componentes, roteamento, content collections, SSR/SSG, middleware, actions e integrações.

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.json

Regra 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" />
DiretivaQuando hidrataUso ideal
client:loadImediatamenteInteratividade crítica above-the-fold
client:idleBrowser ociosoComponentes não essenciais
client:visibleAo entrar na viewportConteúdo abaixo do fold
client:mediaCondição de mídiaComponentes só para mobile/desktop
client:onlySó no cliente (sem SSR)Depende de window/document

Por que isso importa: Um site com 5 ilhas interativas e client:visible envia 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/primeiro

Rotas 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

  1. Rotas estáticas (/sobre.astro) têm prioridade sobre dinâmicas (/[slug].astro)
  2. Rotas com mais segmentos têm prioridade: /blog/2024/post > /blog/[slug]
  3. 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:

ModeoutputComportamento
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 cloudflare

API 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:name iguais 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" />