Quando Usar
- Landing pages e sites de marketing
- Sites corporativos
- Sites com muito conteúdo (blogs, documentação)
- Qualquer projeto onde SEO e desempenho são críticos
- Sites que são principalmente estáticos com pouca interatividade
- Sites que precisam de carregamento inicial rápido e Core Web Vitals
NÃO para aplicações web completas — use vue-frontend para aplicações interativas (dashboards, CRUD, tempo real).
Regras Críticas
- Zero JS por padrão — Astro NÃO envia JavaScript a menos que você adicione uma diretiva
client: - TypeScript SEMPRE — igual a todos os projetos Vera Sesiom, modo estrito
- Use Content Collections para conteúdo estruturado — posts de blog, documentação, estudos de caso
- Arquitetura de Ilhas — componentes Vue/React apenas onde interatividade é necessária
- Saída estática (SSG) por padrão — SSR apenas quando conteúdo dinâmico exigir
- Tailwind CSS para estilização — igual ao frontend Vue
- Screaming Architecture — pastas gritam o domínio, não o framework
Critérios de Decisão: Astro vs Vue
| Quando usar Astro | Quando usar Vue |
|---|---|
| Landing page, site de marketing | Aplicação web (dashboard, admin) |
| Muito conteúdo (blog, docs) | UI altamente interativa |
| SEO é crítico | Funcionalidades em tempo real |
| Conteúdo majoritariamente estático | Gerenciamento de estado complexo |
| Interatividade mínima (formulários, carrosséis) | Navegação SPA completa |
| Zero JS é uma funcionalidade | Benefícios do ecossistema Vue importam |
Regra: Se o site é conteúdo que as pessoas LEEM → Astro. Se é um aplicativo que as pessoas USAM → Vue.
Estrutura do Projeto (Screaming Architecture)
apps/landing/ (ou apps/website/)
├── src/
│ ├── pages/ # Roteamento baseado em arquivos (.astro)
│ │ ├── index.astro # Página inicial
│ │ ├── about.astro # Página sobre
│ │ ├── pricing.astro # Página de preços
│ │ ├── blog/
│ │ │ ├── index.astro # Lista do blog
│ │ │ └── [slug].astro # Post do blog (dinâmico)
│ │ └── contact.astro
│ ├── layouts/ # Layouts de página
│ │ ├── BaseLayout.astro # Estrutura HTML base
│ │ ├── BlogLayout.astro # Layout de post do blog
│ │ └── LandingLayout.astro # Layout de landing page
│ ├── components/ # Componentes Astro (estáticos por padrão)
│ │ ├── sections/ # Seções de página
│ │ │ ├── Hero.astro
│ │ │ ├── Features.astro
│ │ │ ├── Pricing.astro
│ │ │ ├── Testimonials.astro
│ │ │ └── CTA.astro
│ │ ├── ui/ # Átomos de UI reutilizáveis
│ │ │ ├── Button.astro
│ │ │ ├── Card.astro
│ │ │ ├── Badge.astro
│ │ │ └── Input.astro
│ │ └── interactive/ # Ilhas Vue/React
│ │ ├── ContactForm.vue # Formulário com validação
│ │ ├── Carousel.vue # Carrossel de imagens
│ │ └── MobileMenu.vue # Navegação móvel
│ ├── content/ # Coleções de conteúdo
│ │ ├── blog/ # Posts de blog em Markdown/MDX
│ │ │ ├── post-1.md
│ │ │ └── post-2.mdx
│ │ └── config.ts # Esquema da coleção (Zod)
│ ├── styles/ # Estilos globais
│ │ └── global.css # Imports do Tailwind
│ ├── assets/ # Imagens otimizadas, fontes
│ │ ├── images/
│ │ └── fonts/
│ └── utils/ # Funções auxiliares
│ ├── date-format.ts
│ └── seo.ts
├── public/ # Ativos estáticos (sem processamento)
│ ├── favicon.ico
│ ├── robots.txt
│ └── fonts/
├── astro.config.mjs # Configuração do Astro
├── tailwind.config.mjs # Configuração do Tailwind
├── tsconfig.json
└── package.json
Padrão de Componente Astro
Todo arquivo .astro tem duas partes: frontmatter (lado do servidor) e template (HTML).
---
// src/components/ui/Button.astro
// Frontmatter: executa no servidor, NÃO no JS do navegador
export interface Props {
variant?: 'primary' | 'secondary' | 'outline'
size?: 'sm' | 'md' | 'lg'
href?: string
}
const {
variant = 'primary',
size = 'md',
href,
...rest
} = Astro.props
const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors'
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
outline: 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50'
}
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
}
const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`
---
{href ? (
<a href={href} class={classes} {...rest}>
<slot />
</a>
) : (
<button class={classes} {...rest}>
<slot />
</button>
)}
Pontos-chave:
- Frontmatter executa em tempo de build (ou tempo de requisição para SSR)
- Não precisa de
export default— o template JÁ É o export <slot />para conteúdo filho- Props são tipadas com interfaces TypeScript
Padrão de Layout
---
// src/layouts/BaseLayout.astro
import Nav from '../components/Nav.astro'
import Footer from '../components/Footer.astro'
export interface Props {
title: string
description?: string
image?: string
}
const { title, description = '', image = '/og-default.png' } = Astro.props
const canonicalURL = new URL(Astro.url.pathname, Astro.site)
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="canonical" href={canonicalURL} />
<title>{title}</title>
<meta name="description" content={description} />
<!-- OpenGraph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:url" href={canonicalURL} />
<meta property="og:type" content="website" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<slot name="head" />
</head>
<body class="bg-white text-gray-900 antialiased">
<Nav />
<main>
<slot />
</main>
<Footer />
</body>
</html>
Uso:
---
// src/pages/index.astro
import BaseLayout from '../layouts/BaseLayout.astro'
import Hero from '../components/sections/Hero.astro'
---
<BaseLayout title="Vera Sesiom - Desenvolvimento de Software" description="Construímos software que importa.">
<Hero />
</BaseLayout>
Padrão de Coleção de Conteúdo
Defina conteúdo estruturado com validação de esquema Zod.
// src/content/config.ts
import { defineCollection, z } from 'astro:content'
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
author: z.string().default('Vera Sesiom'),
image: z.object({
url: z.string(),
alt: z.string()
}).optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false)
})
})
export const collections = {
blog: blogCollection
}
---
title: "Por que Escolhemos Astro para Landing Pages"
description: "Desempenho e experiência do desenvolvedor importam. Veja por que Astro é nossa escolha para sites de conteúdo."
pubDate: 2024-01-15
author: "Líder Técnico"
image:
url: "/images/blog/astro-landing.jpg"
alt: "Logo do Astro em um site moderno"
tags: ["astro", "desempenho", "arquitetura"]
---
Conteúdo vai aqui...
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content'
import BaseLayout from '../../layouts/BaseLayout.astro'
const posts = (await getCollection('blog'))
.filter(post => !post.data.draft)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
---
<BaseLayout title="Blog" description="Artigos recentes da Vera Sesiom">
<h1>Blog</h1>
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>
<h2>{post.data.title}</h2>
<p>{post.data.description}</p>
<time>{post.data.pubDate.toLocaleDateString()}</time>
</a>
</li>
))}
</ul>
</BaseLayout>
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content'
import BaseLayout from '../../layouts/BaseLayout.astro'
export async function getStaticPaths() {
const posts = await getCollection('blog')
return posts.map(post => ({
params: { slug: post.slug },
props: { post }
}))
}
const { post } = Astro.props
const { Content } = await post.render()
---
<BaseLayout title={post.data.title} description={post.data.description}>
<article>
<header>
<h1>{post.data.title}</h1>
<time>{post.data.pubDate.toLocaleDateString()}</time>
<p>Por {post.data.author}</p>
</header>
<Content />
</article>
</BaseLayout>
Arquitetura de Ilhas (Componentes Vue)
Componentes interativos enviam JS apenas quando necessário. Use diretivas client: para controlar a hidratação.
<!-- src/components/interactive/ContactForm.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
const form = ref({
name: '',
email: '',
message: ''
})
const errors = ref<Record<string, string>>({})
const isValid = computed(() => {
return form.value.name &&
form.value.email.includes('@') &&
form.value.message.length > 10
})
async function submitForm() {
if (!isValid.value) return
// Enviar para API
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form.value)
})
if (response.ok) {
// Lidar com sucesso
form.value = { name: '', email: '', message: '' }
}
}
</script>
<template>
<form @submit.prevent="submitForm" class="space-y-4">
<div>
<label for="name">Nome</label>
<input v-model="form.name" type="text" id="name" required />
</div>
<div>
<label for="email">Email</label>
<input v-model="form.email" type="email" id="email" required />
</div>
<div>
<label for="message">Mensagem</label>
<textarea v-model="form.message" id="message" rows="4" required />
</div>
<button type="submit" :disabled="!isValid">
Enviar Mensagem
</button>
</form>
</template>
Usando no Astro:
---
// src/pages/contact.astro
import BaseLayout from '../layouts/BaseLayout.astro'
import ContactForm from '../components/interactive/ContactForm.vue'
---
<BaseLayout title="Fale Conosco">
<h1>Entre em Contato</h1>
<p>Adoraríamos ouvir de você.</p>
<!-- client:visible = hidrata quando scrollado para a visualização (MELHOR PRÁTICA) -->
<ContactForm client:visible />
</BaseLayout>
Árvore de Decisão de Diretivas Client
| Diretiva | Quando Usar | JS Enviado |
|---|---|---|
| Sem diretiva | Conteúdo estático, sem interatividade | Zero JS |
client:visible | Interatividade abaixo da dobra (formulários, carrosséis) — PADRÃO | Quando scrollado para a visualização |
client:load | Interatividade crítica acima da dobra | Imediatamente |
client:idle | Melhorias não críticas (analytics, chat) | Após a página ficar ociosa |
client:media | Condicional por breakpoint (nav móvel) | Quando a media query corresponde |
client:only | Componentes tipo SPA (pular SSR) | Imediatamente, sem SSR |
Melhores Práticas:
- Padrão: sem diretiva — puramente estático
- Padrão:
client:visiblequando interatividade é necessária - Use
client:loadcom moderação — apenas para interatividade crítica acima da dobra - Use
client:idlepara melhorias não bloqueantes
---
<!-- Bons exemplos -->
import Hero from './Hero.astro' <!-- Estático, sem JS -->
import Carousel from './Carousel.vue'
import MobileMenu from './MobileMenu.vue'
import Chat from './Chat.vue'
import Stats from './Stats.astro' <!-- Contador estático = CSS -->
<Hero /> <!-- Estático, sem hidratação -->
<Carousel client:visible /> <!-- Hidrata quando visível -->
<MobileMenu client:media="(max-width: 768px)" /> <!-- Apenas móvel -->
<Chat client:idle /> <!-- Após a página se estabilizar -->
<Stats /> <!-- Animações CSS puras, sem JS necessário -->
Otimização de Imagens
Use astro:assets para otimização automática.
---
// src/pages/index.astro
import { Image } from 'astro:assets'
import heroImage from '../assets/images/hero.png'
---
<Image
src={heroImage}
alt="Imagem do hero"
width={800}
height={600}
loading="lazy"
format="webp"
/>
Para imagens de CMS ou fontes externas:
---
import { Image } from 'astro:assets'
---
<Image
src="https://example.com/image.jpg"
alt="Imagem externa"
width={800}
height={600}
/>
Regras:
- Sempre especifique
widtheheightpara prevenção de CLS - Use
loading="lazy"para imagens abaixo da dobra - Prefira formatos
webpouavif - Mantenha imagens do hero otimizadas e dimensionadas adequadamente
Padrão de SEO
---
// src/components/SEO.astro
export interface Props {
title: string
description: string
image?: string
article?: boolean
publishedTime?: Date
author?: string
tags?: string[]
}
const {
title,
description,
image = '/og-default.png',
article = false,
publishedTime,
author,
tags = []
} = Astro.props
const canonicalURL = new URL(Astro.url.pathname, Astro.site)
const imageURL = new URL(image, Astro.site)
---
<!-- Meta Tags Primárias -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph -->
<meta property="og:type" content={article ? 'article' : 'website'} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={imageURL} />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageURL} />
{article && publishedTime && (
<meta property="article:published_time" content={publishedTime.toISOString()} />
)}
{article && author && (
<meta property="article:author" content={author} />
)}
{tags.map(tag => (
<meta property="article:tag" content={tag} />
))}
<!-- Structured Data / JSON-LD -->
<script type="application/ld+json" set:html={JSON.stringify({
'@context': 'https://schema.org',
'@type': article ? 'Article' : 'WebPage',
headline: title,
description,
image: imageURL,
url: canonicalURL,
...(publishedTime && { datePublished: publishedTime.toISOString() })
})}>
</script>
Bônus: RSS Feed
// src/pages/rss.xml.js
import rss from '@astrojs/rss'
import { getCollection } from 'astro:content'
export async function GET(context) {
const posts = await getCollection('blog')
return rss({
title: 'Blog da Vera Sesiom',
description: 'Artigos recentes e insights',
site: context.site,
items: posts.map(post => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/blog/${post.slug}/`
}))
})
}
Trabalhando com o Template
- Monorepo — Use
pnpmworkspaces com o projeto na pastaapps/ - Construir —
pnpm run build(saída emdist/) - Dev —
pnpm run dev(com live reload e hidratação parcial) - Analisar — Verifique se Zero JS padrão permanece (sem
client:*a menos que necessário) - Acessibilidade — Sempre use elementos semânticos e atributos
alt - Desempenho — Verifique Lighthouse/Web Vitals em cada build
Resumo de Melhores Práticas
- Zero JS por padrão — Apenas hidrate quando necessário
- TypeScript estrito — Sempre, sem exceções
- Coleções de conteúdo para textos — Validasão de esquema Zod
- Ilhas Vue para interatividade — Componentes mínimos e direcionados
- Imagens otimizadas — Sempre especifique dimensões, prefira formatos modernos
- SEO completo — Meta tags, OpenGraph, JSON-LD
- Screaming Architecture — Pastas refletem o domínio do site
- Acessível por padrão — HTML semântico, contraste, navegação por teclado