Publicando Sites com Astro
Crie sites estáticos rápidos e focados em conteúdo com a abordagem SSG de runtime zero do Astro, hidratação parcial e suporte extensivo a Markdown.
Conteúdo
- Início Rápido
- Quando Não Usar
- Estrutura do Projeto
- SSG vs SSR vs Híbrido
- Coleções de Conteúdo — API Content Layer, Carregadores Personalizados
- Realce de Sintaxe — Shiki, Transformadores, Expressive Code
- Integração de Diagramas — Mermaid, PlantUML, Temas para Modo Escuro
- Busca do Lado do Cliente — Pagefind (controles, ponderação), Fuse.js
- Documentação Versionada — Múltiplas versões
- Internacionalização (i18n) — Roteamento, Fallbacks
- Padrões Comuns — Paginação, Tags, RSS, Formulários
- Melhores Práticas de Desempenho — Pré-busca, CSS Crítico
- Publicação (Deploy) — Netlify
- Checklist Pré-Publicação
- Testes e Qualidade — Vitest, Playwright, Verificação de Links
- Solução de Problemas
Início Rápido
# Crie um novo projeto (use o template Blog para sites Markdown)
npm create astro@latest
# Desenvolvimento
npm run dev # Servidor local em http://localhost:4321
npm run build # Gera arquivos estáticos em dist/
npm run preview # Visualiza a build de produção
Quando Não Usar
Este skill foca em geração de sites estáticos (SSG). Considere outras abordagens para:
- Aplicações com dados em tempo real - Use o modo SSR com conexões a banco de dados
- Fluxos de autenticação de usuário - Requer manipulação de sessão no lado do servidor
- E-commerce com inventário dinâmico - Use modo híbrido ou SSR completo
- Aplicações de página única (SPAs) - Considere frameworks React/Vue diretamente
Para padrões híbridos SSG+SSR, veja a documentação de adaptadores do Astro.
Estrutura do Projeto
src/
components/ # Componentes Astro, React, Vue, Svelte
content/ # Coleções de Conteúdo (Markdown/MDX)
config.ts # Schemas das coleções
docs/ # Exemplo de coleção
layouts/ # Wrappers de página com slots
pages/ # Roteamento baseado em arquivos
public/ # Ativos estáticos (imagens, fontes, favicons)
astro.config.mjs # Configuração do framework
SSG vs SSR vs Híbrido
| Modo | Quando as Páginas Renderizam | Caso de Uso |
|---|---|---|
| SSG (padrão) | Tempo de build | Blogs, documentação, sites de marketing |
| SSR | Cada requisição | Dados dinâmicos, personalização |
| Híbrido | Mistura de ambos | Páginas estáticas + endpoints dinâmicos |
Para sites puramente estáticos, use o padrão output: 'static' — nenhum adaptador é necessário.
Coleções de Conteúdo
Padrão Legado (Astro 4.x)
Defina schemas em src/content/config.ts:
import { defineCollection, z } from "astro:content";
export const collections = {
docs: defineCollection({
schema: z.object({
title: z.string(),
description: z.string().optional(),
tags: z.array(z.string()).optional(),
order: z.number().optional(),
draft: z.boolean().default(false)
})
})
};
API Content Layer (Astro 5.0+)
Novo padrão com o carregador glob() — até 75% mais rápido em builds para sites grandes:
// src/content.config.ts (nota: nome de arquivo diferente)
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
schema: ({ image }) => z.object({
title: z.string(),
pubDate: z.coerce.date(),
draft: z.boolean().default(false),
cover: image(), // Valida que a imagem existe
author: reference('authors'), // Referência entre coleções
})
});
export const collections = { blog };
Padrões Avançados de Schema
schema: ({ image }) => z.object({
cover: image(), // Valida imagem em src/
category: z.enum(['tech', 'news']),
author: reference('authors'), // Ref entre coleções
relatedPosts: z.array(reference('blog')).optional(),
})
Carregadores Personalizados (Conteúdo Remoto)
Busque conteúdo de APIs externas (lançamentos do GitHub, CMS, etc.):
// src/loaders/github-releases.ts
import type { Loader } from 'astro/loaders';
export function githubReleasesLoader(repo: string): Loader {
return {
name: 'github-releases',
load: async ({ store, logger }) => {
logger.info(`Buscando lançamentos para ${repo}`);
const response = await fetch(`https://api.github.com/repos/${repo}/releases`);
const releases = await response.json();
for (const release of releases) {
store.set({
id: release.tag_name,
data: {
version: release.tag_name,
published_at: release.published_at,
body: release.body // Notas de lançamento em Markdown
}
});
}
}
};
}
Registre em content.config.ts:
import { githubReleasesLoader } from './loaders/github-releases';
const releases = defineCollection({
loader: githubReleasesLoader('owner/repo'),
schema: z.object({
version: z.string(),
published_at: z.string(),
body: z.string(),
})
});
Consulte e renderize coleções:
---
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const docs = await getCollection("docs");
return docs.map(doc => ({
params: { slug: doc.slug },
props: { doc }
}));
}
const { doc } = Astro.props;
const { Content } = await doc.render();
---
<article>
<h1>{doc.data.title}</h1>
<Content />
</article>
Realce de Sintaxe
Configuração Básica do Shiki
import { defineConfig } from "astro/config";
export default defineConfig({
markdown: {
shikiConfig: {
theme: "github-dark",
wrap: true
}
}
});
Tema Duplo Claro/Escuro
shikiConfig: {
themes: {
light: 'github-light',
dark: 'github-dark',
},
}
Adicione CSS para alternar temas:
@media (prefers-color-scheme: dark) {
.astro-code, .astro-code span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
}
Realce de Linhas e Transformadores
```typescript {2,4}
const a = 1;
const b = 2; // destacado
const c = 3;
console.log(a + b + c); // destacado
```
~~
**Transformadores Shiki (Astro 4.14+):**
```javascript
import { transformerNotationFocus, transformerNotationDiff } from '@shikijs/transformers';
shikiConfig: {
transformers: [transformerNotationFocus(), transformerNotationDiff()],
}
```
Use comentários de notação no código:
- `// [!code focus]` - Foca esta linha
- `// [!code ++]` - Marca como adição (verde)
- `// [!code --]` - Marca como remoção (vermelho)
### Expressive Code (Recomendado para Documentação)
Blocos de código ricos com botão de copiar, nomes de arquivo, realce de diferenças:
```bash
npm install astro-expressive-code
```
```javascript
import expressiveCode from 'astro-expressive-code';
export default defineConfig({
integrations: [expressiveCode()],
});
```
Funcionalidades: Botão de copiar, abas de arquivo, marcadores de linha, frames de terminal, marcadores de texto.
## Integração de Diagramas
### Mermaid (Recomendado)
Instale a integração do Astro:
```bash
npm install astro-mermaid mermaid
```
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mermaid from 'astro-mermaid';
export default defineConfig({
integrations: [mermaid({ theme: 'default' })]
});
```
Use em Markdown:
~~~markdown
```mermaid
graph TD;
A-->B;
B-->C;
```
~~
Funcionalidades: Renderização no lado do cliente, alternância automática de tema, funcionamento offline, sem necessidade de Playwright.
**Alternativa (SVG estático em tempo de build):** Use `rehype-mermaid` com Playwright para diagramas pré-renderizados (requer `npx playwright install --with-deps`).
**Estratégias de Tema para Modo Escuro:**
1. **Variáveis CSS** - Deixe o navegador resolver as cores em tempo de execução:
```javascript
// config mermaid
mermaid.initialize({
theme: 'base',
themeVariables: {
primaryColor: 'var(--diagram-primary)',
lineColor: 'var(--diagram-line)'
}
});
```
2. **Elemento Picture** - Gere ambos os temas, alterne com media query:
```html
<picture>
<source srcset="/diagrams/flow-dark.svg" media="(prefers-color-scheme: dark)">
<img src="/diagrams/flow-light.svg" alt="Diagrama de fluxo">
</picture>
```
3. **SVG Inline** - Segmente classes SVG com CSS (risco: conflitos de estilo) - adicione critérios de acessibilidade com possibilidade de ter role='image' ou role="presentation":
```css
.dark .mermaid-svg .node rect {
fill: var(--bg-dark);
}
```
### PlantUML
```bash
npx astro add plantuml
```
Use em Markdown:
~~~markdown
```plantuml
@startuml
Alice -> Bob: Olá
Bob --> Alice: Oi!
@enduml
```
~~
## Busca do Lado do Cliente
### Pagefind (Recomendado para Sites Grandes)
Busca estática sem configuração que indexa no tempo de build:
```bash
npm install pagefind
```
Adicione ao `package.json`:
```json
{
"scripts": {
"build": "astro build && npx pagefind --site dist",
"postbuild": "pagefind --site dist"
}
}
```
Use em componentes:
```astro
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-ui.js" type="text/javascript"></script>
<div id="search"></div>
<script>
window.addEventListener('DOMContentLoaded', () => {
new PagefindUI({ element: '#search', showSubResults: true });
});
</script>
```
Funcionalidades: Sem serviço externo, funciona offline, indexação automática, bundle pequeno (~10KB).
**Controle Granular de Indexação:**
```html
<!-- Indexe apenas o conteúdo principal, não cabeçalhos/barras laterais -->
<main data-pagefind-body>
<h1 data-pagefind-meta="title">Título da Página</h1>
<p data-pagefind-weight="10">Texto introdutório importante</p>
<!-- Exclua de trechos de busca -->
<nav data-pagefind-ignore>
<a href="/related">Posts Relacionados</a>
</nav>
</main>
```
| Atributo | Propósito |
|-----------|-----------|
| `data-pagefind-body` | Limita a indexação a este elemento apenas |
| `data-pagefind-ignore` | Exclui elemento do índice |
| `data-pagefind-meta="chave"` | Define campo de metadados |
| `data-pagefind-weight="10"` | Aumenta relevância (padrão: 1) |
### Pagefind vs Fuse.js
| Funcionalidade | Pagefind | Fuse.js |
|----------------|----------|---------|
| Arquitetura | Blocos binários pré-construídos | Em memória em tempo de execução |
| Largura de banda | Baixa (carrega apenas blocos necessários) | Alta (baixa índice completo) |
| Escalabilidade | 10.000+ páginas | < 500 páginas |
| Multilíngue | Stemming nativo | Configuração manual |
| Caso de Uso | Busca global no site | Filtragem de listas pequenas |
### Fuse.js (Alternativa Leve)
Para sites menores com necessidades de UI personalizada:
```astro
---
import { getCollection } from "astro:content";
const posts = await getCollection('blog');
const searchIndex = JSON.stringify(posts.map(post => ({
title: post.data.title,
slug: post.slug,
body: post.body.slice(0, 500)
})));
---
<input type="search" id="search" placeholder="Buscar..." />
<ul id="results"></ul>
<script define:vars={{ searchIndex }}>
import Fuse from 'fuse.js';
const fuse = new Fuse(JSON.parse(searchIndex), {
keys: ['title', 'body'],
threshold: 0.3
});
document.getElementById('search').addEventListener('input', (e) => {
const results = fuse.search(e.target.value);
const resultsEl = document.getElementById('results');
// Limpa resultados anteriores com segurança
resultsEl.replaceChildren();
// Constrói resultados usando métodos DOM seguros
results.forEach(r => {
const li = document.createElement('li');
const link = document.createElement('a');
link.href = `/blog/${r.item.slug}`;
link.textContent = r.item.title;
li.appendChild(link);
resultsEl.appendChild(li);
});
});
</script>
```
Para necessidades empresariais, considere Algolia (API de busca hospedada).
## Documentação Versionada
### Padrão de Documentação com Múltiplas Versões
Use estrutura baseada em pastas:
```
src/content/docs/
v1/
getting-started.md
api-reference.md
v2/
getting-started.md
api-reference.md
new-feature.md
```
## Internacionalização (i18n)
Configure em `astro.config.mjs`:
```javascript
export default defineConfig({
i18n: {
defaultLocale: "en",
locales: ["en", "fr", "es"],
routing: {
prefixDefaultLocale: false
}
}
});
```
Estruture o conteúdo por localidade:
```
src/content/docs/
en/
getting-started.md
fr/
getting-started.md
```
Detecte a localidade em componentes:
```astro
---
const locale = Astro.currentLocale || 'en';
---
```
### Fallback para Traduções Ausentes
Mostre conteúdo em inglês com um banner quando as traduções não existirem:
```astro
---
// src/pages/[lang]/[...slug].astro
import { getCollection, getEntry } from "astro:content";
const languages = ['en', 'es', 'fr'];
const defaultLang = 'en';
export async function getStaticPaths() {
const englishDocs = await getCollection('docs', ({ id }) => id.startsWith('en/'));
const paths = [];
for (const doc of englishDocs) {
const slug = doc.id.replace(/^en\//, '');
for (const lang of languages) {
const localizedId = `${lang}/${slug}`;
const localizedDoc = await getEntry('docs', localizedId);
paths.push({
params: { lang, slug },
props: {
entry: localizedDoc || doc, // Fallback para inglês
isFallback: !localizedDoc
}
});
}
}
return paths;
}
const { entry, isFallback } = Astro.props;
const { Content } = await entry.render();
---
{isFallback && (
<div class="translation-notice">
Esta página ainda não está disponível no seu idioma.
</div>
)}
<Content />
```
## Padrões Comuns
### Listagens Paginadas
Para sites com mais de 50 posts, divida em páginas:
```astro
---
// src/pages/blog/page/[page].astro
import { getCollection } from "astro:content";
const POSTS_PER_PAGE = 10;
export async function getStaticPaths() {
const allPosts = await getCollection("blog");
const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
const paths = [];
for (let page = 1; page <= totalPages; page++) {
paths.push({
params: { page: String(page) },
props: { page, totalPages }
});
}
return paths;
}
const { page, totalPages } = Astro.props;
const start = (page - 1) * POSTS_PER_PAGE;
const end = start + POSTS_PER_PAGE;
const posts = (await getCollection("blog"))
.filter(post => !post.data.draft)
.slice(start, end);
---
<h1>Blog - Página {page}</h1>
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
<span>{post.data.pubDate.toLocaleDateString()}</span>
</li>
))}
</ul>
<nav>
{page > 1 && <a href={`/blog/page/${page - 1}`}>Anterior</a>}
{page < totalPages && <a href={`/blog/page/${page + 1}`}>Próxima</a>}
</nav>
```
### Tags e Categorias
Gere páginas de arquivo por tag:
```astro
---
// src/pages/tags/[tag].astro
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const posts = await getCollection("blog");
const tags = [...new Set(posts.flatMap(p => p.data.tags || []))];
return tags.map(tag => ({
params: { tag },
props: {
posts: posts.filter(p => p.data.tags?.includes(tag))
}
}));
}
const { tag } = Astro.params;
const { posts } = Astro.props;
---
<h1>Tag: {tag}</h1>
<ul>
{posts.map(post => (
<li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li>
))}
</ul>
```
### Feed RSS
Gere um feed RSS usando a integração `@astrojs/rss`:
```bash
npm install @astrojs/rss
```
```astro
---
// 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: 'Meu Blog',
description: 'Um blog sobre construção de sites com Astro',
site: context.site,
items: posts.map(post => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/blog/${post.slug}/`
}))
});
}
```
### Formulários
Para gerenciamento de formulários, considere soluções sem servidor como Formspree, Netlify Forms, ou um endpoint personalizado em um adaptador SSR.
## Melhores Práticas de Desempenho
### Pré-busca (Prefetching)
Habilite a pré-busca integrada para navegação instantânea:
```javascript
// astro.config.mjs
export default defineConfig({
prefetch: true
});
```
Use o componente `ClientRouter` para pré-buscar links quando eles ficarem visíveis.
### CSS Crítico
Inclua inline o CSS crítico e carregue o restante de forma assíncrona para melhorar a First Contentful Paint (FCP).
## Publicação (Deploy)
### Netlify
Instale o adaptador:
```bash
npm install @astrojs/netlify
```
```javascript
import netlify from '@astrojs/netlify';
export default defineConfig({
output: 'static', // ou 'server' para SSR
adapter: netlify()
});
```
Crie um arquivo `netlify.toml`:
```toml
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
```
### Vercel
```bash
npm install @astrojs/vercel
```
```javascript
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server',
adapter: vercel()
});
```
### GitHub Pages
```bash
npm install @astrojs/github-pages
```
Configure o caminho base se o site estiver em um subdiretório:
```javascript
export default defineConfig({
site: 'https://usuario.github.io/repositorio',
base: '/repositorio',
adapter: githubPages()
});
```
### GCP/Firebase
Use o adaptador Firebase:
```bash
npm install @astrojs/firebase
```
## Checklist Pré-Publicação
- [ ] Testar todos os links internos e externos
- [ ] Verificar SEO: meta tags, Open Graph, descrições
- [ ] Configurar redirecionamentos 301 se necessário
- [ ] Otimizar imagens (formato, tamanho, lazy loading)
- [ ] Verificar sitemap.xml e robots.txt
- [ ] Configurar análises (Google Analytics, Plausible, etc.)
- [ ] Testar em dispositivos móveis e diferentes navegadores
## Testes e Qualidade
### Vitest (Testes Unitários)
```bash
npm install vitest
```
```javascript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node'
}
});
```
### Playwright (Testes End-to-End)
```bash
npm install @playwright/test
npx playwright install
```
```javascript
// e2e/home.spec.js
import { test, expect } from '@playwright/test';
test('home page has title', async ({ page }) => {
await page.goto('http://localhost:4321');
await expect(page).toHaveTitle(/Meu Blog/);
});
```
### Verificação de Links
Use ferramentas como `linkinator` ou `broken-link-checker` para verificar links quebrados:
```bash
npx linkinator http://localhost:4321
```
## Solução de Problemas
- **Erro `Module not found`**: Verifique se todos os pacotes estão instalados (`npm install`).
- **Página não atualiza**: Limpe o cache do Astro com `rm -rf .astro`.
- **Imagens não aparecem**: Confirme que estão em `public/` ou use o formato de import do Astro.
---
*Este guia cobre as práticas essenciais para construir e publicar sites Astro. Para mais detalhes, consulte a [documentação oficial do Astro](https://docs.astro.build).*